Visual Basic Start Page

Visual Basic

Welcome to the Visual Basic documentation. We’ve worked to make this documentation easy to use, beginning with these links to places you can use as first stops in your search for information.

The Visual Basic Programmer's Guide is a comprehensive manual on programming with Visual Basic.

The Component Tools Guide is a suite of guides to help you create and use ActiveX-based components.

For details on data access programming in Visual Basic see the Data Access Guide.

Visual Basic is a member of the Visual Studio 6.0 family of development products, which includes:

  • Visual Basic
  • Visual C++
  • Visual FoxPro
  • Visual InterDev
  • Visual J++
  • Visual SourceSafe
  • MSDN Library
Jump to the Visual Studio 6.0 Start Page.

Copyright
Accessibility Information

Getting Started
Take just a few minutes to get familiar with Visual Basic 6.

What's New
Find out about all the new Visual Basic 6 features.

Explore the Samples
Explore the Visual Basic sample applications.

Finding Information
Learn to navigate Visual Basic's online documentation and help system in this MSDN Library browser.

Readme
Check the readme file for installation issues.

Reference
Reference information for the Visual Basic language and controls.

Visual Basic on the Web


Visual Basic Documentation Map

Visual Basic includes many documentation tools, each designed to help you learn and use a particular aspect of the product. The documentation provided with Visual Basic is listed below.

Programmer's Guide

A guide to creating applications with Visual Basic. Explains programming techniques, using frequent examples to show how the techniques work in typical applications.

Using ActiveX Controls

An introduction to the ActiveX controls — formerly called OLE controls — included with the Visual Basic Professional and Enterprise editions. Start here for general information, scenarios of real-life uses, and code. Included in the Component Tools Guide.

Creating ActiveX Components

A guide to creating ActiveX controls, ActiveX documents, and ActiveX code components (formerly OLE servers). Includes introductory step-by-step chapters as well as in-depth design guides. Included in the Component Tools Guide.

Extending the Visual Basic Environment with Add-Ins

A guide to using the Visual Basic extensibility object model to tailor the development environment to your unique needs. Use objects in the model to build add-ins that automate repetitive tasks, monitor the development environment for certain events, or perform complex tasks. Included in the Component Tools Guide.

Accessing DLLs and the Windows API

A guide to enhancing functionality provided by Visual Basic by calling functions in dynamic-link libraries (DLLs). Through DLLs, you can access the thousands of procedures that form the backbone of the Microsoft Windows operating system, as well as routines written in other languages. Included in the Component Tools Guide.

Introduction to Internet Applications

An introduction to creating applications that can run on the Internet or an intranet, presenting HTML pages inside forms-based applications, and deploying these applications and other ActiveX components on the Web. Included in the Component Tools Guide.

Data Access Guide

A guide to creating and manipulating databases using Microsoft ActiveX Data Objects (ADO) and other data access technologies.

Reference

Provides a complete reference for features available in all editions of Visual Basic. Includes sections on the programming language, controls, the development environment, and more.

Samples

Describes the sample applications provided with Visual Basic.

Microsoft DAO 3.51

Provides an overview and complete reference for Data Access Objects, a programming model that allows you to programmatically access and manipulate data in local or remote databases, and to manage databases, their objects and their structure.

Using the Repository with Visual Basic

A guide to using Microsoft Repository, an extensible object modeling system that allows full programmatic access via Automation objects. Microsoft Repository comes with the Repository Add-in, a Visual Basic add-in module that automatically stores and maintains the structure of your Visual Basic projects in an object-oriented data repository.

Other Documentation

You may find the following sets of documentation useful; these are located elsewhere in the MSDN™ library.

Active Server Pages

Provides introductory material about ASP.

Active Server Pages SDK

Provides reference information about the ASP objects, @directives, and installable components.

Internet Information Server 4.0

Internet Information Server is a platform for a new generation of World Wide Web applications that must interact with other BackOffice components and applications.

Internet Client SDK

Contains information about a variety of technologies that Microsoft has developed to facilitate development of Internet applications and content, including dynamic HTML.

Microsoft Data Access SDK

Provides information about ActiveX Data Objects (ADO), OLE DB, and Open Database Connectivity (ODBC).

Microsoft Repository 2.0

Provides information about the Microsoft Repository, which produces a common place to persist information about objects and relationships between them.

Microsoft SQL Server Programmer's Toolkit

This toolkit covers Transact-SQL, programming ODBC for SQL, and much more.

Microsoft Transaction Server 2.0

Microsoft Transaction Server (MTS) is a component-based transaction processing system for developing, deploying, and managing high-performance, scalable, and robust enterprise, Internet, and intranet server applications.

Microsoft Visual Database Tools

A guide to using the Database Designer and Query Designer to create data-driven applications that rely on live connections to Microsoft SQL Server databases.

MSDN Library Help

Explains how to use the Help browser and find information in the Microsoft Developer Network Library.

Visual Component Manager

Describes a tool that stores, organizes, and shares reusable components.

Visual SourceSafe Documentation

A guide to using Visual SourceSafe, an easy-to-use tool for storing files, controlling access to files, and maintaining multiple versions of files.

Visual Studio Enterprise Edition

A guide to distributed application development for business-critical data management. This book includes information on designing solutions based on a flexible architecture, developing large-scale data access strategies, and creating integrated solutions with Visual Studio tools.

Under Books in the table of contents, you can also find Advanced Microsoft Visual Basic 5, Hardcore Visual Basic, Internet Programming with Visual Basic 5.0, and the Microsoft Jet Database Engine Programmer's Guide.


Visual Basic Editions

Visual Basic is available in three editions, each geared to meet a specific set of development requirements. The features available to you depend on which product you have purchased.

Visual Basic Enterprise Edition Features

The Enterprise edition allows professionals to create robust distributed applications in a team setting. It includes all the features of the Professional edition, plus Back Office tools such as SQL Server, Microsoft Transaction Server, Internet Information Server, Visual SourceSafe, SNA Server, and more. Printed documentation provided with the Enterprise edition includes the Visual Studio Enterprise Features book plus Microsoft Developer Network CD’s containing full online documentation.

Visual Basic Professional Edition

The Professional edition provides computer professionals with a full-featured set of tools for developing solutions for others. It includes all the features of the Learning edition, plus additional ActiveX controls, the Internet Information Server Application Designer, Integrated Data Tools and Data Environment, Active Data Objects, and the Dynamic HTML Page Designer. Documentation provided with the Professional edition includes the Visual Studio Professional Features book plus Microsoft Developer Network CD’s containing full online documentation.

Visual Basic Learning Edition

The Visual Basic Learning edition allows programmers to easily create powerful applications for Microsoft Windows and Windows NT®. It includes all intrinsic controls, plus grid, tab, and data-bound controls. Documentation provided with this edition includes Learn VB Now (a multimedia CD-ROM title), plus Microsoft Developer Network CDs containing full online documentation.


Visual Basic Enterprise Edition Features

The Visual Basic Enterprise Edition contains all of the features included with the Visual Basic Professional Edition and the Visual Basic Learning Edition. In addition, it contains the following features, available in the Enterprise Edition only. For more information about other editions of Visual Basic, see Visual Basic Editions.

Note These features are also part of the Visual Studio Enterprise Edition, which contains the full complement of Microsoft enterprise development tools.

Application Performance Explorer

This tool enables you to testing the run-time performance and resource load characteristics of various enterprise application design scenarios you are considering.

Internet Information Server 4.0

Internet Information Server is a platform for a new generation of World Wide Web applications that must interact with other BackOffice components and applications.

Microsoft Transaction Server 2.0

Microsoft Transaction Server (MTS) is a component-based transaction processing system for developing, deploying, and managing high-performance, scalable, and robust enterprise, Internet, and intranet server applications.

OLE DB

OLE DB is a set of OLE interfaces that provide applications with uniform access to data stored in diverse information sources. These interfaces support the amount of DBMS functionality appropriate to the data source, enabling it to share its data.

RemoteData Control

The Remote Data control implements data access by using RDO, the Microsoft Remote Data Objects.

SQL Debugging

The T-SQL debugger allows you to interactively debug remote stored procedures written in Microsoft SQL Server's Transact SQL dialect, from within the Visual Basic development environment.

SQL Server 6.5

The Microsoft SQL Server Programmer's Toolkit covers Transact-SQL, programming ODBC for SQL, and much more.

Stored Procedure Editor

The SQL Editor allows you to create and edit stored procedures and triggers in both SQL Server and Oracle from within the Visual Basic development environment.

UserConnection Designer

This ActiveX designer provides design-time support for programmatic data access. This tool has been superseded in this version of Visual Basic by the Data Environment designer, listed above.

Visual Component Manager

This tool stores, organizes, and shares reusable components. It can contain wizards, ActiveX components, Database Connection objects, and many other components used in Visual Basic projects. It is suited to enterprise-wide component cataloguing and reuse because it is based on the open and scalable Microsoft Repository.

Visual Database Tools Integration (Query Designer and Database Designer)

With these designers you can visually create and modify database schemas and queries: Create SQL Server and Oracle database tables, drag and drop to create views, and automatically change column data types.

Visual SourceSafe

This source-control tool tracks changes to files and stores the changes so that files, for example code modules, can be easily and economically reused.


What's New in Visual Basic 6.0

What's New in Data Access

What's New in Internet Features

What's New in Controls

What's New in Component Creation

What's New in Language Features

What's New in Wizards

Note For Visual Basic 6.0 all the documentation for the product — as well as a lot of other essential programming information — is available in the MSDN™ Library. Click "See Also" at the top of a topic to find links to related information.


What's New in Data Access

ADO (ActiveX Data Objects)

All Editions This new data access technology features a simpler object model, better integration with other Microsoft and non-Microsoft technologies, a common interface for both local and remote data access, remotable and disconnected recordsets, a user-accessible data binding interface, and hierarchical recordsets.

Data Environment

Professional and Enterprise Edition The Data Environment designer provides an interactive, design-time environment for creating ADO objects. These can be used as a data source for data-aware objects on a form or report, or accessed programmatically as methods and properties exposed off of the Data Environment object. The Data Environment designer supports all the functionality of Visual Basic's UserConnection designer, as well as additional features, including drag and drop, hierarchies, grouping, and aggregates.

ADO Data Control

All Editions A new OLEDB-aware data source control that functions much like the intrinsic Data and Remote Data controls, in that it allows you to create a database application with minimum code.

Enhanced Data Binding

Professional and Enterprise Editions In previous versions of Visual Basic, it was only possible to bind controls together on a form. In Visual Basic 6.0, we’ve made it possible to bind any ADO/OLE DB data source to any ADO/OLE DB data consumer. You can set the DataSource property of controls at run time to dynamically bind to data sources. You can create classes that are data sources and consumers, and bind them through the new BindingsCollection object. You can create user controls that are data sources, similar to the ADO Data Control. You can create user controls that are complex-bound, similar to the DataGrid control.

OLE DB Support

All Editions OLE DB is a set of COM interfaces that provide applications with uniform access to data stored in diverse information sources, both relational and nonrelational. These interfaces support the amount of DBMS functionality appropriate to the data source, enabling it to share its data. ADO is the way that programmers access OLE DB. All the new data bound controls, the Data Environment, and the Data Report designer are OLEDB-aware.

Visual Database Tools Integration (Query Designer and Database Designer)

Enterprise Edition Visually create and modify database schemas and queries: Create SQL Server and Oracle database tables, drag and drop to create views, and automatically change column data types.

Setup Wizard Data Enhancements

All Editions The Package and Deployment Wizard (formerly the Setup Wizard) incorporates support for ADO, OLE DB, RDO, ODBC, and DAO.

Data Report

Professional and Enterprise Editions Allows you to use drag and drop to quickly create reports from any recordset, including hierarchical recordsets.

Data Sources

Professional and Enterprise Editions Create user controls and classes that are data sources, to which other controls can be bound.

Data View Window

Professional and Enterprise Editions You can use the Data View window to browse all of the databases you are connected to and see their tables, views, stored procedures, etc.

SQL Editor

Enterprise Edition Allows you to add new stored procedures to existing SQL Server and Oracle databases. You can write triggers using the editor too.

Hierarchical FlexGrid Control

All Editions An updated version of the FlexGrid control that, in addition to supporting all the functionality of the FlexGrid control, can display a hierarchy of ADO Recordsets. Each Recordset returned is displayed as a separate band within the grid and can be formatted independently.

DataRepeater Control

Professional and Enterprise Editions Insert a UserControl into the DataRepeater to create a custom view of a database, similar to Access forms. The UserControl can contain TextBox, CheckBox, DataGrid or other controls bound to data fields.

Passing ADO Recordsets

All Editions You can now pass ADO Recordsets across processes and even across machines (using HTTP or DCOM), which provides an efficient means for moving data between tiers in a multitier application.

Data Form Wizard Enhancements

Professional and Enterprise Editions Now gives you the ability to build code-only forms where controls are not bound to a data control. It allows you to use ADO code. The wizard is integrated with the Application, Chart, and FlexGrid wizards.

Data Object Wizard

Professional and Enterprise Editions Automates creating middle-tier objects bound to the Data Environment or UserControls.

File System Objects

All Editions Offer a streamlined set of routines to traverse the file system and create text files and directories.

Format Objects

All Editions Provide two-way conversion of data between a database and a bound control: When a value is read from a database, the Format objects add formatting appropriate to the bound control.

DataGrid Control

All Editions An OLEDB-aware version of DBGrid, the control allows you to quickly build an application to view and edit recordsets. It also supports the new ADO Data control.

DataList Control, DataCombo Controls

All Editions These controls are OLE DB versions of the DBList and DBCombo controls. They also support the new ADO Data control.


What's New in Internet Features

IIS Applications

Professional and Enterprise Editions Write server-side Internet applications that use Visual Basic code to respond to user requests from a browser.

DHTML Applications

Professional and Enterprise Editions The Dynamic HTML (DHTML) technology in Microsoft Internet Explorer exposes properties, methods, and events for each element on an Web page. DHTML Applications allow you to write Visual Basic code to respond to actions on an HTML page, without transferring processing to the server.

Web Publishing Wizard

All Editions Use the Package and Deployment Wizard (formerly the Setup Wizard) to easily post your packaged application to a Web site.

AsyncRead Enhancements

All Editions The asynchronous operations were enhanced to give more information about the progress and status of the operation, such as how many bytes have currently been read and how many bytes are to be read. New language members include the AsyncReadProgress event, BytesMax property, BytesRead property, Status property (AsyncProperty object), StatusCode property, and Target property; the AsyncRead method and AsyncReadComplete event were enhanced.

Internet Explorer 4.x Download Support for ActiveX Documents

Professional and Enterprise Editions Downloading ActiveX documents in Internet Explorer 4.x now works the same as downloading ActiveX controls — for example, there is support for relative URLs.


What's New in Controls

New Controls

ADO Data Control

All Editions A new OLEDB-aware data source control that functions much like the intrinsic Data and Remote Data controls, in that it allows you to create a database application with minimum code.

Coolbar Control

Professional and Enterprise Editions A container control that can be used to create user-configurable toolbars similar to those in Microsoft Internet Explorer.

DataGrid Control

All Editions An OLEDB-aware version of DBGrid, the control allows you to quickly build an application to view and edit recordsets. It also supports the new ADO Data control.

DataList Control, DataCombo Controls

All Editions These controls are OLE DB versions of the DBList and DBCombo controls. They also support the new ADO Data control.

DataRepeater Control

Professional and Enterprise Editions Insert a UserControl into the DataRepeater to create a custom view of a database, similar to Access forms. The UserControl can contain TextBox, CheckBox, or other controls bound to data fields.

DateTimePicker Control

Professional and Enterprise Editions Place this control on a form when you need to provide a drop-down calendar for quick entry of dates and times. Please read this note on upgrading applications that use the Windows common controls.

Flat Scrollbar Control

Professional and Enterprise Editions Get the elegant look of flat scroll bars in your applications with this new control. Please read this note on upgrading applications that use the Windows common controls.

Hierarchical FlexGrid Control

All Editions An updated version of the FlexGrid control that, in addition to supporting all the functionality of the FlexGrid control, can display a hierarchy of ADO Recordsets. Each Recordset returned is displayed as a separate band within the grid and can be formatted independently.

ImageCombo Control

All Editions Behaves like the standard ComboBox control with one addition: You can now add images to the list of items. Please read this note on upgrading applications that use the Windows common controls.

MonthView Control

Professional and Enterprise Editions Allows the end user to pick dates and contiguous ranges of dates from a graphic representation of a calendar. Please read this note on upgrading applications that use the Windows common controls.

Enhanced Controls

Intrinsic Control Enhancements

All Editions New data-binding properties and data validation enhancements.

ImageList Control Enhancements

All Editions Now supports .gif, .jpg, and .cur files.

ListView Control Enhancements

All Editions

New collection: ListSubItems Collection.

New properties: AllowColumnReorder, CausesValidation, Checkboxes, ColumnHeaderIcons, FlatScrollBar, FullRowSelect, GridLines, HotTracking, HoverSelection, Picture, PictureAlignment, Position Property (ColumnHeader Object), TextBackground.

New events: ItemCheck, Validate.

MSChart Control Enhancements

Professional and Enterprise Editions Now supports data binding and the IObjectSafety interface.

New properties: CausesValidation, DataMember, DataSource, OLEDragMode, OLEDropMode.

New method: OLEDrag.

New events: OLECompleteDrag, OLEDragDrop, OLEDragOver, OLEGiveFeedback, OLESetData, OLEStartDrag, Validate.

ProgressBar Control Enhancements

All Editions

New properties: Orientation, Scrolling.

New method: Refresh.

Slider Control Enhancements

All Editions

New properties: CausesValidation, Text, TextPosition.

New event: Validate.

TabStrip Control Enhancements

All Editions

New properties: CausesValidation, HighLighted Property (Tab Object), HotTracking, MultiSelect, Placement, Separators, TabMinWidth, TabStyle.

New method: DeselectAll.

New event: Validate.

Toolbar Control Enhancements

All Editions

New object/collection: ButtonMenu Object, ButtonMenus Collection.

New properties: DisabledImageList, HotImageList, Style, TextAlignment.

New events: ButtonDropDown, ButtonMenuClick.

TreeView Control Enhancements

All Editions

New properties: CausesValidation, Checkboxes, FullRowSelect, HotTracking, Scroll, SingleSel.

New events: NodeCheck, Validate.

New Features of Controls

Data Validation Enhancements

All Editions Using the new Validate Event in conjunction with the CausesValidation Property allows you to validate control contents before allowing the control to lose focus.

Dynamic Control Addition

All Editions Programmatically add and remove controls to or from a form. A sample application demonstrating dynamic control addition can be found in the \CtlsAdd subdirectory of the samples directory. For information about Visual Basic samples and where to find them, see the Samples Start Page.

LoadPicture Enhancements

All Editions You can now specify size and color depth.

ValidateControls Method

All Editions Validates the contents of the last control on the form before exiting the form.


What's New in Component Creation

Create Your Own Data Consumers

Professional and Enterprise Editions You can create data consumers, based on a UserControl or class, that support complex binding capabilities.

Create Your Own Data Sources

Professional and Enterprise Editions You can create your own data sources, based on a UserControl or class.

Project Start Options

All Editions New F5 options allow you to choose where to run a component for debugging purposes — for example, running a control in a browser instead of in a Visual Basic form.

Lightweight UserControls

Professional and Enterprise Editions You can create windowless and transparent UserControls, which require fewer resources.

Array Enhancements

All Editions Procedures can now return arrays, which means that your classes can have arrays as properties. You can also assign the entire contents of one array to another array, as long as the array on the left side of the assignment is a dynamic array.

Class Persistence

All Editions Class properties can be persisted to a PropertyBag for storage and retrieval.

Support for Managing ClassIDs/Compatibility

Professional and Enterprise Editions Project compatibility now maintains Class IDs between versions of ActiveX components.

Validation Events for UserControls

Professional and Enterprise Editions The Validate event allows you to validate a control’s data before the LostFocus event is fired.

Apartment-model Threading

Professional and Enterprise Editions Create ActiveX components that use apartment-model threading to execute procedures asynchronously on separate threads. (New for Visual Basic 5.0 Service Pack 2.)

Class Builder Utility Enhancements

Professional and Enterprise Editions Supports ParamArray, Optional, ByVal, and Default values in the argument list and supports Enums.

CreateObject Function Enhancement

All Editions A new argument allows you to create objects on remote machines.

Data Object Wizard

Professional and Enterprise Editions Automates creating middle-tier objects bound to the Data Environment or UserControls.

FontChanged Event

All Editions Provides notification to your ActiveX control when font characteristics are changed by an external source.

Microsoft Transaction Server Support

Professional and Enterprise Editions The new MTSTransactionMode property allows you to specify the transactional behavior of a class used in conjunction with the Microsoft Transaction Server.

HasDC Property

All Editions Determines whether a user control allocates a fixed hDC for its lifetime rather than using a shared hDC as needed.


What's New in Language Features

User-defined Types in Public Methods

All Editions User-defined types can be arguments or return types of public properties and methods.

Functions Can Return Arrays

All Editions Functions and properties procedures will be able to return arrays.

Resizable Arrays Can Be Assigned

All Editions Variable-sized arrays can now appear on the left side of an assignment. Fixed-sized arrays cannot appear on the left side of an assignment, although they can appear on the right.

File System Objects

All Editions Offers a streamlined set of routines to traverse the file system and create text files and directories.

CallByName

All Editions Allows access to a property or method by specifying a string containing the name of the property or method, rather than by coding the name explicitly.

CreateObject Function Enhancements

All Editions CreateObject now lets you specify a machine name as an optional argument, which allows you to create objects on remote machines.

StrConv Function Enhancement

All Editions New LCID argument enables you to specify a LocaleID for a string different than the system's LocaleID.

Dictionary Object

All Editions The Dictionary object overcomes some of the Collection object's limitations and is easier to use because it generates fewer run-time errors for common operations and is more flexible.

New String Functions

All Editions

  • Filter Function

  • FormatCurrency Function

  • FormatDateTime Function

  • FormatNumber Function

  • FormatPercent Function

  • InstrRev Function

  • Join Function

  • MonthName Function

  • Replace Function

  • Round Function

  • Split Function

  • StrReverse Function

  • WeekdayName Function


What's New in Wizards

Setup Wizard Enhancements

All Editions The Package and Deployment Wizard (formerly the Setup Wizard) lets you deploy your .cab file to a Web server, network share, or other folder. The new wizard incorporates data access support for ADO, OLE DB, RDO, ODBC, and DAO as well as support for the new IIS and DHTML applications. It also features better control over Start menu groups and icons for your setup program. The new wizard can be run from within Visual Basic as an add-in, or via the command line, as part of a batch process.

Data Object Wizard

Professional and Enterprise Editions Automates creating middle-tier objects bound to the Data Environment or UserControls.

Data Form Wizard Enhancements

Professional and Enterprise Editions Now gives you the ability to build code-only forms where controls are not bound to a data control. It allows you to use ADO code. The wizard is integrated with the Application, Chart, and FlexGrid wizards.

Application Wizard Enhancements

All Editions Now lets you save your settings as profiles for later use, allowing you to create multiple applications with the same format. You can also launch the Data Form Wizard and the Toolbar Wizard from within the Application Wizard to create dataforms and toolbars. Menus are now completely customizable.

Add-In Designer

Professional and Enterprise Editions Allows you to begin development by easily specifying your add-in's default load behavior, name, target application and version, and other properties if desired.

Class Builder Utility Enhancements

Professional and Enterprise Editions Supports ParamArray, Optional, ByVal, and Default values in argument list and supports Enums.

Toolbar Wizard

All Editions When you have the Application Wizard loaded, the Toolbar Wizard automatically opens when you add a toolbar to a form so that you can create customized toolbars.


Upgrading Applications that Use the Windows Common Controls

The Windows common controls have been upgraded to include new controls and functions and to use new file names.

Visual Basic version 6.0 ships with two versions of the Windows common controls. The first version is for backward compatibility only and should be used when you do not want to upgrade an existing application to use the new version.

Why Upgrade?

Visual Basic version 5.X common controls were contained in two .OCX files: ComCtl32.ocx and ComCt232.ocx. Both of these files called into the ComCtl32.dll file, a system file, for functionality. The new control versions are contained in two new files: MsComCtl.ocx and MsComCt2.ocx. Neither of these files are linked to the ComCtl32.dll file. Besides the new controls added to these files, another reason for upgrading is easier distribution and maintenance. The file changes are summarized below:

Old File Name | New File Name
ComCtl32.ocx | MsComctl.ocx
ComCt232.ocx | MsComct2.ocx
ComCtl32.dll | -- This file is not needed.

How to Upgrade

When an application — created with Visual Basic version 5 and using these controls — is first loaded in Visual Basic version 6, a dialog box will appear informing you that a new version is available, and that the controls can be upgraded. At that time, you can choose to upgrade the project to the new controls, or continue to use the older control versions.

If you do not wish to upgrade the application, you can prevent this dialog from appearing by unchecking the "Upgrade ActiveX Controls" option found in the Project Name Properties dialog box (from the Project menu), however by doing so all upgrade messages will be turned off for the project. You can further confirm which version of the controls is being used by opening the Components dialog box (from the Project menu), and checking which file name is associated with the checked "Microsoft Windows Common Controls X.X". The version number is 5.0 for the old files, and 6.0 for the new.

List of Windows Common Controls

The new files contain the following controls:

File | Control
MsComctl.ocx | TreeView ListView ProgressBar Slider ImageList Toolbar TabStrip StatusBar ImageCombo
MsComct2.ocx | Animation UpDown MonthView DateTimePicker FlatScrollBar


Find It Fast

To help narrow your information search, these topics assemble links to information found throughout the documentation, and organize them based on tasks common to Visual Basic programming.

Topics for a Quick Start with Visual Basic

Links to topics that get you started quickly with a step-by-step introduction, or that give you background information you can use to learn about Visual Basic more thoroughly.

Topics on Designing Applications

Links to topics on planning your application, whether you are programming for standalone distribution or for integrated client/server applications.

Topics on Accessing Data

Links to topics on accessing data locally or remotely.

Topics on Programming with ActiveX/OLE

Links to topics on using ActiveX to create controls, ActiveX documents, code components, and other objects.

Topics on Internet Programming

Links to topics on client- and server-side Internet/intranet programming.

Topics on Enterprise Programming

Links to topics on the development tools provided in the Enterprise Edition and on architectural and design guidelines for using these tools to build three-tiered client/server applications.

Topics on Debugging, Handling Errors, and Enhancing Performance

Links to topics offering techniques to help you make applications run perfectly.

Topics on Compiling and Distributing

Links to topics on packaging your application for use by others.

Topics on Efficient Programming

Links to topics on making your development work easier and faster.


Topics for a Quick Start with Visual Basic

Whether you're looking for a hands-on introduction to a particular kind of application or for information on the fundamentals, you'll find links to topics that get you started. Included are links to information on conventions and guidelines that are helpful to know when you begin a new kind of project.

Step-by-step Topics

Your First Visual Basic Application

All Editions Introduces Visual Basic fundamentals through a step-by-step procedure to create a simple application.

Creating an ActiveX Control

Professional and Enterprise Editions Provides a series of step-by-step procedures that create a simple control. Introduces ActiveX control concepts, raising events, debugging design-time behavior, and property pages.

Creating an ActiveX Document

Professional and Enterprise Editions Provides a series of step-by-step procedures that create a simple ActiveX document, and demonstrate ActiveX document concepts, navigation, properties and methods, menus, and debugging techniques.

Creating an ActiveX DLL

Professional and Enterprise Editions Provides step-by-step procedures that get you off to a running start with in-process debugging, multiple projects, global objects, object lifetime concepts, and circular references.

Creating an ActiveX EXE

Professional and Enterprise Editions Provides step-by-step procedures that introduce threads, events in class modules, call-backs, and out-of-process debugging.

Creating a Basic Add-In

Professional and Enterprise Editions Leads you step-by-step though the process of creating a simple, basic working add-in. Afterward, there is an explanation of what was done and why.

What's New in Wizards?

All Editions Provides links to information on wizards.

Working with Templates

All Editions Introduces you to templates that make it easier to build applications and objects.

Other Introductory Topics

Visual Basic Basics

All Editions Provides links to five chapters with beginning information to give you the foundation you need for anything you want to do in Visual Basic.

What Can You Do With Visual Basic?

Learning, Professional, and Enterprise Editions Provides links to a dozen chapters with more advanced information to help you build on the foundation offered in "Visual Basic Basics."

Visual Basic Enterprise Development

Enterprise Edition Offers links to information on tools, techniques, and design guidelines for building three-tiered client/server applications with Visual Basic.

Programming Practices Topics

Visual Basic Coding Conventions

All Editions Suggests guidelines for writing consistent and readable code.

ActiveX Component Standards and Guidelines

Professional and Enterprise Editions Offers guidelines for object naming, component shutdown, implementing collections and Application objects, and more.


Topics on Designing Applications

Visual Basic makes it easy to get any new programming project moving, but prior design plays an important role in developing any application. These links include topics from across the spectrum to help you be aware of important issues before you get started.

General Topics

Developing an Application in Visual Basic

All Editions Introduces the integrated development environment and the process of creating your first application.

Creating a User Interface

All Editions Provides in-depth coverage of interface styles, forms, menus, toolbars, and more.

Designing for Performance and Compatibility

All Editions Offers optimizing techniques for making your applications faster or smaller.

International Issues

All Editions Introduces considerations for creating multilingual and multicultural applications.

ActiveX/OLE Topics

General Principles of Component Design

Professional and Enterprise Editions Provides in-depth discussion of design issues to consider when creating ActiveX components; procedures to debug, register, and distribute servers; and version compatibility features.

Designing the User Interface for ActiveX Documents

Professional and Enterprise Editions Provides details about designing menus and adding forms, as well as how the Viewport works.

Enterprise Topics

Data Access and Security Strategies

Visual Studio Enterprise Edition Offers strategies for identifying application requirements, making design decisions, and developing components deployed across a network.

Choosing the Right Data Access Technology

Visual Studio Enterprise Edition Compares data access development models.

Programming Practices Topics

Visual Basic Coding Conventions

All Editions Suggests guidelines for writing consistent and readable code.

ActiveX Component Standards and Guidelines

Professional and Enterprise Editions Offers guidelines for object naming, component shutdown, implementing collections and Application objects, and more.


Topics on Accessing Data

Visual Basic provides a wide variety of data access alternatives, for working with both local and remote databases, including enterprise-level three-tiered client/server applications.

General Topics

What’s New in Data Access?

All Editions Provides links to what's new or modified in ActiveX Data Objects, data controls and data-bound controls, and database programming tools and technology.

Data Access Guide

All Editions Introduces tools and techniques for working with local databases with ActiveX Data Objects.

Enterprise Topics

Data Access and Security Strategies

Visual Studio Enterprise Edition Provides an overview of using data in the enterprise, design strategies, and implementing legacy data access. Also links to information on client/server data access alternatives, including ActiveX Data Objects, Remote Data Objects, the ODBC API, and Data Access Objects.


Topics on Programming with ActiveX/OLE

From objects you use internally to ActiveX controls to ActiveX documents to code components, you can use ActiveX to accomplish more tasks than ever.

Topics on General ActiveX Programming

All Editions Links to general information on using ActiveX technology.

Topics on Programming ActiveX Controls

Professional and Enterprise Editions Links to information about creating and using ActiveX controls.

Topics on Programming ActiveX Documents

Professional and Enterprise Editions Links to information on creating ActiveX documents.

Topics on Programming ActiveX Code Components (OLE Servers)

Professional and Enterprise Editions Links to information on creating and using ActiveX code components.


Topics on General ActiveX Programming

These links take you to general information to help you make the most of ActiveX technology.

What’s New in OLE/ActiveX?

All Editions Links to information about creating your own ActiveX controls and ActiveX documents, in addition to other ActiveX features.

Programming with Objects

All Editions Provides fundamental in-depth information about objects, including creating classes and using designers.

Programming with Components

All Editions Offers fundamental techniques for using ActiveX components in your applications.

General Principles of Component Design

Professional and Enterprise Editions Contains advanced information of importance to component designers, including terminology, concepts, instancing for class modules, polymorphism, and object models.

ActiveX Component Standards and Guidelines

Professional and Enterprise Editions Offers guidelines for object naming, component shutdown, implementing collections and Application objects, and more.

Extending the Visual Basic Environment with Add-Ins

Professional and Enterprise Editions Provides a guide to using the Visual Basic extensibility object model to tailor the development environment to your unique needs with add-ins.

What's New in Wizards?

All Editions Provides links to information on wizards, many of which are helpful in creating ActiveX components.


Topics on Programming ActiveX Controls

Use these links to get the information you need to create your own ActiveX controls, or use those included with Visual Basic.

Creating an ActiveX Control

Professional and Enterprise Editions Provides a series of step-by-step procedures that create a simple control. Introduces ActiveX control concepts, raising events, debugging design-time behavior, and property pages.

Building ActiveX Controls

Professional and Enterprise Editions Contains in-depth explanations of how Visual Basic ActiveX controls work, what features you can implement, subtleties of debugging, and discussions of implementation techniques.

Using ActiveX Controls

Professional and Enterprise Editions Introduces the ActiveX controls included with the Visual Basic Professional and Enterprise editions. Start here for general information, scenarios offering real-world uses, and code.

Other Internet Technologies in Visual Basic

Professional and Enterprise Editions Briefly discusses how you can use ActiveX controls, both to create new Web applications and to extend existing applications with Internet technology.


Topics on Programming ActiveX Documents

You can create Visual Basic applications for deployment in an Internet browser. Use these links to get the information you need to create your own ActiveX documents.

Creating an ActiveX Document

Professional and Enterprise Editions Provides a series of step-by-step procedures that create a simple ActiveX document and demonstrates ActiveX document concepts, navigation, properties and methods, menus, and debugging techniques.

Building ActiveX Documents

Professional and Enterprise Editions Provides in-depth discussions of terminology, concepts, Internet features, navigation, debugging, migrating from forms, and in-process vs. out-of-process implementation.

Other Internet Technologies in Visual Basic

Professional and Enterprise Editions Briefly overviews principal features that enable you to deploy these Visual Basic "applications for the Internet" for a well-integrated site design.


Topics on Programming ActiveX Code Components (OLE Servers)

Whether you're creating your own components or taking advantage of the functionality of another, you'll get the information you need through these links.

Creating an ActiveX DLL

Professional and Enterprise Editions Provides step-by-step procedures that get you off to a running start with in-process debugging, multiple projects, global objects, object lifetime concepts, and circular references.

Creating an ActiveX EXE

Professional and Enterprise Editions Provides step-by-step procedures that introduce threads, events in class modules, call-backs, and out-of-process debugging.

Building Code Components

Professional and Enterprise Editions Provides in-depth discussions of in-process and out-of-process components, threading, instancing, call-backs, and events.

Other Internet Technologies in Visual Basic

Professional and Enterprise Editions Briefly introduces code components as sources of functionality for both the client and server.

Accessing DLLs and the Windows API

Professional and Enterprise Editions Provides a guide to enhancing functionality provided by Visual Basic by calling functions in dynamic-link libraries (DLLs), including those that form the backbone of the Microsoft Windows operating system, as well as routines written in other languages.


Topics on Debugging, Handling Errors, and Enhancing Performance

When you're fine tuning your projects, Visual Basic provides all the tools you need to develop an application that runs without a hitch. Here are links to information on tips and tools you can use to make things run perfectly.

Topics on Debugging

Links to topics on debugging all kinds of development projects.

Topics on Handling Errors

Links to topics on writing code that help your application expect the unexpected.

Topics on Enhancing Performance

Links to topics on making your application more efficient, smaller, faster.


Topics on Debugging

Use these links to find information on tips and tools for getting your code running perfectly.

General Topics

Debugging Your Code and Handling Errors

All Editions Contains links and fundamental information about what to do when something doesn’t work as planned, either at design or run time.

ActiveX/OLE Topics

Debugging Class Modules

All Editions Describes the Break in Class Module error trapping option, plus the ALT+F8 and ALT+F5 keystrokes for stepping or running past errors.

Debugging, Testing, and Deploying Components

Professional and Enterprise Editions Contains links and general information, such as setting up test projects, debugging features, adding Help, version compatibility, and localization.

Debugging Controls

Professional and Enterprise Editions Discusses the finer points of running and debugging code for ActiveX controls you create.

Debugging ActiveX Documents in Internet Explorer

Professional and Enterprise Editions Details the use of Internet Explorer as a debugging tool.


Topics on Handling Errors

Use these links to find information on handling the unexpected in your applications.

General Topics

Debugging Your Code and Handling Errors

All Editions Contains links and fundamental information about what to do when something doesn’t works as planned, either at design or run time.

ActiveX/OLE Topics

Handling Run-Time Errors in ActiveX Components

All Editions Contains information on trapping errors when you work with ActiveX components.

Generating and Handling Errors in ActiveX Components

Professional and Enterprise Editions Provides details on how to return errors to clients that use components you create, and how to handle errors from other components.


Topics on Enhancing Performance

Use these links to find information on optimizing your applications, making them smaller, faster, and more efficient.

General Topics

Designing for Performance and Compatibility

All Editions Offers optimizing techniques for making your applications faster or smaller.

Compiling Your Project to Native Code

Professional and Enterprise Editions Discusses how you can use the Professional or Enterprise edition of Visual Basic to compile your code in native code format.

Compiled vs. Interpreted Applications

Professional and Enterprise Editions Discusses the pros and cons of native-code executables.

ActiveX/OLE Topics

Speeding Object References

All Editions Describes how you can make your applications run faster by optimizing the way Visual Basic resolves object references.

How Binding Affects ActiveX Component Performance

Professional and Enterprise Editions Outlines how the performance of your component is affected by the way clients declare the variables they use to keep references to your objects.

How Marshaling Affects ActiveX Component Performance

Professional and Enterprise Editions Describes how method argument declarations in out-of-process components can impact performance.

Creating Robust Controls

Professional and Enterprise Editions Lists and briefly explains some do's and don'ts to keep in mind when creating ActiveX controls.

Enterprise Topics

Data Access Performance Tuning

Enterprise Edition Offers techniques on improving SQL Server performance.

Application Performance Explorer

Enterprise Edition Introduces a software utility (written in Visual Basic) that allows you to run automated "what-if" tests to profile the performance of a multi-tier application in different network topologies.


Topics on Compiling and Distributing

Visual Basic provides a variety of options to consider when you're ready to build and distribute your development efforts. Use information you can reach through the links in these topics to choose the options best for you.

Compiling Your Application

Links to topics on choices to make when its time to compile, including whether or not to compile to native code.

Distributing Your Application

Links to topics for information on distribution options in various contexts, from client/server development to the Internet.


Topics on Compiling Applications

Want to compile your Visual Basic application to native code? Now you can. Use these links to make that and other important choices.

General Topics

Making and Running an Executable File

All Editions Outlines the basics of compiling a project.

Removing Debugging Information Before Compiling

All Editions Outlines using conditional compilation to conveniently delete debugging statements when you compile.

Compiling Your Project to Native Code

Professional and Enterprise Editions Discusses how you can use the Professional or Enterprise edition of Visual Basic to compile your code in native code format.

Compiled vs. Interpreted Applications

Professional and Enterprise Editions Discusses the pros and cons of native-code executables.


Topics on Distributing Your Application

From standard executables to ActiveX components on the Internet, you have more distribution options than ever. Use these links for information on options in the various contexts.

General Topics

Distributing Your Applications

All Editions Explains how to use the Package and Deployment Wizard to create installation programs.

International Issues

All Editions Discusses considerations for multilingual and multicultural applications.

ActiveX/OLE Topics

Debugging, Testing, and Deploying Components

Professional and Enterprise Editions Contains more general information, such as setting up test projects, debugging features, adding Help, version compatibility, and localization.

Distributing Controls

Professional and Enterprise Editions Discusses setup programs and explains licensing and versioning issues for ActiveX controls you create.

Downloading ActiveX Components

Professional and Enterprise Editions Offers information on how to package and download your ActiveX controls, code components, and ActiveX documents.


Topics on Efficient Programming

You can make your development work easier and faster using tools provided with Visual Basic, tools you create yourself, and by following tips provided in the documentation.

Tools Topics

What's New in Wizards?

All Editions Provides links to information on wizards.

Working with Templates

All Editions Introduces you to using templates that make it easier to build applications and objects.

Extending the Visual Basic Environment with Add-Ins

Professional and Enterprise Editions Provides a complete guide to using the Visual Basic extensibility object model to tailor the development environment to your unique needs.

Data Access Tools

Professional and Enterprise Editions Describes controls, designers, and technologies in Visual Basic for meeting data access programming needs.

Visual Basic Enterprise Edition Features

Enterprise Edition Describes the tools included with the Enterprise edition to assist in building component-based applications, and for testing and deploying them across the network.

Techniques Topics

Working with Resource Files

All Editions Introduces you to using resource files to store version-specific text and bitmaps.

Using Resource Files for Localization

All Editions Describes how to create, localize, and use a resource file for separating localizable information from your code.

Working with Command Line Switches

All Editions Explains how to use command line switches to control how Visual Basic executes.

Visual Basic Coding Conventions

All Editions Suggests guidelines for writing consistent and readable code.


Document Conventions

This manual uses the following typographic conventions.

Example of convention | Description
Sub, If, Case Else, Print, True, BackColor, Click, Debug, Long | Words with initial letter capitalized indicate language-specific keywords.
setup | Words you’re instructed to type appear in bold.
event-driven | In text, italic letters can indicate defined terms, usually the first time they occur in the book. Italic formatting is also used occasionally for emphasis.
variable | In syntax and text, italic letters can indicate placeholders for information you supply.
[expressionlist] | In syntax, items inside square brackets are optional.
{While | Until} | In syntax, braces and a vertical bar indicate a choice between two or more items. You must choose one of the items unless all of the items also are enclosed in square brackets.
Sub HelloButton_Click()
Readout.Text = _
"Hello, world!"
End Sub

This font is used for code.

ENTER
 | Capital letters are used for the names of keys and key sequences, such as ENTER and CTRL+R.

ALT+F1
 | A plus sign (+) between key names indicates a combination of keys. For example, ALT+F1 means to hold down the ALT key while pressing the F1 key.

DOWN ARROW
 | Individual direction keys are referred to by the direction of the arrow on the key top (LEFT, RIGHT, UP, or DOWN). The phrase "arrow keys" is used when describing these keys collectively.

BACKSPACE, HOME
 | Other navigational keys are referred to by their specific names.

c:\Vb\Samples\Calldlls.vbp
 | Paths and file names are given in mixed case.

Programming Style in This Manual

The following guidelines are used in writing programs in this manual. For more information, "Programming Fundamentals" in the Programmer's Guide.

  •
Keywords appear with initial letters capitalized:
    ' Sub, If, ChDir, Print, and True are keywords.
Print "Title Page"

    •

  •
Line labels are used instead of line numbers. The use of line labels is restricted to error-handling routines:
    ErrorHandler:
Power = conFailure
End Function

    •

  •
An apostrophe (') introduces comments:
    ' This is a comment; these two lines
' are ignored when the program is running.

    •

  •
Control-flow blocks and statements in Sub, Function, and Property procedures are indented from the enclosing code:
    Private Sub cmdRemove_Click ()
Dim Ind As Integer

Ind = lstClient.ListIndex    ' Get index.
' Make sure list item is selected.
If Ind >= 0 Then
lstClient.RemoveItem Ind    ' Remove it
' from list box.
' Display number.
lblDisplay.Caption = lstClient.ListCount
Else
Beep                     ' If nothing selected, beep.
End If
End Sub

    •

  •
Lines too long to fit on one line (except comments) may be continued on the next line using a line-continuation character, which is a single leading space followed by an underscore ( _):
    Sub Form_MouseDown (Button As Integer, _
Shift As Integer, X As Single, Y As Single)

    •

  •
Intrinsic constant names are in a mixed-case format, with a two-character prefix indicating the object library that defines the constant. Constants from the Visual Basic (VB) and Visual Basic for applications (VBA) object libraries are prefaced with a "vb"; constants from the data access (DAO) object library are prefaced with a "db"; constants from the Microsoft Excel object library are prefaced with an "xl". For example:
    vbTileHorizontal
dbAppendOnly
xlDialogBorder

Throughout this book, user-defined constants are usually prefaced with "con" and are mixed case. For example:
    conYourOwnConstant

When using Windows API constants, however, the code examples still follow the same conventions as in previous versions of Visual Basic. For example:
    EM_LINESCROLL

    •


Programmer's Guide

Welcome to the Visual Basic® Programmer’s Guide, a comprehensive manual on programming with Visual Basic. To accommodate the wealth of features and capabilities in Visual Basic, the Programmer’s Guide is divided into two parts.

The first part covers the basic concepts, providing a foundation for programmers new to Visual Basic. Part 2 covers more advanced programming concepts and techniques. Additional information helpful in using the product is presented in the appendices.

Visual Basic Basics

An introduction to programming in Visual Basic.
What Can You Do With Visual Basic?

Advanced topics on Visual Basic programming.
Visual Basic Specifications, Limitations, and File Formats

Technical details for Visual Basic.
Visual Basic Coding Conventions

Suggested guidelines for consistent and readable code.
Native Code Compiler Switches

Details on command line switches for compiling to native code.
Adding Help to Your Application

Guidelines for adding online Help to a Visual Basic application.


Visual Basic Basics

Many of the things that you can do with Visual Basic really aren’t very basic at all. The Visual Basic language is quite powerful — if you can imagine a programming task, it can probably be accomplished using Visual Basic. As you might guess, there’s a lot to be learned before you can consider yourself a guru; but once you understand the basics of Visual Basic you’ll find that you are productive in no time at all.

The first five chapters of the Visual Basic Programmer’s Guide cover the basics, providing the foundation that you will need for anything you want to do in Visual Basic.

Chapters

Introducing Visual Basic

Explains how to install Visual Basic and get assistance while you work.
Developing an Application in Visual Basic

An introduction to the integrated development environment and the process of creating your first application.
Forms, Controls, and Menus

An introduction to the objects that you can put together to create an application.
Managing Projects

An introduction to the tools used to organize your work in Visual Basic.
Programming Fundamentals

An introduction to the nuts and bolts of the Visual Basic language.


Introducing Visual Basic

This chapter contains information on installing Microsoft Visual Basic on your system, adding or removing Visual Basic components, and resources for learning or getting additional help with Visual Basic.

Topics

Welcome to Visual Basic

A brief introduction.
Installing Visual Basic

Instructions for installing Visual Basic.
Getting Assistance While You Work

A discussion of the user assistance model.


Welcome to Visual Basic

Welcome to Microsoft Visual Basic, the fastest and easiest way to create applications for Microsoft Windows®. Whether you are an experienced professional or brand new to Windows programming, Visual Basic provides you with a complete set of tools to simplify rapid application development.

So what is Visual Basic? The "Visual" part refers to the method used to create the graphical user interface (GUI). Rather than writing numerous lines of code to describe the appearance and location of interface elements, you simply add prebuilt objects into place on screen. If you've ever used a drawing program such as Paint, you already have most of the skills necessary to create an effective user interface.

The "Basic" part refers to the BASIC (Beginners All-Purpose Symbolic Instruction Code) language, a language used by more programmers than any other language in the history of computing. Visual Basic has evolved from the original BASIC language and now contains several hundred statements, functions, and keywords, many of which relate directly to the Windows GUI. Beginners can create useful applications by learning just a few of the keywords, yet the power of the language allows professionals to accomplish anything that can be accomplished using any other Windows programming language.

The Visual Basic programming language is not unique to Visual Basic. The Visual Basic programming system, Applications Edition included in Microsoft Excel, Microsoft Access, and many other Windows applications uses the same language. The Visual Basic Scripting Edition (VBScript) is a widely used scripting language and a subset of the Visual Basic language. The investment you make in learning Visual Basic will carry over to these other areas.

Whether your goal is to create a small utility for yourself or your work group, a large enterprise-wide system, or even distributed applications spanning the globe via the Internet, Visual Basic has the tools you need.

  • Data access features allow you to create databases, front-end applications, and scalable server-side components for most popular database formats, including Microsoft SQL Server and other enterprise-level databases.

  • ActiveX™ technologies allow you to use the functionality provided by other applications, such as Microsoft Word word processor, Microsoft Excel spreadsheet, and other Windows applications. You can even automate applications and objects created using the Professional or Enterprise editions of Visual Basic.

  • Internet capabilities make it easy to provide access to documents and applications across the Internet or intranet from within your application, or to create Internet server applications.

  • Your finished application is a true .exe file that uses a Visual Basic Virtual Machine that you can freely distribute.

Visual Basic Editions

Visual Basic is available in three versions, each geared to meet a specific set of development requirements.

  • The Visual Basic Learning edition allows programmers to easily create powerful applications for Microsoft Windows and Windows NT®. It includes all intrinsic controls, plus grid, tab, and data-bound controls. Documentation provided with this edition includes the Learn VB Now CD plus the Microsoft Developer Network (MSDN™) Library CDs containing full online documentation.

  • The Professional edition provides computer professionals with a full-featured set of tools for developing solutions for others. It includes all the features of the Learning edition, plus additional ActiveX controls, the Internet Information Server Application Designer, integrated Visual Database Tools and Data Environment, Active Data Objects, and the Dynamic HTML Page Designer. Documentation provided with the Professional edition includes the Visual Studio Professional Features book plus Microsoft Developer Network CDs containing full online documentation.

  • The Enterprise edition allows professionals to create robust distributed applications in a team setting. It includes all the features of the Professional edition, plus Back Office tools such as SQL Server, Microsoft Transaction Server, Internet Information Server, Visual SourceSafe, SNA Server, and more. Printed documentation provided with the Enterprise edition includes the Visual Studio Enterprise Features book plus Microsoft Developer Network CDs containing full online documentation.


Installing Visual Basic

You install Visual Basic on your computer using the Setup program. The Setup program installs Visual Basic and other product components from the CD-ROM to your hard disc. It also installs the files necessary to view the documentation on the Microsoft Developer Network CD. If you choose, you can install only the Visual Basic documentation and samples to your machine.

Important You cannot simply copy files from the CD-ROM to your hard disc and run Visual Basic. You must use the Setup program, which decompresses and installs the files in the appropriate directories.

  • Before You Run Setup Things to check prior to installation.

  • Setting Up Visual Basic Instructions for installing Visual Basic.


Before You Run Setup

Before you install Visual Basic, make sure that your computer meets the minimum requirements, and read the Readme file, located at the root directory on your installation disc.

Check the Hardware and System Requirements

To run Visual Basic, you must have certain hardware and software installed on your computer. The system requirements include:

  • Microsoft Windows 95 or later, or Microsoft Windows NT Workstation 4.0 (Service Pack 3 recommended) or later.

  • 486DX/66 MHz or higher processor (Pentium or higher processor recommended), or any Alpha processor running Microsoft Windows NT Workstation.

  • A CD-ROM disc drive.

  • VGA or higher-resolution screen supported by Microsoft Windows.

  • 16 MB of RAM for Windows 95, 32 MB of RAM for Windows NT Workstation.

  • A mouse or other suitable pointing device.

For More Information For more details about requirements, see "System Requirements for Visual Basic" in "Visual Basic Specifications, Limitations, and File Formats."

Read the Readme File

The Readme file lists any changes to the Visual Basic documentation since its publication. It can be found by selecting Read Me First from the initial setup screen, or in the root directory of the CD-ROM. It can also be accessed from the Visual Basic Start Page in the documentation. Check the first section of the file for details and new information about installing Visual Basic.


Setting Up Visual Basic

When you run the Setup program, a directory is created for Visual Basic; you can then select the components of Visual Basic that you want to install.

With the exception of the operating system files in the \Os directory, files on the compact disc are not compressed, so they're usable directly from the disc. For example, there are numerous tools and components in the \Tools directory that can be run or installed directly from the CD-ROM.

To set up from compact disc

  1. Insert the compact disc in the CD-ROM drive.

  2. Use the appropriate command in your operating environment to run the Setup program, which is available in the root directory on Disk 1. If AutoPlay is enabled on your system, the Setup program will automatically load when you insert the compact disc.

  3. Select Install Visual Basic 6.0.

  4. Follow the setup instructions on the screen.

For More Information See the Readme file for detailed instructions on installing Visual Basic.

Adding or Removing Components of Visual Basic

You can run Setup as many times as necessary. For example, you can run Setup to reinstall Visual Basic in another directory, or to install other portions of Visual Basic.

To add or remove components of Visual Basic

  1. Insert the compact disc in the CD-ROM drive.

  2. Use the appropriate command in your operating environment to run the Setup program, which is available in the root directory on the compact disc. If AutoPlay is enabled on your system, the Setup program will automatically load when you insert the compact disc.

  3. Select the Custom button in the Microsoft Visual Basic 6.0 Setup dialog box.

  4. Select the components to be installed (or deselect the components to be removed) in the Options list box of the Custom dialog box.

  5. Follow the setup instructions on the screen.

Starting Visual Basic

Once you have completed the Setup procedure, you can start Visual Basic by using the Start button on the Windows task bar. If AutoPlay is enabled on your system, you can also start Visual Basic by inserting the Visual Basic compact disc.

For More information See "Developing an Application in Visual Basic."


Getting Assistance While You Work

The online documentation system references nearly all aspects of Visual Basic. It includes:

  • All of the Visual Basic books, providing conceptual information on using the multitude of features in Visual Basic.

  • Language Reference, containing extensive information on the Visual Basic programming environment and language.

  • Visual Basic online links, providing pointers to sources of Visual Basic information on the web.

  • Microsoft Product Support Services, with information on obtaining technical support.

Note You can view the documentation from the MSDN CDs (you must go through the MSDN installation process) or you can custom install the Visual Basic documents and samples to your machine during MSDN installation.

Getting the Most Out of Help Content

Help content includes several features designed to make finding information easier.

  • What's New in Visual Basic 6.0?

    Use this section to go quickly to information on new and enhanced features of Visual Basic. Organized by feature category, it provides close to 200 described links to more information.

  • Find It Fast

    Use this section to sort out subject areas covered throughout the documentation. Debugging information, for example, comes in a variety of flavors, depending on the kind of project you're working on. The described links in this section make the search easier.

  • Overview topics

    Use these to get information about topics in a book or chapter before you go to the topics themselves. By providing a glimpse of content in each topic, the described links in overviews at the head of books, parts, and chapters save time.

  • See Also links

    Click the See Also link under the topic title to view the titles of topics you can go to for more or related information.


Context-Sensitive Help

Many parts of Visual Basic are context sensitive. Context sensitive means you can get help on these parts directly without having to go through the Help menu. For example, to get Help on any keyword in the Visual Basic language, place the insertion point on that keyword in the Code window and press F1.

You can press F1 from any context-sensitive part of the Visual Basic interface to display Help information about that part. The context-sensitive parts are:

  • Every window in Visual Basic (Properties window, Code window, and so on)

  • Controls in the Toolbox

  • Objects on a form or document object

  • Properties in the Properties window

  • Visual Basic keywords (statements, functions, properties, methods, events, and special objects)

  • Error messages


Running Code Examples from Help

Many of the language topics in Help contain code examples that you can run from Visual Basic. The following procedures show you how to copy and run a code example from Help.

Note The following procedure is for code examples that do not contain public declarations.

To copy a code example from Help

  1. Create a new form by choosing Add Form from the Project menu, or use an existing form. (For more information on creating and using forms, see "Developing An Application in Visual Basic.")

  2. Choose Index from the Help menu.

  3. In Help, search for graphics, and go to the topic called "FillColor Property."

  4. In the FillColor Property topic, click the Example jump, located in the nonscrolling region near the top of the window. (A jump is a word that you can click to go to another topic. Jumps are underlined and the jump text is colored.)

    Select the subroutine portion of the example. Note that the first "Sub" marks the beginning of the procedure and the last "End Sub" marks the end of the procedure.

  5. Right-click the selected text and select Copy from the context menu. The text is copied onto the Clipboard.

  6. Return to the form you created and double-click the form to display the Code window.

  7. Place the insertion point below any existing code in the Code window.

  8. From the Edit menu, choose Paste. The example now appears in the Code window.

  9. From the Run menu, choose Start, or press F5.

  10. Click the form to run the example code.

    Note Some code examples require you to draw controls on the form. For more information on drawing controls, see "Forms, Controls, and Menus."


Visual Basic Online Links

If you have a modem or other means of access, additional information about Visual Basic is available on the web.

Microsoft Web Site

Microsoft's web site contains several areas of interest to Visual Basic programmers. The Visual Basic home page is located at http://www.microsoft.com/vbasic/. Information available at this site includes:

  • Updates on new features, product releases, related products, seminars and special events.

  • Additional information on Visual Basic features, including white papers, tips and tutorials, and training resources.

  • New product downloads including updates to program files, help updates, drivers, and other Visual Basic related files.

Tip The Visual Basic web site also contains a special Owner’s Area for registered owners that contains many free samples, components, tools, and much more. Why not go to http://www.microsoft.com/vbasic/owners/ and register your copy of Visual Basic right now?

To access the Microsoft Visual Basic Web site

  1. Choose Microsoft on the Web from the Help menu.

  2. Select the appropriate option from the submenus.

    Note You must have a Web browser installed and you must be connected to the Internet for these options to work. Some of the content on the Microsoft Web site is optimized for Microsoft Internet Explorer and may not be fully visible to other browsers. You can download the latest version of Internet Explorer from the Web site.


Microsoft Product Support Services

Microsoft offers a variety of support options to help you get the most from Visual Basic.

If you have a question about the product, first look in the online documentation. If you can't find the answer, contact Microsoft Product Support Services.

Support services are available both within the United States and through subsidiary offices worldwide. For complete details, see Technical Support under the Visual Basic Help menu.

Tell Us What You Think

Microsoft is committed to providing the best possible products to our customers. With each new version, Visual Basic has evolved in order to meet the changing needs of Windows programmers.

We're always interested in hearing from our customers. If you have any suggestions or comments regarding improvements or features that you would like to see in future versions of Visual Basic, let us know. You can send your suggestions via e-mail to vbwish@microsoft.com, or by calling (425) 936-WISH.


Developing an Application in Visual Basic

It takes just a few minutes to build your first Visual Basic application. You create the user interface by "drawing" controls, such as text boxes and command buttons, on a form. Next, you set properties for the form and controls to specify such values as captions, color, and size. Finally, you write code to bring the application to life. The basic steps you take in creating your first application will show you principles that you'll use with every other application you develop.

This chapter provides an overview of the application development process, describes the terms and skills you need to use Visual Basic, and takes you step by step through several simple applications.

Topics

Visual Basic Concepts

A comparison of Visual Basic and traditional programming languages.
Elements of the Integrated Development Environment

An introduction to the Visual Basic environment and tools.
Your First Visual Basic Application

Step-by-step creation of a simple application, illustrating basic techniques.

Sample application

Firstapp.vbp

Some of the code examples in this chapter are taken from the Firstapp.vbp sample application which is listed in the Samples directory.


Visual Basic Concepts

In order to understand the application development process, it is helpful to understand some of the key concepts upon which Visual Basic is built. Because Visual Basic is a Windows development language, some familiarity with the Windows environment is necessary. If you are new to Windows programming, you need to be aware of some fundamental differences between programming for Windows versus other environments.

How Windows Works: Windows, Events and Messages

A complete discussion of the inner workings of Windows would require an entire book. A deep understanding of all of the technical details isn't necessary. A simplified version of the workings of Windows involves three key concepts: windows, events and messages.

Think of a window as simply a rectangular region with its own boundaries. You are probably already aware of several different types of windows: an Explorer window in Windows 95, a document window within your word processing program, or a dialog box that pops up to remind you of an appointment. While these are the most common examples, there are actually many other types of windows. A command button is a window. Icons, text boxes, option buttons and menu bars are all windows.

The Microsoft Windows operating system manages all of these many windows by assigning each one a unique id number (window handle or hWnd). The system continually monitors each of these windows for signs of activity or events. Events can occur through user actions such as a mouse click or a key press, through programmatic control, or even as a result of another window's actions.

Each time an event occurs, it causes a message to be sent to the operating system. The system processes the message and broadcasts it to the other windows. Each window can then take the appropriate action based on its own instructions for dealing with that particular message (for example, repainting itself when it has been uncovered by another window).

As you might imagine, dealing with all of the possible combinations of windows, events and messages could be mind-boggling. Fortunately, Visual Basic insulates you from having to deal with all of the low-level message handling. Many of the messages are handled automatically by Visual Basic; others are exposed as Event procedures for your convenience. This allows you to quickly create powerful applications without having to deal with unnecessary details.

Understanding the Event-Driven Model

In traditional or "procedural" applications, the application itself controls which portions of code execute and in what sequence. Execution starts with the first line of code and follows a predefined path through the application, calling procedures as needed.

In an event-driven application, the code doesn't follow a predetermined path — it executes different code sections in response to events. Events can be triggered by the user's actions, by messages from the system or other applications, or even from the application itself. The sequence of these events determines the sequence in which the code executes, thus the path through the application's code differs each time the program runs.

Because you can't predict the sequence of events, your code must make certain assumptions about the "state of the world" when it executes. When you make assumptions (for example, that an entry field must contain a value before running a procedure to process that value), you should structure your application in such a way as to make sure that the assumption will always be valid (for example, disabling the command button that starts the procedure until the entry field contains a value).

Your code can also trigger events during execution. For example, programmatically changing the text in a text box cause the text box's Change event to occur. This would cause the code (if any) contained in the Change event to execute. If you assumed that this event would only be triggered by user interaction, you might see unexpected results. It is for this reason that it is important to understand the event-driven model and keep it in mind when designing your application.

Interactive Development

The traditional application development process can be broken into three distinct steps: writing, compiling, and testing code. Unlike traditional languages, Visual Basic uses an interactive approach to development, blurring the distinction between the three steps.

With most languages, if you make a mistake in writing your code, the error is caught by the compiler when you start to compile your application. You must then find and fix the error and begin the compile cycle again, repeating the process for each error found. Visual Basic interprets your code as you enter it, catching and highlighting most syntax or spelling errors on the fly. It's almost like having an expert watching over your shoulder as you enter your code.

In addition to catching errors on the fly, Visual Basic also partially compiles the code as it is entered. When you are ready to run and test your application, there is only a brief delay to finish compiling. If the compiler finds an error, it is highlighted in your code. You can fix the error and continue compiling without having to start over.

Because of the interactive nature of Visual Basic, you'll find yourself running your application frequently as you develop it. This way you can test the effects of your code as you work rather than waiting to compile later.


Elements of the Integrated Development Environment

The working environment in Visual Basic is often referred to as the integrated development environment or IDE because it integrates many different functions such as design, editing, compiling, and debugging within a common environment. In most traditional development tools, each of these functions would operate as a separate program, each with its own interface. In this section, the following topics are discussed:

  • Starting the Visual Basic IDE Getting up and running.

  • Integrated Development Environment Elements An introduction to the various parts of the IDE.

  • Environment Options Configuring Visual Basic to your personal preferences.


Starting the Visual Basic IDE

When you run the Visual Basic Setup program, it allows you to place the program items in an existing program group or create a new program group and new program items for Visual Basic in Windows. You are then ready to start Visual Basic from Windows.

To start Visual Basic from Windows

  1. Click Start on the Task bar.

  2. Select Programs, and then Microsoft Visual Basic 6.0.

    –or–

    Click Start on the Task bar.

    Select Programs.

    Use the Windows Explorer to find the Visual Basic executable file.

  3. Double-click the Visual Basic icon.

You can also create a shortcut to Visual Basic, and double-click the shortcut.

When you first start Visual Basic, you see the interface of the integrated development environment, as shown in Figure 2.1.

Figure 2.1 The Visual Basic integrated development environment


Integrated Development Environment Elements

The Visual Basic integrated development environment (IDE) consists of the following elements.

Menu Bar

Displays the commands you use to work with Visual Basic. Besides the standard File, Edit, View, Window, and Help menus, menus are provided to access functions specific to programming such as Project, Format, or Debug.

Context Menus

Contain shortcuts to frequently performed actions. To open a context menu, click the right mouse button on the object you're using. The specific list of shortcuts available from context menus depends on the part of the environment where you click the right mouse button. For example, the context menu displayed when you right click on the Toolbox lets you display the Components dialog box, hide the Toolbox, dock or undock the Toolbox, or add a custom tab to the Toolbox.

Toolbars

Provide quick access to commonly used commands in the programming environment. You click a button on the toolbar once to carry out the action represented by that button. By default, the Standard toolbar is displayed when you start Visual Basic. Additional toolbars for editing, form design, and debugging can be toggled on or off from the Toolbars command on the View menu.

Toolbars can be docked beneath the menu bar or can "float" if you select the vertical bar on the left edge and drag it away from the menu bar.

Toolbox

Provides a set of tools that you use at design time to place controls on a form. In addition to the default toolbox layout, you can create your own custom layouts by selecting Add Tab from the context menu and adding controls to the resulting tab.

For More Information To learn more about specific controls, see "Forms, Controls, and Menus" and "Using Visual Basic's Standard Controls." For information on how to add controls to the Toolbox, see "Adding Controls to a Project" in "Managing Projects."

Project Explorer Window

Lists the forms and modules in your current project. A project is the collection of files you use to build an application.

For More Information For information on projects, see "Managing Projects."

Properties Window

Lists the property settings for the selected form or control. A property is a characteristic of an object, such as size, caption, or color.

For More Information For information on properties, see "Understanding Properties, Methods, and Events" in "Forms, Controls, and Menus."

Object Browser

Lists objects available for use in your project and gives you a quick way to navigate through your code. You can use the Object Browser to explore objects in Visual Basic and other applications, see what methods and properties are available for those objects, and paste code procedures into your application.

For More Information For more information on using the Object Browser to view procedures, see "Finding Out About Objects" in "Programming with Objects." For details on using add-ins to extend the Visual Basic programming environment, see "Using Wizards and Add-ins" in "Managing Projects."

Form Designer

Serves as a window that you customize to design the interface of your application. You add controls, graphics, and pictures to a form to create the look you want. Each form in your application has its own form designer window.

For More Information To learn how to add controls to an application, see "Your First Visual Basic Application" later in this chapter. To learn more about designing an interface, see "Creating a User Interface."

Code Editor Window

Serves as an editor for entering application code. A separate code editor window is created for each form or code module in your application.

For More Information To learn more about entering code and using the code editor, see "Programming Fundamentals."

Form Layout Window

The Form Layout window (Figure 2.2) allows you to position the forms in your application using a small graphical representation of the screen.

Figure 2.2 The Form Layout window

Immediate, Locals, and Watch Windows

These additional windows are provided for use in debugging your application. They are only available when you are running your application within the IDE.

For More Information To learn more about debugging and using the debug windows, see "Debugging Your Code and Handling Errors."

Note You can also add features to the Visual Basic interface by using a program called an add-in. Add-ins, which are available from Microsoft and third-party developers, can provide features like source code control, which allows you to support group development projects.


Environment Options

Visual Basic provides a great deal of flexibility, allowing you to configure the working environment to best suit your individual style. You can choose between a single or multiple document interface, and you can adjust the size and positioning of the various Integrated Development Environment (IDE) elements. Your layout will persist between sessions of Visual Basic.

SDI or MDI Interface

Two different styles are available for the Visual Basic IDE: single document interface (SDI) or multiple document interface (MDI). With the SDI option, all of the IDE windows are free to be moved anywhere on screen; as long as Visual Basic is the current application, they will remain on top of any other applications. With the MDI option, all of the IDE windows are contained within a single resizable parent window.

To switch between SDI and MDI modes

  1. Select Options from the Tools menu.

    The Options dialog box is displayed.

  2. Select the Advanced tab.

  3. Check or uncheck the SDI Development Environment check box.

    The IDE will start in the selected mode the next time you start Visual Basic.

    –or–

    Run Visual Basic from the command line with a /sdi or /mdi parameter.

Docking Windows

Many of the windows in the IDE can be docked, or connected, to each other or to the edge of the screen. These include the Toolbox, Form Layout Window, Project Explorer, Properties window, Color Palette, and Immediate, Locals, and Watch windows.

With the MDI option, windows can be docked to any side of the parent window; with SDI they can only be docked beneath the menu bar. Docking capabilities can be toggled on or off for a given window by selecting the appropriate check box on the Docking tab of the Options dialog box, available from the Options command on the Tools menu.

To dock or undock a window

  1. Select the window you wish to dock or undock.

  2. Drag the window to the desired location by holding down the left mouse button.

  3. The outline of the window will be displayed as you drag.

  4. Release the mouse button.


Your First Visual Basic Application

Creating an application in Visual Basic is simple. How simple? For the answer, try out the Hello, Visual Basic and Firstapp applications that follow.

  • Hello, Visual Basic Step-by-step creation of the classic "Hello World" application.

  • The Firstapp Sample Application Create your first "real" application by duplicating the Firstapp sample application.


Hello, Visual Basic

There are three main steps to creating an application in Visual Basic:

  1. Create the interface.

  2. Set properties.

  3. Write code.

To see how this is done, use the steps in the following procedures to create a simple application that consists of a text box and a command button. When you click the command button, the message "Hello, world!" appears in the text box.

Creating the Interface

Forms are the foundation for creating the interface of an application. You can use forms to add windows and dialog boxes to your application. You can also use them as containers for items that are not a visible part of the application's interface. For example, you might have a form in your application that serves as a container for graphics that you plan to display in other forms.

The first step in building a Visual Basic application is to create the forms that will be the basis for your application's interface. Then you draw the objects that make up the interface on the forms you create. For this first application, you'll use two controls from the Toolbox.

Button | Control
 | Text box
 | Command button

To draw a control using the Toolbox

  1. Click the tool for the control you choose to draw — in this case, the text box.

  2. Move the pointer onto your form. The pointer becomes a cross hair, as shown in Figure 2.3.

    Figure 2.3 Drawing a text box with the Toolbox

  3. Place the cross hair where you want the upper-left corner of the control.

  4. Drag the cross hair until the control is the size you want. (Dragging means holding the left mouse button down while you move an object with the mouse.)

  5. Release the mouse button.

    The control appears on the form.

Another simple way to add a control to a form is to double-click the button for that control in the Toolbox. This creates a default-size control located in the center of the form; then you can move the control to another location on the form.

Resizing, Moving, and Locking Controls

Notice that small rectangular boxes called sizing handles appear at the corners of the control; you'll use these sizing handles in the next step as you resize the control. You can also use the mouse, keyboard, and menu commands to move controls, lock and unlock control positions, and adjust their positions.

To resize a control

  1. Select the control you intend to resize by clicking it with the mouse.

    Sizing handles appear on the control.

  2. Position the mouse pointer on a sizing handle, and drag it until the control is the size you choose.

    The corner handles resize controls horizontally and vertically, while the side handles resize in only one direction.

  3. Release the mouse button.

    –or–

    Use SHIFT with the arrow keys to resize the selected control.

To move a control

  • Use the mouse to drag the control to a new location on the form.

    –or–

    Use the Properties window to change the Top and Left properties.

When a control is selected, you can use CTRL with the arrow keys to move the control one grid unit at a time. If the grid is turned off, the control moves one pixel at a time.

To lock all control positions

  • From the Format menu, choose Lock Controls.

    –or–

    Click the Lock Controls Toggle button on the Form Editor toolbar.

This will lock all controls on the form in their current positions so that you don't inadvertently move them once you have them in the desired location. This will lock controls only on the selected form; controls on other forms are untouched. This is a toggle command, so you can also use it to unlock control positions.

To adjust the position of locked controls

  • You can "nudge" the control that has the focus by holding CTRL down and pressing the appropriate arrow key.

    –or–

    You can change the control's Top and Left properties in the Property window.

You now have the interface for the "Hello, world!" application, as shown in Figure 2.4.

Figure 2.4 The interface for the "Hello, world!" application

Setting Properties

The next step is to set properties for the objects you've created. The Properties window (Figure 2.5) provides an easy way to set properties for all objects on a form. To open the Properties window, choose the Properties Window command from the View menu, click the Properties Window button on the toolbar, or use the context menu for the control.

Figure 2.5 The Properties window

The Properties window consists of the following elements:

  • Object box — Displays the name of the object for which you can set properties. Click the arrow to the right of the object box to display the list of objects for the current form.

  • Sort tabs — Choose between an alphabetic listing of properties or a hierarchical view divided by logical categories, such as those dealing with appearance, fonts, or position.

  • Properties list — The left column displays all of the properties for the selected object. You can edit and view settings in the right column.

To set properties from the Properties window

  1. From the View menu, choose Properties, or click the Properties button on the toolbar.

    The Properties window displays the settings for the selected form or control.

  2. From the Properties list, select the name of a property.

  3. In the right column, type or select the new property setting.

    Enumerated properties have a predefined list of settings. You can display the list by clicking the down arrow at the right of the Settings box, or you can cycle through the list by double-clicking a list item.

For the "Hello, world!" example, you'll need to change three property settings. Use the default settings for all other properties.

Object | Property | Setting
Form | Caption | Hello, world!
Text box | Text | (Empty)
Command button | Caption | OK

Setting the Icon Property

All forms in Visual Basic have a generic, default icon that appears when you minimize that form. However, you will probably change this icon to one that illustrates the use of the form or your application. To assign an icon to a form, set the Icon property for that form. You can use 32 x 32 pixel icons that were standard in 16-bit versions of Microsoft Windows and are also used in Windows 95 and Windows NT, as well as the 16 x 16 pixel icons used in Windows 95.

Writing Code

The Code Editor window is where you write Visual Basic code for your application. Code consists of language statements, constants, and declarations. Using the Code Editor window, you can quickly view and edit any of the code in your application.

To open the Code window

  • Double-click the form or control for which you choose to write code.

    –or–

    From the Project Explorer window, select the name of a form or module, and choose the View Code button.

Figure 2.6 shows the Code Editor window that appears when you double-click the Command button control, and the events for that command.

Figure 2.6 The Code Editor window

You can choose to display all procedures in the same Code window, or display a single procedure at a time.

To display all procedures in the same Code window

  1. From the Tools menu, select the Options dialog box.

  2. On the Editor tab in the Options dialog box, select the check box to the left of Default to Full Module View. The check box to the left of Procedure Separator adds or removes a separator line between procedures.

    –or–

    Click the Full Module View button in the lower left corner of the Code Editor window.

To display one procedure at a time in the Code window

  1. From the Tools menu, select the Options dialog box.

  2. On the Editor tab in the Options dialog box, clear the check box to the left of Default to Full Module View.

    –or–

    Click the Procedure View button in the lower left corner of the Code Editor window.

The Code window includes the following elements:

  • Object list box — Displays the name of the selected object. Click the arrow to the right of the list box to display a list of all objects associated with the form.

  • Procedure list box — Lists the procedures, or events, for an object. The box displays the name of the selected procedure — in this case, Click. Choose the arrow to the right of the box to display all the procedures for the object.

Creating Event Procedures

Code in a Visual Basic application is divided into smaller blocks called procedures. An event procedure, such as those you'll create here, contains code that is executed when an event occurs (such as when a user clicks a button). An event procedure for a control combines the control's actual name (specified in the Name property), an underscore (_), and the event name. For example, if you want a command button named Command1 to invoke an event procedure when it is clicked, use the procedure Command1_Click.

To create an event procedure

  1. In the Object list box, select the name of an object in the active form. (The active form is the form that currently has the focus.)

    For this example, choose the command button, Command1.

  2. In the Procedure list box, select the name of an event for the selected object.

    Here, the Click procedure is already selected, because it's the default procedure for a command button. Note that a template for the event procedure is now displayed in the Code window.

  3. Type the following code between the Sub and End Sub statements:
    Text1.Text = "Hello, world!"

    The event procedure should look like this:

    Private Sub Command1_Click ()
   Text1.Text = "Hello, world!"
End Sub

You'll note here that the code is simply changing the Text property of the control named Text1 to read "Hello, world!" The syntax for this example takes the form of object.property, where Text1 is the object and Text is the property. You can use this syntax to change property settings for any form or control in response to events that occur while your application is running.

For More Information For information on creating other types of procedures, see "Introduction to Procedures" in "Programming Fundamentals."

Running the Application

To run the application, choose Start from the Run menu, or click the Start button on the toolbar, or press F5. Click the command button you've created on the form, and you'll see "Hello, world!" displayed in the text box.


The Firstapp Sample Application

Visual Basic provides you with a wealth of tools beyond the ones used in this first application, so you'll soon use many other features to manage and customize your applications. Reviewing sample applications can be an excellent way to learn more about Visual Basic. The following example illustrates how easy it can be to create a useful application in Visual Basic.

The Firstapp application demonstrates how a data control and a grid control can be used to display a table of information from a database. Visual Basic makes it easy to access database information from within your application. The data control provides the ability to navigate through the database recordset, synchronizing the display of records in the grid control with the position in the recordset.

The application consists of a data control, a MSFlexGrid control, a list box control, and two command buttons. The grid displays a table of information about products retrieved from the Northwind database. As the user selects an item by using the navigation buttons on the data control, the name of the selected product is displayed in the data control. The user can also add items to a "shopping list" in the list box control by double-clicking the current selection in the grid.

To add items to the list box, you use the AddItem method. (A method is a Visual Basic function that acts on a particular object, in this case a ListBox object.) The syntax for specifying a method (object.method) is similar to the syntax for setting a property (object.property). The AddItem method allows you to dynamically add items to the list box while the application is running. Conversely, the Clear method is used to remove all items from the list box.

For More Information To learn more about methods, see "Understanding Properties, Methods, and Events" in "Forms, Controls, and Menus."

Creating a Project

You begin creating this application by choosing New Project from the File menu, then selecting Standard EXE in the New Project dialog box (when you first start Visual Basic, the New Project dialog box is presented). Visual Basic creates a new project and displays a new form. To draw the interface, you use a data control, a MSFlexGrid control, a list box control, and two command buttons. The MSFlexGrid control isn't in the default toolbox, so you'll need to add it:

To add a control to the toolbox

  1. Select Components from the context menu for the toolbox. (You can right-click within the toolbox window to display the context menu.)

    The Components dialog box will be displayed.

  2. Find the MSFlexGrid (Microsoft Flex Grid 6.0) in the Controls list box and select the check box to its left.

  3. Click the OK button.

The icon for the MSFlexGrid control will appear in the toolbox.

Use the Toolbox to draw a data control, an MSFlexGrid control, a list box control, and two command buttons on the form. If you don't remember how, check out "Creating the Interface" earlier in this chapter.

Setting Properties

In the Properties window, set properties for the objects according to the following table. Use the default settings for all other properties.

Object | Property | Setting
Form | Caption | Products
Data1 | DatabaseName RecordSource | path \Nwind.mdb Products
MSFlexGrid1 | DataSource | Data1
Command1 | Caption | Clear
Command2 | Caption | Exit

The DatabaseName property of the data control must include the actual path to the database. By default, the Nwind.mdb database is installed in the same directory as Visual Basic. When you select the DatabaseName property in the Properties window, you can click the button to the right of the property to display a standard File Open dialog box to browse for the file. Once the DatabaseName property has been set, the RecordSource property in the Properties window will contain a list of tables or recordsets for the selected database. Setting the DataSource property of the MSFlexGrid control to Data1 automatically links the grid to the data control.

Writing Event Code

All the code for the application is contained in the Command1_Click, Command2_Click, Data1_Reposition, and MSFlexGrid1_DblClick event procedures. Double-click the form or control to display the Code window, and then type the code for each event procedure.

Add this code to the Command1_Click event procedure to clear the list box when the user clicks the button:

Private Sub Command1_Click ()
   List1.Clear               ' Clears the list box.
End Sub

In the above statement, you are invoking the Clear method of the list box, List1. The Clear method deletes the contents of the list box.

Add this code to the Command2_Click event procedure to unload the form from memory and end the application:

Private Sub Command2_Click ()
   Unload Form1
   End                     ' Ends application.
End Sub

In the above procedure, the first statement invokes the Unload event for the form. If you needed to perform a function at shutdown, such as saving a file, you could place that code in the form's Unload event procedure. The second statement calls the End function, which ends the application.

Add this code to the Data1_Reposition event procedure to update the caption each time a record is selected:

Private Sub Data1_Reposition ()
   Data1.Caption = Data1.Recordset("ProductName")
End Sub

In the above statement, you are assigning the value on the right (the contents of the Title field in the Recordset of the data control) to the property on the left (the Caption property of the data control object).

Add this code to the MSFlexGrid_DblClick event procedure to add an item to the list box when the user double-clicks a selected row:

Private Sub MSFlexGrid1_DblClick ()
   List1.AddItem MSFlexGrid.Text
End Sub

In the above statement, you are invoking the AddItem method of the list box (List1). The text to be added to the list box is contained in the argument of the method, in this case, the value of the title field in the recordset of the data control. Passing a value to an argument is similar to assigning a value to a property; unlike the assignment statement, the equal sign isn't required.

Saving a Project

You finish your work on the application by choosing Save Project from the File menu. Visual Basic will prompt you separately to save the form and then the project. One possible name for the project is "Northwind Shopping List." Both Windows 95 and Windows NT allow you to use file names up to 255 characters in length, and file names can include spaces. Older versions of Microsoft Windows limited you to file names of eight characters, with a three-character extension.

Enhancing Your Application

You have just completed your first Visual Basic application: one that performs a simple but useful function. You can use this application as a basis for adding similar functionality in your own applications, substituting your own data instead of Nwind.mdb. Of course, to make this application truly useful, you might want to add functionality to save or print the contents of the list box, to add additional information such as price and availability, and even to gather credit card information and transmit an order across the Internet. As you continue on through the rest of the Programmer's Guide, you will find examples of doing all that and a lot more.


Forms, Controls, and Menus

The first step to creating an application with Visual Basic is to create the interface, the visual part of the application with which the user will interact. Forms and controls are the basic building blocks used to create the interface; they are the objects that you will work with to build your application.

Forms are objects that expose properties which define their appearance, methods which define their behavior, and events which define their interaction with the user. By setting the properties of the form and writing Visual Basic code to respond to its events, you customize the object to meet the requirements of your application.

Controls are objects that are contained within form objects. Each type of control has its own set of properties, methods and events that make it suitable for a particular purpose. Some of the controls you can use in your applications are best suited for entering or displaying text. Other controls let you access other applications and process data as if the remote application was part of your code.

This chapter introduces the basic concepts of working with forms and controls and their associated properties, methods, and events. Many of the standard controls are discussed, as well as form-specific items such as menus and dialog boxes.

Topics

Understanding Properties, Methods and Events

An introduction to objects and their associated properties, methods, and events.
Designing a Form

The basics of working with a form's properties, methods, and events.
Clicking Buttons to Perform Actions

An introduction to the command button control.
Controls for Displaying and Entering Text

An introduction to the label and text box controls.
Controls That Present Choices to Users

An introduction to the check box, option button, list box, combo box, and scroll bar controls.
Controls That Display Pictures and Graphics

An introduction to the picture box, image, shape, and line controls.
Additional Controls

An introduction to the other standard Visual Basic controls.
Understanding Focus

A brief discussion of focus as it applies to controls.
Setting the Tab Order

An introduction to the concepts of tab order within a form.
Menu Basics

An introduction to menu controls and the menu editor.
Prompting the User with Dialog Boxes

An introduction to dialog boxes.

Sample application

Controls.vbp

The code examples in this chapter are taken from the Controls.vbp sample application which is listed in the Samples directory.


Understanding Properties, Methods and Events

Visual Basic forms and controls are objects which expose their own properties, methods and events. Properties can be thought of as an object's attributes, methods as its actions, and events as its responses.

An everyday object like a child's helium balloon also has properties, methods and events. A balloon's properties include visible attributes such as its height, diameter and color. Other properties describe its state (inflated or not inflated), or attributes that aren't visible such as its age. By definition, all balloons have these properties; the settings of these properties may differ from one balloon to another.

A balloon also has inherent methods or actions that it might perform. It has an inflate method (the action of filling it with helium), a deflate method (expelling its contents) and a rise method (if you were to let go of it). Again, all balloons are capable of these methods.

Balloons also have predefined responses to certain external events. For instance, a balloon would respond to the event of being punctured by deflating itself, or to the event of being released by rising into the air.

Figure 3.1 Objects have properties, respond to events, and perform methods

If you were able to program a balloon, the Visual Basic code might look like the following. To set the balloon's properties:

Balloon.Color = Red
Balloon.Diameter = 10
Balloon.Inflated = True

Note the syntax of the code — the object (Balloon) followed by the property (.Color) followed by the assignment of the value (Red). You could change the color of the balloon from code by repeating this statement and substituting a different value. Properties can also be set in the Properties window while you are designing your application.

A balloon's methods are invoked like this:

Balloon.Inflate
Balloon.Deflate
Balloon.Rise 5

The syntax is similar to the property — the object (a noun) followed by the method (a verb). In the third example, there is an additional item, called an argument, which denotes the distance to rise. Some methods will have one or more arguments to further describe the action to be performed.

The balloon might respond to an event as follows:

Sub Balloon_Puncture()
   Balloon.Deflate
   Balloon.MakeNoise "Bang"
   Balloon.Inflated = False
   Balloon.Diameter = 1
End Sub

In this case, the code describes the balloon's behavior when a puncture event occurs: invoke the Deflate method, then invoke the MakeNoise method with an argument of "Bang" (the type of noise to make). Since the balloon is no longer inflated, the Inflated property is set to False and the Diameter property is set to a new value.

While you can't actually program a balloon, you can program a Visual Basic form or control. As the programmer, you are in control. You decide which properties should be changed, methods invoked or events responded to in order to achieve the desired appearance and behavior.


Designing a Form

Form objects are the basic building blocks of a Visual Basic application, the actual windows with which a user interacts when they run the application. Forms have their own properties, events, and methods with which you can control their appearance and behavior.

Figure 3.2 Forms and controls have their own properties, events, and methods

The first step in designing a form is to set its properties. You can set a form's properties at design time in the Properties window, or at run time by writing code.

Note You work with forms and controls, set their properties, and write code for their events at design time, which is any time you're building an application in the Visual Basic environment. Run time is any time you are actually running the application and interacting with the application as the user would.

Setting Form Properties

Many of a form's properties affect its physical appearance. The Caption property determines the text that is shown in the form's title bar; the Icon property sets the icon that is displayed when a form is minimized. The MaxButton and MinButton properties determine whether the form can be maximized or minimized. By changing the BorderStyle property, you can control the resizing behavior of the form.

Height and Width properties determine the initial size of a form; Left and Top properties determine the form's location in relation to the upper left-hand corner of the screen. The WindowState property can be set to start the form in a maximized, minimized, or normal state.

The Name property sets the name by which you will refer to the form in code. By default, when a form is first added to a project, its name is set to Form1, Form2, and so forth. It's a good idea to set the Name property to something more meaningful, such as "frmEntry" for an order entry form.

The best way to familiarize yourself with the many form properties is to experiment. Change some of the properties of a form in the Properties window (Figure 3.3), then run the application to see their effect. You can learn more about each property by selecting it and pressing F1 to view the context-sensitive Help.

Figure 3.3 The Properties window

Form Events and Methods

As objects, forms can perform methods and respond to events.

The Resize event of a form is triggered whenever a form is resized, either by user interaction or through code. This allows you to perform actions such as moving or resizing controls on a form when its dimensions have changed.

The Activate event occurs whenever a form becomes the active form; the Deactivate event occurs when another form or application becomes active. These events are convenient for initializing or finalizing the form's behavior. For example, in the Activate event you might write code to highlight the text in a particular text box; in the Deactivate event you might save changes to a file or database.

To make a form visible, you would invoke the Show method:

Form2.Show

Invoking the Show method has the same effect as setting a form's Visible property to True.

Many of a form's methods involve text or graphics. The Print, Line, Circle, and Refresh methods are useful for printing or drawing directly onto a form's surface. These methods and more are discussed in "Working with Text and Graphics."

For More Information For additional information on forms, see "More About Forms" in "Creating a User Interface."


Clicking Buttons to Perform Actions

The easiest way to allow the user to interact with an application is to provide a button to click. You can use the command button control provided by Visual Basic, or you can create your own "button" using an image control containing a graphic, such as an icon.

Using Command Buttons

Most Visual Basic applications have command buttons that allow the user to simply click them to perform actions. When the user chooses the button, it not only carries out the appropriate action, it also looks as if it's being pushed in and released. Whenever the user clicks a button, the Click event procedure is invoked. You place code in the Click event procedure to perform any action you choose.

There are many ways to choose a command button at run time:

  • Use a mouse to click the button.

  • Move the focus to the button by pressing the TAB key, and then choose the button by pressing the SPACEBAR or ENTER. (See "Understanding Focus" later in this chapter.)

  • Press an access key (ALT+ the underlined letter) for a command button.

  • Set the command button's Value property to True in code:
    cmdClose.Value = True
  • Invoke the command button's Click event in code:
    cmdClose_Click
  • If the command button is the default command button for the form, pressing ENTER chooses the button, even if you change the focus to a different control other than a command button. At design time, you specify a default command button by setting that button's Default property to True.

  • If the command button is the default Cancel button for the form, then pressing ESC chooses the button, even if you change the focus to another control. At design time, you specify a default Cancel button by setting that button's Cancel property to True.

All these actions cause Visual Basic to invoke the Click event procedure.

The Test Buttons Application

You use the Caption property to display text on the button to tell the user what the button does. In Figure 3.4, the Test Buttons example from the Controls sample application contains a command button with its Caption property set to "Change Signal." (For a working version of this example, see Button.frm in the Controls.vbp sample application.)

Notice that 'S' is the access key for this button, denoted by an underline. Inserting an ampersand (&) in the text of the Caption property makes the character following it the access key for that button (for example, Change &Signal).

Figure 3.4 Command button with a caption

When a user clicks the command button, the code in the command button's Click event procedure is executed. In the example, a different traffic light icon is displayed each time the button is clicked.

For More Information For information on additional properties of the command button, see "Using Visual Basic's Standard Controls."


Controls for Displaying and Entering Text

Label and text box controls are used to display or enter text. Use labels when you want your application to display text on a form, and text boxes when you want to allow the user to enter text. Labels contain text that can only be read, while text boxes contain text that can be edited.

To provide this feature | Use this control
Text that can be edited by the user, for example an order entry field or a password box | Text box
Text that is displayed only, for example to identify a field on a form or display instructions to the user | Label

Labels and text boxes are discussed in the following sections:

  • Using Labels to Display Text The basics of using the label control.

  • Working with Text Boxes The basics of using text boxes.


Using Labels to Display Text

A label control displays text that the user cannot directly change. You can use labels to identify controls, such as text boxes and scroll bars, that do not have their own Caption property. The actual text displayed in a label is controlled by the Caption property, which can be set at design time in the Properties window or at run time by assigning it in code.

By default, the caption is the only visible part of the label control. However, if you set the BorderStyle property to 1 (which you can do at design time), the label appears with a border — giving it a look similar to a text box. You can also change the appearance of the label by setting the BackColor, BackStyle, ForeColor, and Font properties.

Sizing a Label to Fit Its Contents

Single-line label captions can be specified at design time in the Properties window. But what if you want to enter a longer caption, or a caption that will change at run time? Labels have two properties that help you size the controls to fit larger or smaller captions: AutoSize and WordWrap.

The AutoSize property determines if a control should be automatically resized to fit its contents. If set to True, the label grows horizontally to fit its contents, as shown in Figure 3.5.

Figure 3.5 AutoSize example

The WordWrap property causes the label to grow vertically to fit its contents, while retaining the same width, as shown in Figure 3.6. For a working version of this example, see Wordwrap.frm in the Controls.vbp sample application.

Figure 3.6 WordWrap example

Note If you run the AutoSize example from Controls.vbp, you'll notice that for the WordWrap example to actually work, both check boxes must be selected. This is because, for the label's WordWrap property to take effect, AutoSize must be set to True. The width of the label is increased only if the width of a single word exceeds the current width of the control.

For More Information For additional information on the label control's properties, see "Using Visual Basic's Standard Controls."


Working with Text Boxes

Text boxes are versatile controls that can be used to get input from the user or to display text. Text boxes should not be used to display text that you don't want the user to change, unless you've set the Locked property to True.

The actual text displayed in a text box is controlled by the Text property. It can be set in three different ways: at design time in the Property window, at run time by setting it in code, or by input from the user at run time. The current contents of a text box can be retrieved at run time by reading the Text property.

Multiple-Line Text Boxes and Word Wrap

By default, a text box displays a single line of text and does not display scroll bars. If the text is longer than the available space, only part of the text will be visible. The look and behavior of a text box can be changed by setting two properties, MultiLine and ScrollBars, which are available only at design time.

Note The ScrollBars property should not be confused with scroll bar controls, which are not attached to text boxes and have their own set of properties.

Setting MultiLine to True enables a text box to accept or display multiple lines of text at run time. A multiple-line text box automatically manages word wrap as long as there is no horizontal scroll bar. The ScrollBars property is set to 0-None by default. Automatic word wrap saves the user the trouble of inserting line breaks at the end of lines. When a line of text is longer than what can be displayed on a line, the text box wraps the text to the next line.

Line breaks cannot be entered in the Properties window at design time. Within a procedure, you create a line break by inserting a carriage return followed by a linefeed (ANSI characters 13 and 10). You can also use the constant vbCrLf to insert a carriage return/linefeed combination. For example, the following event procedure puts two lines of text into a multiple-line text box (Text1) when the form is loaded:

Sub Form_Load ()
   Text1.Text = "Here are two lines" _
   & vbCrLf & "in a text box"
End Sub

Working with Text in a Text Box

You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties. These properties are only available at run time.

When a text box first receives the focus, the default insertion point or cursor position within the text box is to the left of any existing text. It can be moved by the user from the keyboard or with the mouse. If the text box loses and then regains the focus, the insertion point will be wherever the user last placed it.

In some cases, this behavior can be disconcerting to the user. In a word processing application, the user might expect new characters to appear after any existing text. In a data entry application, the user might expect their typing to replace any existing entry. The SelStart and SelLength properties allow you to modify the behavior to suit your purpose.

The SelStart property is a number that indicates the insertion point within the string of text, with 0 being the left-most position. If the SelStart property is set to a value equal to or greater than the number of characters in the text box, the insertion point will be placed after the last character, as shown in Figure 3.7. For a working version of this example, see Text.frm in the Controls.vbp sample application.

Figure 3.7 Insertion point example

The SelLength property is a numeric value that sets the width of the insertion point. Setting the SelLength to a number greater than 0 causes that number of characters to be selected and highlighted, starting from the current insertion point. Figure 3.8 shows the selection behavior.

Figure 3.8 Selection example

If the user starts typing while a block of text is selected, the selected text will be replaced. In some cases, you might want to replace a text selection with new text by using a paste command. The SelText property is a string of text that you can assign at run time to replace the current selection. If no text is selected, SelText will insert its text at the current insertion point.

For More Information For additional information on the text box control's properties, see "Using Visual Basic's Standard Controls."


Controls That Present Choices to Users

Most applications need to present choices to their users, ranging from a simple yes/no option to selecting from a list containing hundreds of possibilities. Visual Basic includes several standard controls that are useful for presenting choices. The following table summarizes these controls and their appropriate uses.

To provide this feature | Use this control
A small set of choices from which a user can choose one or more options. | Check boxes
A small set of options from which a user can choose just one. | Option buttons (use frames if additional groups are needed)
A scrollable list of choices from which the user can choose. | List box
A scrollable list of choices along with a text edit field. The user can either choose from the list or type a choice in the edit field. | Combo box

Check boxes, option buttons, list boxes, and combo boxes are discussed in the following sections:

  • Selecting Individual Options with Check Boxes The basics of using the check box control.

  • Grouping Options with Option Buttons The basics of using the option button control.

  • Using List Boxes and Combo Boxes An introduction to the list box and combo box controls.

  • Using Scroll Bars as Input Devices A brief introduction to the scroll bar control.


Selecting Individual Options with Check Boxes

A check box indicates whether a particular condition is on or off. You use check boxes in an application to give users true/false or yes/no options. Because check boxes work independently of each other, a user can select any number of check boxes at the same time. For example in Figure 3.9, Bold and Italic can both be checked.

Figure 3.9 Check boxes

The Check Box Application

The Check Box example uses a check box to determine whether the text is displayed in regular or italic font. For a working version of this example, see Check.frm in the Controls.vbp sample application.

The application has a text box, a label, a command button, and two check boxes, as shown in Figure 3.10.

Figure 3.10 Check box example

The following table lists the property settings for the objects in the application.

Object | Property | Setting
Form | Name Caption | frmCheck Check Box Example
Text box | Name Text | txtDisplay Some sample text
First Check box | Name Caption | chkBold &Bold
Second Check box | Name Caption | chkItalic &Italic
Command button | Name Caption | cmdClose &Close

When you check Bold or Italic, the check box's Value property is set to 1; when unchecked, its Value property is set to 0. The default Value is 0, so unless you change Value, the check box will be unchecked when it is first displayed. You can use the constants vbChecked and vbUnchecked to represent the values 1 and 0.

Events in the Check Box Application

The Click event for the check box occurs as soon as you click the box. This event procedure tests to see whether the check box has been selected (that is, if its Value = vbChecked). If so, the text is converted to bold or italic by setting the Bold or Italic properties of the Font object returned by the Font property of the text box.

Private Sub chkBold_Click ()
   If ChkBold.Value = vbChecked Then   ' If checked.
      txtDisplay.Font.Bold = True
   Else                           ' If not checked.
      txtDisplay.Font.Bold = False
   End If
End Sub

Private Sub chkItalic_Click ()
   If ChkItalic.Value = vbChecked Then   ' If checked.
      txtDisplay.Font.Italic = True
   Else                           ' If not checked.
      txtDisplay.Font.Italic = False
   End If
End Sub


Grouping Options with Option Buttons

Option buttons present a set of two or more choices to the user. Unlike check boxes, however, option buttons should always work as part of a group; selecting one option button immediately clears all the other buttons in the group. Defining an option button group tells the user, "Here is a set of choices from which you can choose one and only one."

For example, in the option button group shown in Figure 3.11, the user can select one of three option buttons.

Figure 3.11 Selecting an option button

Creating Option Button Groups

All of the option buttons placed directly on a form (that is, not in a frame or picture box) constitute one group. If you want to create additional option button groups, you must place some of them inside frames or picture boxes.

All the option buttons inside any given frame constitute a separate group, as do all the option buttons inside a picture box. When you create a separate group this way, always draw the frame or picture box first, and then draw the option buttons on top of it. Figure 3.12 shows a form with two option button groups.

Figure 3.12 Option button groups

A user can select only one option button in the group when you draw option buttons in a frame.

To group controls in a frame

  1. Select the frame control from the toolbox and draw the frame on the form.

  2. Select the option button control from the toolbox and draw the control within the frame.

  3. Repeat step 2 for each additional option button you wish to add to the frame.

Drawing the frame first and then drawing each control on the frame allows you to move the frame and controls together. If you try to move existing controls onto a frame, the controls will not move with the frame.

Note If you have existing controls that you want to group in a frame, you can select all the controls and cut and paste them into a frame or picture control.

Containers for Controls

While controls are independent objects, a certain parent and child relationship exists between forms and controls. Figure 3.12 demonstrates how option buttons can be contained within a form or within a frame control.

To understand the concept of containers, you need to understand that all controls are children of the form on which they are drawn. In fact, most controls support the read-only Parent property, which returns the form on which a control is located. Being a child affects the placement of a control on the parent form. The Left and Top properties of a control are relative to the parent form, and controls cannot be moved outside the boundaries of the parent. Moving a container moves the controls as well, and the control's position relative to the container's Left and Top properties does not change because the control moves with the container.

Selecting or Disabling Option Buttons

An option button can be selected by:

  • Clicking it at run time with the mouse.

  • Tabbing to the option button group and then using the arrow keys to select an option button within the group.

  • Assigning its Value property to True in code:
    optChoice.Value = True
  • Using a shortcut key specified in the caption of a label.

To make a button the default in an option button group, set its Value property to True at design time. It remains selected until a user selects a different option button or code changes it.

To disable an option button, set its Enabled property to False. When the program is run it will appear dimmed, meaning that it is unavailable.

The Options Application

The form shown in Figure 3.13 uses option buttons to determine the processor type and operating system for a fictional computer. When the user selects a option button in either group, the caption of the label is changed to reflect the current choices. For a working version of this example, see Options.frm in the Controls.vbp sample application.

Figure 3.13 Option button example

The following table lists the property settings for the objects in the application.

Object | Property | Setting
Label | Name Caption | lblDisplay (Empty)
Command button | Name Caption | cmdClose &Close
First option button | Name Caption | opt486 &486
Second option button | Name Caption Value | opt586 &Pentium True
Third option button | Name Caption | opt686 P&entium Pro
Frame | Name Caption | fraSystem &Operating System
Fourth option button | Name Caption | optWin95 Windows 95
Fifth option button | Name Caption Value | optWinNT Windows NT True

Events in the Options Application

The Options application responds to events as follows:

  • The Click events for the first three option buttons assign a corresponding description to a form-level string variable, strComputer.

  • The Click events for the last two option buttons assign a corresponding description to a second form-level variable, strSystem.

The key to this approach is the use of these two form-level variables, strComputer and strSystem. These variables contain different string values, depending on which option buttons were last selected.

Each time a new option button is selected, the code in its Click event updates the appropriate variable:

Private Sub opt586_Click()
   strComputer = "Pentium"
   Call DisplayCaption
End Sub

It then calls a sub procedure, called DisplayCaption, that concatenates the two variables and updates the label's Caption property:

Sub DisplayCaption()
   lblDisplay.Caption = "You selected a " & _
   strComputer & " running " & strSystem
End Sub

A sub procedure is used because the procedure of updating the Caption property is essentially the same for all five option buttons, only the value of the variables change from one instance to the next. This saves you from having to repeat the same code in each of the Click events.

For More Information Variables and sub procedures are discussed in detail in "Programming Fundamentals."


Using List Boxes and Combo Boxes

List boxes and combo boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the combo box or list box, scroll bars automatically appear on the control. The user can then scroll up and down or left to right through the list. Figure 3.14 shows a single-column list box.

Figure 3.14 Single-column list box

A combo box control combines the features of a text box and a list box. This control allows the user to select either by typing text into the combo box or by selecting an item from its list. Figure 3.15 shows a combo box.

Figure 3.15 Combo box

In contrast to some other controls that contain a single value; for example the label's Caption property or the text box's Text property, list boxes and combo boxes contain multiple values or a collection of values. They have built-in methods for adding, removing and retrieving values from their collections at run time. To add several items to a list box named List1, the code would look like this:

List1.AddItem "Paris"
List1.AddItem "New York"
List1.AddItem "San Francisco"

List boxes and combo boxes are an effective way to present a large number of choices to the user in a limited amount of space.

For More Information For additional information on the list box and combo box controls, see "Using Visual Basic's Standard Controls."


Using Scroll Bars as Input Devices

Although scroll bars are often tied to text boxes or windows, you'll sometimes see them used as input devices. Because these controls can indicate the current position on a scale, scroll bar controls can be used individually to control program input — for example, to control the sound volume or to adjust the colors in a picture. The HScrollBar (horizontal) and VScrollBar (vertical) controls operate independently from other controls and have their own set of events, properties, and methods. Scroll bar controls are not the same as the built-in scroll bars that are attached to text boxes, list boxes, combo boxes, or MDI forms (text boxes and MDI forms have a ScrollBars property to add or remove scroll bars that are attached to the control).

Windows interface guidelines now suggest using slider controls as input devices instead of scroll bars. Examples of slider controls can be seen in the Windows 95 control panel. A Windows 95 style slider control is included in the Professional and Enterprise editions of Visual Basic.

For More Information For additional information on scroll bar controls, see "Using Visual Basic's Standard Controls."


Controls That Display Pictures and Graphics

Because Windows is a graphical user interface, it's important to have a way to display graphical images in your application's interface. Visual Basic includes four controls that make it easy to work with graphics: the picture box control, the image control, the shape control, and the line control.

The image, shape and line controls are sometimes referred to as "lightweight" graphical controls. They require less system resources and consequently display somewhat faster than the picture box control; they contain a subset of the properties, methods and events available in the picture box. Each is best suited for a particular purpose.

To provide this feature | Use this control
A container for other controls. | Picture box
Printing or graphics methods. | Picture box
Displaying a picture. | Image control or picture box
Displaying a simple graphical element | Shape or line control

Picture boxes, image controls, shape controls, and line controls are discussed in the following sections:

  • Working with the Picture Box Control The basics of using the picture box control.

  • Lightweight Graphical Controls The basics of using the image, shape, and line controls.

  • The Images Application An example of using the graphical controls.


Working With the Picture Box Control

The primary use for the picture box control is to display a picture to the user. The actual picture that is displayed is determined by the Picture property. The Picture property contains the file name (and optional path) for the picture file that you wish to display.

Note Form objects also have a Picture property that can be set to display a picture directly on the form's background.

To display or replace a picture at run time, you can use the LoadPicture function to set the Picture property. You supply the name (and optional path) for the picture and the LoadPicture function handles the details of loading and displaying it:

picMain.Picture = LoadPicture("VANGOGH.BMP")

The picture box control has an AutoSize property that, when set to True, causes the picture box to resize automatically to match the dimensions of its contents. Take extra care in designing your form if you plan on using a picture box with the AutoSize enabled. The picture will resize without regard to other controls on the form, possibly causing unexpected results, such as covering up other controls. It's a good idea to test this by loading each of the pictures at design time.

Using the Picture Box as a Container

The picture box control can also be used as a container for other controls. Like the frame control, you can draw other controls on top of the picture box. The contained controls move with the picture box and their Top and Left properties will be relative to the picture box rather than the form.

A common use for the picture box container is as a toolbar or status bar. You can place image controls on it to act as buttons, or add labels to display status messages. By setting the Align property to Top, Bottom, Left, or Right, the picture box will "stick" to the edge of the form. Figure 3.16 shows a picture box with its Align property set to Bottom. It contains two label controls which could be used to display status messages.

Figure 3.16 Picture box used as a status bar

Other Uses for the Picture Box

The picture box control has several methods that make it useful for other purposes. Think of the picture box as a blank canvas upon which you can paint, draw or print. A single control can be used to display text, graphics or even simple animation.

The Print method allows you to output text to the picture box control just as you would to a printer. Several font properties are available to control the characteristics of text output by the Print method; the Cls method can be used to erase the output.

Circle, Line, Point and Pset methods may be used to draw graphics on the picture box. Properties such as DrawWidth, FillColor, and FillStyle allow you to customize the appearance of the graphics.

Animation can be created using the PaintPicture method by moving images within the picture control and rapidly changing between several different images.

For More Information For additional information on the picture box control, see "Using Visual Basic's Standard Controls."


Lightweight Graphical Controls

The image, shape and line controls are considered to be lightweight controls; that is, they support only a subset of the properties, methods, and events found in the picture box. Because of this, they typically require less system resources and load faster than the picture box control.

Using Image Controls Instead of Picture Boxes

The image control is similar to the picture box control but is used only for displaying pictures. It doesn't have the ability to act as a container for other controls, and it doesn't support the advanced methods of the picture box.

Pictures are loaded into the image control just as they are in the picture box: at design time, set the Picture property to a file name and path; at run time, use the LoadPicture function.

The sizing behavior of the image control differs from that of the picture box. It has a Stretch property while the picture box has an AutoSize property. Setting the AutoSize property to True causes a picture box to resize to the dimensions of the picture; setting it to False causes the picture to be cropped (only a portion of the picture is visible). When set to False (the default), the Stretch property of the image control causes it to resize to the dimensions of the picture. Setting the Stretch property to True causes the picture to resize to the size of the image control, which may cause the picture to appear distorted.

For More Information For additional information on the image control, see "Using Visual Basic's Standard Controls."

Using an Image Control to Create Your Own Buttons

An image control also recognizes the Click event, so you can use this control anywhere you'd use a command button. This is a convenient way to create a button with a picture instead of a caption. Grouping several image controls together horizontally across the top of the screen — usually within a picture box — allows you to create a toolbar in your application.

For instance, the Test Buttons example shows an image control that users can choose like they choose a command button. When the form is first displayed, the control displays one of three traffic icons from the Icon Library included with Visual Basic. Each time the image control is clicked, a different icon is displayed. (For a working version of this example, see Button.frm in the Controls.vbp sample application.)

If you inspect the form at design time, you will see that it actually contains all three icons "stacked" on top of each other. By changing the Visible property of the top image control to False, you allow the next image (with its Visible property set to True) to appear on top.

Figure 3.17 shows the image control with one of the traffic icons (Trffc10a.ico).

Figure 3.17 Image control with a traffic icon

To create a border around the image control, set the BorderStyle property to 1-Fixed Single.

Note Unlike command buttons, image controls do not appear pushed in when clicked. This means that unless you change the bitmap in the MouseDown event, there is no visual cue to the user that the "button" is being pushed.

For More Information For information on displaying a graphic image in an image control, see "Using Visual Basic's Standard Controls."

Using Shape and Line Controls

Shape and line controls are useful for drawing graphical elements on the surface of a form. These controls don't support any events; they are strictly for decorative purposes.

Several properties are provided to control the appearance of the shape control. By setting the Shape property, it can be displayed as a rectangle, square, oval, circle, rounded rectangle, or rounded square. The BorderColor and FillColor properties can be set to change the color; the BorderStyle, BorderWidth, FillStyle, and DrawMode properties control how the shape is drawn.

The line control is similar to the shape control but can only be used to draw straight lines.

For More Information For additional information on the shape and line controls, see "Using Visual Basic's Standard Controls."


The Images Application

The form shown in Figure 3.18 uses four image controls, a shape control, a picture box, and a command button. When the user selects a playing card symbol, the shape control highlights the symbol and a description is displayed in the picture box. For a working version of this example, see Images.frm in the Controls.vbp sample application.

Figure 3.18 Image and shape control example

The following table lists the property settings for the objects in the application.

Object | Property | Setting
Picture box | Name Align | picStatus Bottom
First image control | Name Picture | imgClub Spade.ico
Second image control | Name Picture | imgDiamond Diamond.ico
Third image control | Name Picture | imgHeart Heart.ico
Fourth image control | Name Caption | imgSpade Spade.ico
Shape control | Name Shape BorderWidth Height Width | shpCard 4 - Rounded Rectangle 2 735 495
Command button | Name Caption | cmdClose &Close

Events in the Images Application

The Images application responds to events as follows:

  • The Click event in each of the image controls sets the Left property of the shape control equal to its own Left property, moving the shape on top of the image.

  • The Cls method of the picture box is invoked, clearing the current caption from the status bar.

  • The Print method of the picture box is invoked, printing the new caption on the status bar.

The code in the image control Click event looks like this:

Private Sub imgHeart_Click()
   shpCard.Left = imgClub.Left
   picStatus.Cls
   picStatus.Print "Selected: Club"
   shpCard.Visible = True
End Sub

Note that the first line in the Click event code assigns a value (the Left property of the image control) to the Left property of the shape control using the = operator. The next two lines invoke methods, so no operator is needed. In the third line, the value ("Selected: Club") is an argument to the Print method.

There is one more line of code in the application that is of interest; it is in the Form Load event.

shpCard.Visible = False

By setting the Visible property of the shape control to False, the shape control is hidden until the first image is clicked. The Visible property is set to True as the last step in the image control Click event.

For More Information For additional information on properties, methods, and events see "Programming Fundamentals."


Additional Controls

Several other standard controls are included in the Visual Basic toolbox. Some controls are useful for working with large amounts of data contained in an external database. Other controls can be used to access the Windows file system. Still other controls defy categorization, but are useful nonetheless.

You can also use ActiveX controls, previously called custom or OLE controls, in a Visual Basic application in the same way that you use the standard controls. The Professional and Enterprise editions of Visual Basic include several ActiveX controls as well as the capability to build your own controls. Additional ActiveX controls for just about any purpose imaginable are available for purchase from numerous vendors.

For More Information For additional information on using ActiveX controls, see "Managing Projects."

Data Access Controls

In today's business, most information is stored in one or more central databases. Visual Basic includes several data access controls for accessing most popular databases, including Microsoft Access and SQL Server.

  • The ADO Data control is used to connect to a database. Think of it as a pipeline between the database and the other controls on your form. Its properties, methods, and events allow you to navigate and manipulate external data from within your own application.

  • The DataList control is similar to the list box control. When used in conjunction with an ADO Data control, it can be automatically filled with a list of data from a field in an external database.

  • The DataCombo control is like a combination of the DataList control and a text box. The selected text in the text box portion can be edited, with the changes appearing in the underlying database.

  • The DataGrid control displays data in a grid or table. When used in conjunction with an ADO Data control, it presents fully editable data from multiple fields in an external database.

  • The Microsoft Hierarchical FlexGrid control is a unique control for presenting multiple views of data. Think of it as a combination of a grid and a tree or outline control. At run time, the user can rearrange columns and rows to provide different views of the data.

For More Information For additional information on data access controls, see "Using Visual Basic's Standard Controls." For more information on working with external data, see the Visual Basic Data Access Guide.

File System Controls

Visual Basic includes three controls for adding file handling capabilities to your application. These controls are normally used together to provide a view of drives, directories and files; they have special properties and events that tie them together.

  • The DriveListBox control looks like a combo box. It provides a drop-down list of drives from which the user can select.

  • The DirListBox is similar to a list box control, but with the built-in capability of displaying a list of directories in the currently selected drive.

  • The FileListBox control also looks like a list box with a list of file names in a selected directory.

Note These controls are provided primarily for backward compatibility with applications created in earlier versions of Visual Basic. The common dialog control provides an easier method of working with file access. For more information on common dialog control, see "Miscellaneous Controls" later in this chapter.

Miscellaneous Controls

Several other standard controls are included in Visual Basic. Each serves a unique purpose.

  • The timer control can be used to create an event in your application at a recurring interval. This is useful for executing code without the need for user interaction.

  • The OLE container control is an easy way to add capabilities like linking and embedding to your application. Through the OLE container control, you can provide access to the functionality of any OLE-enabled application such as Microsoft Excel, Word and many others.

  • The common dialog control adds built-in dialog boxes to your application for the selection of files, colors, fonts, and printing functions.

For More Information For additional information on any of the standard controls, see "Using Visual Basic's Standard Controls."


Understanding Focus

Focus is the ability to receive user input through the mouse or keyboard. When an object has the focus, it can receive input from a user. In the Microsoft Windows interface, several applications can be running at any time, but only the application with the focus will have an active title bar and can receive user input. On a Visual Basic form with several text boxes, only the text box with the focus will display text entered by means of the keyboard.

The GotFocus and LostFocus events occur when an object receives or loses focus. Forms and most controls support these events.

Event | Description
GotFocus | Occurs when an object receives focus.
LostFocus | Occurs when an object loses focus. A LostFocus event procedure is primarily used for verification and validation updates, or for reversing or changing conditions you set up in the object's GotFocus procedure.

You can give focus to an object by:

  • Selecting the object at run time.

  • Using an access key to select the object at run time.

  • Using the SetFocus method in code.

You can see when some objects have the focus. For example, when command buttons have the focus, they appear with a highlighted border around the caption (see Figure 3.19).

Figure 3.19 A command button showing focus

An object can receive focus only if its Enabled and Visible properties are set to True. The Enabled property allows the object to respond to user-generated events such as keyboard and mouse events. The Visible property determines whether an object is visible on the screen.

Note A form can receive focus only if it doesn't contain any controls that can receive the focus.

Validate Event of Controls

Controls also have a Validate event, which occurs before a control loses focus. However, this event occurs only when the CausesValidation property of the control that is about to receive the focus is set to True. In many cases, because the Validate event occurs before the focus is lost, it is more appropriate than the LostFocus event for data validation. For more information, see "Validating Control Data By Restricting Focus" in "Using Visual Basic's Standard

Controls That Can't Receive Focus

Some controls, such as the lightweight controls, cannot receive focus. Lightweight controls include the following:

  • Frame control

  • Image control

  • Label control

  • Line control

  • Shape control

Additionally, controls that are invisible at run time, such as the Timer control, cannot receive focus.


Setting the Tab Order

The tab order is the order in which a user moves from one control to another by pressing the TAB key. Each form has its own tab order. Usually, the tab order is the same as the order in which you created the controls.

For example, assume you create two text boxes, Text1 and Text2, and then a command button, Command1. When the application starts, Text1 has the focus. Pressing TAB moves the focus between controls in the order they were created, as shown in Figure 3.20.

Figure 3.20 Tab example

To change the tab order for a control, set the TabIndex property. The TabIndex property of a control determines where it is positioned in the tab order. By default, the first control drawn has a TabIndex value of 0, the second has a TabIndex of 1, and so on. When you change a control's tab order position, Visual Basic automatically renumbers the tab order positions of the other controls to reflect insertions and deletions. For example, if you make Command1 first in the tab order, the TabIndex values for the other controls are automatically adjusted upward, as shown in the following table.

Control | TabIndex before it is changed | TabIndex after it is changed
Text1 | 0 | 1
Text2 | 1 | 2
Command1 | 2 | 0

The highest TabIndex setting is always one less than the number of controls in the tab order (because numbering starts at 0). Even if you set the TabIndex property to a number higher than the number of controls, Visual Basic converts the value back to the number of controls minus 1.

Note Controls that cannot get the focus, as well as disabled and invisible controls, don't have a TabIndex property and are not included in the tab order. As a user presses the TAB key, these controls are skipped.

Removing a Control from the Tab Order

Usually, pressing TAB at run time selects each control in the tab order. You can remove a control from the tab order by setting its TabStop property to False (0).

A control whose TabStop property has been set to False still maintains its position in the actual tab order, even though the control is skipped when you cycle through the controls with the TAB key.

Note An option button group has a single tab stop. The selected button (that is, the button with its Value set to True) has its TabStop property automatically set to True, while the other buttons have their TabStop property set to False.


Menu Basics

If you want your application to provide a set of commands to users, menus offer a convenient and consistent way to group commands and an easy way for users to access them.

Figure 3.21 Illustrates the elements of a menu interface on an untitled form.

Figure 3.21 The elements of a menu interface on a Visual Basic form

The menu bar appears immediately below the title bar on the form and contains one or more menu titles. When you click a menu title (such as File), a menu containing a list of menu items drops down. Menu items can include commands (such as New and Exit), separator bars, and submenu titles. Each menu item the user sees corresponds to a menu control you define in the Menu Editor (described later in this chapter).

To make your application easier to use, you should group menu items according to their function. In Figure 3.21, for example, the file-related commands New, Open, and Save As… are all found on the File menu.

Some menu items perform an action directly; for example, the Exit menu item on the File menu closes the application. Other menu items display a dialog box — a window that requires the user to supply information needed by the application to perform the action. These menu items should be followed by an ellipsis (…). For example, when you choose Save As… from the File menu, the Save File As dialog box appears.

A menu control is an object; like other objects it has properties that can be used to define its appearance and behavior. You can set the Caption property, the Enabled and Visible properties, the Checked property, and others at design time or at run time. Menu controls contain only one event, the Click event, which is invoked when the menu control is selected with the mouse or using the keyboard.

For More Information For additional information on menu controls, see "Creating Menus with the Menu Editor" in "Creating a User Interface."

Pop-up Menus

A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar, as shown in Figure 3.22. The items displayed on the pop-up menu depend on the location of the pointer when the right mouse button is pressed; therefore, pop-up menus are also called context menus. (In Windows 95, you activate context menus by clicking the right mouse button.) You should use pop-up menus to provide an efficient method for accessing common, contextual commands. For example, if you click a text box with the right mouse button, a contextual menu would appear, as shown in Figure 3.22.

Figure 3.22 A pop-up menu

Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method.

For More Information For additional information on creating pop-up menus, see "Creating Menus" in "Creating a User Interface."

Using the Menu Editor

With the Menu Editor, you can add new commands to existing menus, replace existing menu commands with your own commands, create new menus and menu bars, and change and delete existing menus and menu bars. The main advantage of the Menu Editor is its ease of use. You can customize menus in a completely interactive manner that involves very little programming.

To display the Menu Editor

  • From the Tools menu, choose Menu Editor.

This opens the Menu Editor, shown in Figure 3.23

Figure 3.23 The Menu Editor

While most menu control properties can be set using the Menu Editor; all menu properties are also available in the Properties window. You would normally create a menu in the Menu Editor; however, to quickly change a single property, you could use the Properties window.

For More Information For additional information on creating menus and using the Menu Editor, see "Creating Menus" in "Creating a User Interface."


Prompting the User with Dialog Boxes

In Windows-based applications, dialog boxes are used to prompt the user for data needed by the application to continue or to display information to the user. Dialog boxes are a specialized type of form object that can be created in one of three ways:

  • Predefined dialog boxes can be created from code using the MsgBox or InputBox functions.

  • Customized dialog boxes can be created using a standard form or by customizing an existing dialog box.

  • Standard dialog boxes, such as Print and File Open, can be created using the common dialog control.

Figure 3.24 shows an example of a predefined dialog box created using the MsgBox function.

Figure 3.24 A predefined dialog box

This dialog is displayed when you invoke the MsgBox function in code. The code for displaying the dialog box shown in Figure 3.24 looks like this:

MsgBox "Error encountered while trying to open file," & vbCrLf & "please retry.", vbExclamation, "Text Editor"

You supply three pieces of information, or arguments, to the MsgBox function: the message text, a constant (numeric value) to determine the style of the dialog box, and a title. Styles are available with various combinations of buttons and icons to make creating dialog boxes easy.

Because most dialog boxes require user interaction, they are usually displayed as modal dialog boxes. A modal dialog box must be closed (hidden or unloaded) before you can continue working with the rest of the application. For example, a dialog box is modal if it requires you to click OK or Cancel before you can switch to another form or dialog box.

Modeless dialog boxes let you shift the focus between the dialog box and another form without having to close the dialog box. You can continue to work elsewhere in the current application while the dialog box is displayed. Modeless dialog boxes are rare; you will usually display a dialog because a response is needed before the application can continue. From the Edit menu, the Find dialog box in Visual Basic is an example of a modeless dialog box. Use modeless dialog boxes to display frequently used commands or information.

For More Information For additional information on creating dialog boxes, see "Creating a User Interface."


Managing Projects

To create an application with Visual Basic, you work with projects. A project is the collection of files you use to build an application. This chapter describes how to build and manage projects.

When you create an application, you will usually create new forms; you might also reuse or modify forms that were created for previous projects. The same is true for other modules or files that you might include in your project. ActiveX controls and objects from other applications can also be shared between projects.

After all of the components in a project have been assembled and the code written, you compile your project to create an executable file.

Topics

Working with Projects

An introduction to projects, the Project Explorer, and project files.
The Structure of a Visual Basic Project

A discussion of the components that make up a project.
Creating, Opening, and Saving Projects

The basics of working with projects.
Adding, Removing, and Saving Files

The basics of working with the files within a project.
Adding Controls to a Project

The basics of working with ActiveX controls and insertable objects.
Making and Running an Executable File

The basics of compiling a project.
Setting Project Options

A discussion of some of the options that can be set on a project by project basis.
Using Wizards and Add-Ins

A discussion of additional tools for working with projects and extending Visual Basic.


Working with Projects

As you develop an application, you work with a project to manage all the different files that make up the application. A project consists of:

  • One project file that keeps track of all the components (.vbp).

  • One file for each form (.frm).

  • One binary data file for each form containing data for properties of controls on the form (.frx). These files are not editable and are automatically generated for any .frm file that contains binary properties, such as Picture or Icon.

  • Optionally, one file for each class module (.cls).

  • Optionally, one file for each standard module (.bas).

  • Optionally, one or more files containing ActiveX controls (.ocx).

  • Optionally, a single resource file (.res).

The project file is simply a list of all the files and objects associated with the project, as well as information on the environment options you set. This information is updated every time you save the project. All of the files and objects can be shared by other projects as well.

When you have completed all the files for a project, you can convert the project into an executable file (.exe): From the File menu, choose the Make project.exe command.

Note With the Professional and Enterprise editions of Visual Basic, you can also create other types of executable files such as .ocx and .dll files. References in this chapter assume a standard .exe project; for additional information related to other project types see the Component Tools Guide, available in the Professional and Enterprise editions.

For More Information For more details about creating executables, see "Making and Running an Executable File," later in this chapter. For information about binary data files and project files, see "Visual Basic Specifications, Limitations, and File Formats."

The Project Explorer

As you create, add, or remove editable files from a project, Visual Basic reflects your changes in the Project Explorer window, which contains a current list of the files in the project. The Project Explorer window in Figure 4.1 shows some of the types of files you can include in a Visual Basic project.

Figure 4.1 The Project Explorer window

The Project File

Each time you save a project, Visual Basic updates the project file (.vbp). A project file contains the same list of files that appears in the Project Explorer window, as well as references to the ActiveX controls and insertable objects that are used in the project.

You can open an existing project file by double-clicking its icon, by choosing the Open Project command from the File menu, or by dragging the file and dropping it on the Project Explorer window.

For More Information The specific format of information stored in the .vbp file is described in "Visual Basic Specifications, Limitations, and File Formats."


The Structure of a Visual Basic Project

The following sections describe the different types of files and objects that you can include in a project.

Form Modules

Form modules (.frm file name extension) can contain textual descriptions of the form and its controls, including their property settings. They can also contain form-level declarations of constants, variables, and external procedures; event procedures; and general procedures.

For More Information For more about creating forms, see "Developing an Application in Visual Basic" and "Creating a User Interface." For information about the format and content of form files, see "Visual Basic Specifications, Limitations, and File Formats."

Class Modules

Class modules (.cls file name extension) are similar to form modules, except that they have no visible user interface. You can use class modules to create your own objects, including code for methods and properties.

For More Information For information about writing code in class modules, see "Creating Your Own Classes" in "Programming with Objects."

Standard Modules

Standard modules (.bas file name extension) can contain public or module-level declarations of types, constants, variables, external procedures, and public procedures.

For More Information For information about using modules, see "Programming Fundamentals" and "Programming with Objects."

Resource Files

Resource files (.res file name extension) contain bitmaps, text strings, and other data that you can change without having to re-edit your code. For example, if you plan to localize your application in a foreign language, you can keep all of the user-interface text strings and bitmaps in a resource file, which you can then localize instead of the entire application. A project can contain no more than one resource file.

For More Information For more information on using resource files, see "Using a Resource File" later in this chapter, and "International Issues."

ActiveX Documents

ActiveX documents (.dob) are similar to forms, but are displayable in an Internet browser such as Internet Explorer. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX documents.

For More Information For more information on ActiveX documents, see "Creating ActiveX Components" in the Component Tools Guide, available in the Professional and Enterprise editions.

User Control and Property Page Modules

User Control (.ctl) and Property Page (.pag) modules are also similar to forms, but are used to create ActiveX controls and their associated property pages for displaying design-time properties. The Professional and Enterprise editions of Visual Basic are capable of creating ActiveX controls.

For More Information For more information on ActiveX control creation, see "Creating an ActiveX Control" in "Creating ActiveX Components," in the Component Tools Guide, available in the Professional and Enterprise editions.

Components

In addition to files and modules, several other types of components can be added to the project.

ActiveX Controls

ActiveX controls (.ocx file name extension) are optional controls which can be added to the toolbox and used on forms. When you install Visual Basic, the files containing the controls included with Visual Basic are copied to a common directory (the \Windows\System subdirectory under Windows 95). Additional ActiveX controls are available from a wide variety of sources. You can also create your own controls using the Professional or Enterprise editions of Visual Basic.

For More Information For more information on using the included ActiveX controls, see "Using the ActiveX Controls" in the Component Tools Guide, available in the Professional and Enterprise editions.

Insertable Objects

Insertable objects, such as a Microsoft Excel Worksheet object, are components you can use as building blocks to build integrated solutions. An integrated solution can contain data in different formats, such as spreadsheets, bitmaps, and text, which were all created by different applications.

For More Information For more information on using other applications' objects, see "Programming with Components."

References

You can also add references to external ActiveX components that may be used by your application. You assign references by using the References dialog, accessed from the References menu item on the Project menu.

For More Information For more information on references, see "Using Other Applications' Objects" later in this chapter.

ActiveX Designers

ActiveX designers are tools for designing classes from which objects can be created. The design interface for forms is the default designer. Additional designers may be available from other sources.

For More Information For more information about ActiveX designers, see "ActiveX Designers" in "Programming with Objects."

Standard Controls

Standard controls are supplied by Visual Basic. Standard controls, such as the command button or frame control, are always included in the toolbox, unlike ActiveX controls and insertable objects, which can be removed from or added to the toolbox.

For More Information For more information on standard controls, see "Forms, Controls, and Menus" and "Using Visual Basic's Standard Controls."


Creating, Opening, and Saving Projects

Four commands on the File menu allow you to create, open, and save projects.

Menu command | Description
New Project | Closes the current project, prompting you to save any files that have changed. You can select a type of project from the New Project dialog. Visual Basic then creates a new project with a single new file.
Open Project | Closes the current project, prompting you to save any changes. Visual Basic then opens an existing project, including the forms, modules, and ActiveX controls listed in its project (.vbp) file.
Save Project | Updates the project file of the current project and all of its form, standard, and class modules.
Save Project As | Updates the project file of the current project, saving the project file under a file name that you specify. Visual Basic also prompts you to save any forms or modules that have changed.

It is also possible to share files between projects. A single file, such as a form, can be part of more than one project. Note that changes made to a form or module in one project will be propagated amongst all projects that share that module.

For More Information For more information about sharing files, see "Adding, Removing, and Saving Files" later in this chapter.

Working with Multiple Projects

In the Professional and Enterprise editions of Visual Basic, it is possible to have more than one project open at a time. This is useful for building and testing solutions involving user-created controls or other components. When more than one project is loaded, the caption of the Project Explorer window will change to Project Group and the components of all open projects will be displayed.

To add an additional project to the current project group

  1. From the File menu, choose Add Project.

    The Add Project dialog box is displayed.

  2. Select an existing project or a new project type, and choose Open.

To remove a project from the current project group

  1. Select a project or a component of a project in the Project Explorer.

  2. From the File menu, choose Remove Project.

For More Information To learn more about working with multiple projects, see "Creating ActiveX Components" in the Component Tools Guide, available in the Professional and Enterprise editions.


Adding, Removing, and Saving Files

Working with files within a project is similar to working with the projects themselves.

To add a file to a project

  1. Select Project, Add filetype (where filetype is the type of file).

    The Add filetype dialog box (Figure 4.2) is displayed.

  2. Select an existing file or a new file type, and choose Open.

Figure 4.2 The Add Form dialog box

When you add a file to a project, you are simply including a reference to the existing file in the project; you are not adding a copy of the file. Therefore, if you make changes to a file and save it, your changes will affect any project that includes the file. To change a file without affecting other projects, select the file in the Project Explorer, choose Save filename As from the File menu, and then save the file under a new file name.

Note You can drag and drop files from the Windows Explorer, File Manager, or Network Neighborhood into the Project window to add them to a project. You can also drag and drop .ocx files onto the toolbox to add new controls.

To remove a file from a project

  1. Select the file in the Project Explorer.

  2. From the Project menu, choose Remove filename.

  3. The file will be removed from the project but not from the disk.

If you remove a file from a project, Visual Basic updates this information in the project file when you save it. If you delete a file outside of Visual Basic, however, Visual Basic cannot update the project file; therefore, when you open the project, Visual Basic displays an error message warning you that a file is missing.

To save an individual file without saving the project

  1. Select the file in the Project Explorer.

  2. From the File menu, choose Save filename .

Merging Text

You can also insert existing text from other files into one of your code modules. This is useful for adding a list of constants or for adding snippets of code that you might have saved in text files.

To insert a text file into your code

  1. From the Project window, select the form or module into which you want to insert code.

  2. Choose the View Code button, and move the cursor to the point in the Code Editor where you want to insert code.

  3. From the Edit menu, choose Insert File.

  4. Select the name of the text file you want to insert, and choose Open.

Note If you edit Visual Basic source files using a text or code editor other than Visual Basic, be careful not to change settings of the attribute VB_PredeclaredId. In particular, changing this attribute may cause serious problems with the GlobalMultiUse and GlobalSingleUse classes.

In general, you should not edit attributes manually, as doing so may put the module into an internally inconsistent state.


Adding Controls to a Project

The set of controls available in the toolbox can be customized for each project. Any given control must be in the toolbox before you can add it to a form in the project. The basic set of standard controls that always appear in the toolbox is described in "Forms, Controls, and Menus."

Adding ActiveX Controls to a Project

You can add ActiveX controls and insertable objects to your project by adding them to the toolbox.

To add a control to a project's toolbox

  1. From the Project menu, choose Components.

    The Components dialog box is displayed, as shown in Figure 4.3. The items listed in this dialog box include all registered ActiveX controls, insertable objects, and ActiveX designers.

  2. To add a control (.ocx file name extension) or an insertable object to the toolbox, select the check box to the left of the control name.

    To view controls with .ocx file name extensions, select the Controls tab. To view insertable objects, such as a Microsoft Excel Chart, select the Insertable Objects tab.

  3. Choose OK to close the Components dialog box. All of the ActiveX controls that you selected will now appear in the toolbox.

Figure 4.3 The Components dialog box

To add ActiveX controls to the Components dialog box, choose the Browse button, and search other directories for files with a .ocx file name extension. When you add an ActiveX control to the list of available controls, Visual Basic automatically selects the check box.

Note Each ActiveX control is accompanied by a file with an .oca extension. This file stores cached type library information and other data specific to the control. The .oca files are typically stored in the same directory as the ActiveX controls and are recreated as needed (file sizes and dates may change).

Removing Controls from a Project

To remove a control from a project

  1. From the Project menu, choose Components.

    The Components dialog box is displayed.

  2. Clear the check box next to each control you want to remove.

    The control icons will be removed from the toolbox.

Note You cannot remove any control from the toolbox if an instance of that control is used on any form in the project.

Using Other Applications' Objects

You can also use objects from other applications, such as those included in the Microsoft Excel object library, either as controls in the toolbox or as programmable objects in your code. To add objects to the toolbox, see "Adding Controls to a Project" earlier in this chapter.

To make another application's objects available in your code, but not as controls, set a reference to that application's object library.

To add a reference to another application's object library

  1. From the Project menu, choose References.

    The References dialog box is displayed, as shown in Figure 4.4.

  2. Select the check box next to each reference you want to add to your project.

    To add references to applications not listed in the References dialog box, choose the Browse button, and then select the application.

  3. Choose OK to add the selected references to your project.

Figure 4.4 The References dialog box

If you are not using any objects in a referenced library, you should clear the check box for that reference to minimize the number of object references Visual Basic must resolve, thus reducing the time it takes your project to compile.

Once you have set references to the object libraries you want, you can find a specific object and its methods and properties in the Object Browser by choosing Object Browser from the View menu. You can use any object listed in the Object Browser in your code.

For More Information " For information on the Object Browser, see "Finding Out About Objects" in "Programming with Objects."

Using a Resource File

A resource file allows you to collect all of the version-specific text and bitmaps for an application in one place. This can include constant declarations, icons, screen text, and other material that may change between localized versions or between revisions or specific configurations.

To add a file to a project

  1. From the Project menu, select Add File.

    The Add File dialog box is displayed.

  2. Select an existing resource file (.res) and choose Open.

A single project can have only one resource file; if you add a second file with a .res extension, an error occurs.

For More Information For more information on the contents of a resource file, see "International Issues."


Making and Running an Executable File

You can make an executable file (.exe) from Visual Basic using the following procedure.

To make an executable file in Visual Basic

  1. From the File menu, choose Make projectname .exe where projectname is the application name for the project.

  2. Type a file name, or browse through the directories and select an existing file name to overwrite an existing executable with a newer version.

  3. By clicking the Options button, you can also specify a number of version-specific details about the executable file in the Project Properties dialog box.

  4. If you want to modify the version number of the project, set the appropriate Major, Minor, and Revision numbers. Selecting Auto Increment will automatically step the Revision number each time you run the Make projectname .exe command for this project.

  5. To specify a new name for the application, under Application, type a new name in the Title box. If you want to specify a new icon, choose one from the list.

  6. You can also enter version-specific commentary on a variety of issues under the Version Information box (comments, company name, trademark and copyright information, and so on) by selecting a topic from the list box and entering information in the text box.

  7. Choose OK to close the Project Properties dialog box, and then choose OK in the Make appname .exe dialog box to compile and link the executable file.

You can run the executable file like any other Windows-based application: double-click the icon for the executable file.

Note Building an executable file from the command line in a DOS session can be useful when you want to compile a project programmatically. In a batch file, type:

Vb6 /make projectname[.vbp] [exename]

For projectname, type the name of the project file. Use the variable exename to rename the resulting executable file.

Conditional Compilation

Conditional compilation lets you selectively compile certain parts of the program. You can include specific features of your program in different versions, such as changing the date and currency display filters for an application distributed in several different languages.

For More Information To learn more about conditional compilation, see "Using Conditional Compilation" in "More About Programming."


Setting Project Options

Visual Basic allows you to customize each project by setting a number of properties. Use the Project Properties dialog box, accessible through the Project Properties command on the Project menu. Property settings are saved to the project (.vbp) file.

The following table describes some of the options you can set.

Option | Description
Startup Object | The first form that Visual Basic displays at run time, or Sub Main ( ).
Project Name | Identifies the project in code. It can't contain periods (.), spaces, or start with a nonalphabetic character. For a public class name, the project name and class name cannot exceed a total of 37 characters.
Help File | The name of the Help file associated with the project.
Project Help Context ID | The context ID for the specific Help topic to be called when the user selects the "?" button while the application's object library is selected in the Object Browser.
Project Description | A user-friendly name for the project. Displayed in the References and Object Browser dialog boxes.

Many other options are also available, including those for compiling, components, and multithreading. When you're ready to access some of the more advanced options, you can find more information by searching Help.

For More Information To learn about setting environment options that affect all projects, see "Developing An Application in Visual Basic."


Using Wizards and Add-Ins

Visual Basic allows you to select and manage add-ins, which are extensions to Visual Basic. These extensions add capabilities to the Visual Basic development environment, for example, special source code control capabilities.

Microsoft and other developers have created add-ins you can use in your applications. Wizards are a type of add-in that can simplify certain tasks, such as creating a form. Several wizards are included in Visual Basic.

To have an add-in appear on the Add-In Manager dialog box, the developer of the add-in must ensure that it is installed properly.

Using the Add-In Manager

You can add or remove an add-in to your project by using the Add-In Manager, which is accessible from the Add-Ins menu. The Add-In Manager dialog box lists the available add-ins.

To install an add-in

  1. From the Add-Ins menu, choose Add-In Manager.

  2. Highlight an add-in from the list and click the desired behaviors in Load Behavior. To unload an add-in or prevent it from loading, clear all Load Behavior boxes.

  3. When you are finished making your selections, choose OK.

    Depending upon your Load Behavior selections, Visual Basic connects the selected add-ins and disconnects the cleared add-ins.

Visual Basic saves your add-in selections between editing sessions.

Note Selecting an add-in may add menu items to the Visual Basic Add-Ins menu.

Using Wizards

Wizards make working with Visual Basic even easier by providing task-specific assistance. For example, the Application Wizard included in Visual Basic helps you to create the framework for an application by presenting a series of questions or choices. It generates the forms and the code behind the forms based on your choices; all you need to do is add code for your own specific functionality.

The Professional and Enterprise editions of Visual Basic include other wizards, including a Data Form Wizard for creating forms to be used with databases, and an ActiveX Document Wizard for converting forms for use in Internet applications.

Wizards are installed or removed using the Add-in Manager. Once installed, they will appear as selections on the Add-Ins menu. Some of the wizards also appear as icons in the related dialog boxes; for example, the Application Wizard can also be accessed using its icon in the New Project dialog box.

To start the Application Wizard

  • From the Add-Ins menu, choose Application Wizard.

    –or–

  1. From the File menu, choose New Project.

  2. Select the Application Wizard icon.


Programming Fundamentals

This chapter introduces the essential components of the Visual Basic language. After creating the interface for your application using forms and controls, you will need to write the code that defines the application's behavior. As with any modern programming language, Visual Basic supports a number of common programming constructs and language elements.

Visual Basic is an object-based programming language. The mere mention of objects may cause undue anxiety in many programmers. Don't worry: whether you realize it or not, you've been dealing with objects most of your life. Once you understand a few basic concepts, objects actually help to make programming easier than ever before.

If you've programmed in other languages, much of the material covered in this chapter will seem familiar. While most of the constructs are similar to other languages, the event-driven nature of Visual Basic introduces some subtle differences. Try and approach this material with an open mind; once you understand the differences you can use them to your advantage.

If you're new to programming, the material in this chapter will serve as an introduction to the basic building blocks for writing code. Once you understand the basics, you will be able to create powerful applications using Visual Basic.

Topics

The Structure of a Visual Basic Application

An introduction to the various components or modules that make up a Visual Basic application.
Before You Start Coding

A brief discussion of some of the considerations in designing an application.
Code Writing Mechanics

An introduction to the features of the Code Editor, plus the rules and regulations for writing code.
Introduction to Variables, Constants and Data Types

An introduction to the basic elements of the Visual Basic language.
Introduction to Procedures

An introduction to Sub and Function procedures.
Introduction to Control Structures

An introduction to decision structures and loops.

Working with Objects

An introduction to using objects in Visual Basic code.

Sample application

Vcr.vbp

Many of the code samples in this chapter are taken from the Vcr.vbp sample application which is listed in the Samples directory.


The Structure of a Visual Basic Application

An application is really nothing more than a set of instructions directing the computer to perform a task or tasks. The structure of an application is the way in which the instructions are organized; that is, where the instructions are stored and the order in which instructions are executed.

Simple applications such as the classic "hello world" example have a simple structure; organization isn't very important with a single line of code. As applications become more complex, the need for organization or structure becomes obvious. Imagine the chaos that would result if your application's code was allowed to execute in random order. In addition to controlling the execution of an application, the structure is important to the programmer: how easily can you find specific instructions within your application?

Because a Visual Basic application is based on objects, the structure of its code closely models its physical representation on screen. By definition, objects contain data and code. The form that you see on screen is a representation of the properties that define its appearance and intrinsic behavior. For each form in an application, there is a related form module (with file name extension .frm) that contains its code.

Figure 5.1 A form and its related form module

Each form module contains event procedures — sections of code where you place the instructions that will execute in response to specific events. Forms can contain controls. For each control on a form, there is a corresponding set of event procedures in the form module. In addition to event procedures, form modules can contain general procedures that are executed in response to a call from any event procedure.

Code that isn't related to a specific form or control can be placed in a different type of module, a standard module (.BAS). A procedure that might be used in response to events in several different objects should be placed in a standard module, rather than duplicating the code in the event procedures for each object.

A class module (.CLS) is used to create objects that can be called from procedures within your application. Whereas a standard module contains only code, a class module contains both code and data — you can think of it as a control without a physical representation.

While "Managing Projects" describes which components you can add to an application, this chapter explains how to write code in the various components that make up an application. By default, your project contains a single form module. You can add additional form, class, and standard modules, as needed. Class modules are discussed in "Programming with Objects."

  • How an Event-Driven Application Works A discussion of the Event-driven model.


How an Event-Driven Application Works

An event is an action recognized by a form or control. Event-driven applications execute Basic code in response to an event. Each form and control in Visual Basic has a predefined set of events. If one of these events occurs and there is code in the associated event procedure, Visual Basic invokes that code.

Although objects in Visual Basic automatically recognize a predefined set of events, it is up to you to decide if and how they will respond to a particular event. A section of code — an event procedure — corresponds to each event. When you want a control to respond to an event, you write code in the event procedure for that event.

The types of events recognized by an object vary, but many types are common to most controls. For example, most objects recognize a Click event — if a user clicks a form, code in the form's Click event procedure is executed; if a user clicks a command button, code in the button's Click event procedure is executed. The actual code in each case will most likely be quite different.

Here's a typical sequence of events in an event-driven application:

  1. The application starts and a form is loaded and displayed.

  2. The form (or a control on the form) receives an event. The event might be caused by the user (for example, a keystroke), by the system (for example, a timer event), or indirectly by your code (for example, a Load event when your code loads a form).

  3. If there is code in the corresponding event procedure, it executes.

  4. The application waits for the next event.

Note Many events occur in conjunction with other events. For example, when the DblClick event occurs, the MouseDown, MouseUp, and Click events also occur.


Before You Start Coding

Perhaps the most important (and often overlooked) part of creating an application in Visual Basic is the design phase. While it's obvious that you need to design a user interface for your application, it may not be as obvious that you need to design the structure of the code. The way you structure your application can make a difference in its performance as well as in the maintainability and usability of your code.

The code in a Visual Basic application is organized in a hierarchical fashion. A typical application consists of one or more modules: a form module for each form in the application, optional standard modules for shared code, and optional class modules. Each module contains one or more procedures that contain the code: event procedures, Sub or Function procedures, and Property procedures.

Determining which procedures belong in which module depends somewhat on the type of application that you are creating. Because Visual Basic is based on objects, it helps to think of your application in terms of the objects that it represents. The design of the sample application for this chapter, Vcr.vbp, is based on the objects that comprise a video cassette recorder and a television. The VCR application consists of two form modules, a standard module, and two class modules. You can use the Object Browser to examine the structure of the project (Figure 5.2).

Figure 5.2 The structure of the VCR project is shown in the Object Browser

The main form for the VCR application (frmVCR) is a visual representation of a combination VCR and television screen (Figure 5.3). It is composed of several objects that model those found in the real world version. A group of Command buttons (cmdPlay, cmdRecord, and so on) mimic the buttons used to operate a VCR. The software VCR also contains a clock (lblTime), a channel indicator (lblChannel), function indicators (shpPlay, shpRecord, and so on), and a "picture tube" (picTV). The event procedures for all of these objects are contained in the Vcr.frm form module.

Figure 5.3 The main form for the VCR application

In many cases there are repetitive procedures that are shared by more than one object. For example, when the Play, Rewind, or Record buttons are "pushed," the Pause and Stop buttons need to be enabled. Rather than repeat this code in each button's Click event procedure, it's better to create a shared Sub procedure that can be called by any button. If these routines need to be modified in the future, all of the modifications can be done in one place. This and other shared procedures are contained in the standard module, Vcr.bas.

Some parts of a VCR aren't visible, such as the tape transport mechanism or the logic for recording a television program. Likewise, some of the functions of the software VCR have no visual representation. These are implemented as two class modules: Recorder.cls and Tape.cls. Code to initiate the "recording" process is contained in the clsRecorder module; code to control the direction and speed of the "tape" is contained in the clsTape module. The classes defined in these modules have no direct references to any of the objects in the forms. Because they are independent code modules, they could easily be reused to build an audio recorder without any modifications.

In addition to designing the structure of your code, it's important to establish naming conventions. By default, Visual Basic names the first form in a project Form1, the second Form2, and so on. If you have several forms in an application, it's a good idea to give them meaningful names to avoid confusion when writing or editing your code. Some suggested naming conventions are presented in "Visual Basic Coding Conventions."

As you learn more about objects and writing code, you can refer to the VCR sample application for examples of various different coding techniques.


Code Writing Mechanics

Before you begin, it's important to understand the mechanics of writing code in Visual Basic. Like any programming language, Visual Basic has its own rules for organizing, editing, and formatting code.

The following topics introduce code modules and procedures, discuss the basics of using the Code Editor, and cover basic rules for writing code.

  • Code Modules An introduction to the different types of code modules and the procedures that they contain.
  • Using the Code Editor An introduction to working with code using the Code Editor.
  • Code Basics A discussion of the rules of the Visual Basic language.


Code Modules

Code in Visual Basic is stored in modules. There are three kinds of modules: form, standard, and class.

Simple applications can consist of just a single form, and all of the code in the application resides in that form module. As your applications get larger and more sophisticated, you add additional forms. Eventually you might find that there is common code you want to execute in several forms. You don't want to duplicate the code in both forms, so you create a separate module containing a procedure that implements the common code. This separate module should be a standard module. Over time, you can build up a library of modules containing shared procedures.

Each standard, class, and form module can contain:

  • Declarations. You can place constant, type, variable, and dynamic-link library (DLL) procedure declarations at the module level of form, class or standard modules.

  • Procedures. A Sub, Function, or Property procedure contains pieces of code that can be executed as a unit. These are discussed in the section "Procedures" later in this chapter.

Form Modules

Form modules (.FRM file name extension) are the foundation of most Visual Basic applications. They can contain procedures that handle events, general procedures, and form-level declarations of variables, constants, types, and external procedures. If you were to look at a form module in a text editor, you would also see descriptions of the form and its controls, including their property settings. The code that you write in a form module is specific to the particular application to which the form belongs; it might also reference other forms or objects within that application.

Standard Modules

Standard modules (.BAS file name extension) are containers for procedures and declarations commonly accessed by other modules within the application. They can contain global (available to the whole application) or module-level declarations of variables, constants, types, external procedures, and global procedures. The code that you write in a standard module isn't necessarily tied to a particular application; if you're careful not to reference forms or controls by name, a standard module can be reused in many different applications.

Class Modules

Class modules (.CLS file name extension) are the foundation of object-oriented programming in Visual Basic. You can write code in class modules to create new objects. These new objects can include your own customized properties and methods. Actually, forms are just class modules that can have controls placed on them and can display form windows.

For More Information For information about writing code in class modules, see "Programming with Objects."

Note The Professional and Enterprise editions of Visual Basic also include ActiveX Documents, ActiveX Designers, and User Controls. These introduce new types of modules with different file name extensions. From the standpoint of writing code, these modules should be considered the same as form modules.


Using the Code Editor

The Visual Basic Code Editor is a window where you write most of your code. It is like a highly specialized word processor with a number of features that make writing Visual Basic code a lot easier. The Code Editor window is shown in Figure 5.4.

Figure 5.4 The Code Editor window

Because you work with Visual Basic code in modules, a separate Code Editor window is opened for each module you select from the Project Explorer. Code within each module is subdivided into separate sections for each object contained in the module. Switching between sections is accomplished using the Object Listbox. In a form module, the list includes a general section, a section for the form itself, and a section for each control contained on the form. For a class module, the list includes a general section and a class section; for a standard module only a general section is shown.

Each section of code can contain several different procedures, accessed using the Procedure Listbox. The procedure list for a form module contains a separate section for each event procedure for the form or control. For example, the procedure list for a Label control includes sections for the Change, Click, and DblClick events, among others. Class modules list only the event procedures for the class itself — Initialize and Terminate. Standard modules don't list any event procedures, because a standard module doesn't support events.

The procedure list for a general section of a module contains a single selection — the Declarations section, where you place module-level variable, constant, and DLL declarations. As you add Sub or Function procedures to a module, those procedures are added in the Procedure Listbox below the Declarations section.

Two different views of your code are available in the Code Editor window. You can choose to view a single procedure at a time, or to view all of the procedures in the module with each procedure separated from the next by a line (as shown in Figure 5.4). To switch between the two views, use the View Selection buttons in the lower left-hand corner of the editor window.

Automatic Code Completion

Visual Basic makes writing code much easier with features that can automatically fill in statements, properties, and arguments for you. As you enter code, the editor displays lists of appropriate choices, statement or function prototypes, or values. Options for enabling or disabling these and other code settings are available on the Editor tab of the Options dialog, accessed through the Options command on the Tools menu.

When you enter the name of a control in your code, the Auto List Members feature presents a drop-down list of properties available for that control (Figure 5.5). Type in the first few letters of the property name and the name will be selected from the list; the TAB key will complete the typing for you. This option is also helpful when you aren't sure which properties are available for a given control. Even if you choose to disable the Auto List Members feature, you can still access it with the CTRL+J key combination.

Figure 5.5 The Auto List Members feature

The Auto Quick Info feature displays the syntax for statements and functions (Figure 5.6). When you enter the name of a valid Visual Basic statement or function the syntax is shown immediately below the current line, with the first argument in bold. After you enter the first argument value, the second argument appears in bold. Auto Quick Info can also be accessed with the CTRL+I key combination.

Figure 5.6 Auto Quick Info

Bookmarks

Bookmarks can be used to mark lines of code in the Code Editor so that you can easily return to them later. Commands to toggle bookmarks on or off as well as to navigate existing bookmarks are available from the Edit, Bookmarks menu item, or from the Edit toolbar

For More Information For more information on key combinations to access these and other functions in the Code Editor window, see "Code Window Keyboard Shortcuts."


Code Basics

This section presents information on code writing mechanics, including breaking and combining lines of code, adding comments to your code, using numbers in code, and following naming conventions in Visual Basic.

Breaking a Single Statement Into Multiple Lines

You can break a long statement into multiple lines in the Code window using the line-continuation character (a space followed by an underscore). Using this character can make your code easier to read, both online and when printed. The following code is broken into three lines with line-continuation characters ( _):

Data1.RecordSource = _
"SELECT * FROM Titles, Publishers" _
& "WHERE Publishers.PubId = Titles.PubID" _
& "AND Publishers.State = 'CA'"

You can't follow a line-continuation character with a comment on the same line. There are also some limitations as to where the line-continuation character can be used.

Combining Statements on One Line

There is usually one Visual Basic statement to a line, and there is no statement terminator. However, you can place two or more statements on a line if you use a colon (:) to separate them:

Text1.Text = "Hello" : Red = 255 : Text1.BackColor = _
Red

In order to make your code more readable, however, it's better to place each statement on a separate line.

For More Information For more information, see "Visual Basic Specifications, Limitations, and File Formats."

Adding Comments to Your Code

As you read through the examples in this guide, you'll often come across the comment symbol ('). This symbol tells Visual Basic to ignore the words that follow it. Such words are remarks placed in the code for the benefit of the developer, and other programmers who might examine the code later. For example:

' This is a comment beginning at the left edge of the
' screen.
Text1.Text = "Hi!"      ' Place friendly greeting in text
                     ' box.

Comments can follow a statement on the same line or can occupy an entire line. Both are illustrated in the preceding code. Remember that comments can't follow a line-continuation character on the same line.

Note You can add or remove comment symbols for a block of code by selecting two or more lines of code and choosing the Comment Block or Uncomment Block buttons on the Edit toolbar.

Understanding Numbering Systems

Most numbers in this documentation are decimal (base 10). But occasionally it's convenient to use hexadecimal numbers (base 16) or octal numbers (base 8). Visual Basic represents numbers in hexadecimal with the prefix &H and in octal with &O. The following table shows the same numbers in decimal, octal, and hexadecimal.

Decimal | Octal | Hexadecimal
9 | &O11 | &H9
15 | &O17 | &HF
16 | &O20 | &H10
20 | &O24 | &H14
255 | &O377 | &HFF

You generally don't have to learn the hexadecimal or octal number system yourself because the computer can work with numbers entered in any system. However, some number systems lend themselves to certain tasks, such as using hexadecimals to set the screen and control colors.

Naming Conventions in Visual Basic

While you are writing Visual Basic code, you declare and name many elements (Sub and Function procedures, variables, constants, and so on). The names of the procedures, variables, and constants that you declare in your Visual Basic code must follow these guidelines:

  • They must begin with a letter.

  • They can't contain embedded periods or type-declaration characters (special characters that specify a data type.

  • They can be no longer than 255 characters. The names of controls, forms, classes, and modules must not exceed 40 characters.

  • They can't be the same as restricted keywords.

A restricted keyword is a word that Visual Basic uses as part of its language. This includes predefined statements (such as If and Loop), functions (such as Len and Abs), and operators (such as Or and Mod).

For More Information For a complete list of keywords, see the Language Reference.

Your forms and controls can have the same name as a restricted keyword. For example, you can have a control named Loop. In your code you cannot refer to that control in the usual way, however, because Visual Basic assumes you mean the Loop keyword. For example, this code causes an error:

Loop.Visible = True            ' Causes an error.

To refer to a form or control that has the same name as a restricted keyword, you must either qualify it or surround it with square brackets: [ ]. For example, this code does not cause an error:

MyForm.Loop.Visible = True   ' Qualified with the form
                           ' name.
[Loop].Visible = True         ' Square brackets also
                           ' work.

You can use square brackets in this way when referring to forms and controls, but not when declaring a variable or defining a procedure with the same name as a restricted keyword. Square brackets can also be used to force Visual Basic to accept names provided by other type libraries that conflict with restricted keywords.

Note Because typing square brackets can get tedious, you might want to refrain from using restricted keywords as the name of forms and controls. However, you can use this technique if a future version of Visual Basic defines a new keyword that conflicts with an existing form or control name when you update your code to work with the new version.


Introduction to Variables, Constants and Data Types

You often need to store values temporarily when performing calculations with Visual Basic. For example, you might want to calculate several values, compare them, and perform different operations on them, depending on the result of the comparison. You need to retain the values if you want to compare them, but you don't need to store them in a property.

Visual Basic, like most programming languages, uses variables for storing values. Variables have a name (the word you use to refer to the value the variable contains) and a data type (which determines the kind of data the variable can store). Arrays can be used to store indexed collections of related variables.

Constants also store values, but as the name implies, those values remain constant throughout the execution of an application. Using constants can make your code more readable by providing meaningful names instead of numbers. There are a number of built-in constants in Visual Basic, but you can also create your own.

Data types control the internal storage of data in Visual Basic. By default, Visual Basic uses the Variant data type. There are a number of other available data types that allow you to optimize your code for speed and size when you don't need the flexibility that Variant provides.

For more detailed information, see:

  • Variables An introduction to variables: what they are and how to use them.
  • Understanding the Scope of Variables A discussion of scope as it applies to variables.
  • Advanced Variable Topics Detailed information about variables.
  • Static Variables An introduction to using static variables to preserve values.
  • Constants An introduction to using constants to represent values.
  • Data Types A discussion of the data types available in Visual Basic.
  • Advanced Variant Topics Detailed information about the Variant data type.
  • Arrays An introduction to the use of arrays for groups of values.
  • Dynamic Arrays A discussion of using dynamic arrays to work with groups of values.


Variables

In Visual Basic, you use variables to temporarily store values during the execution of an application. Variables have a name (the word you use to refer to the value the variable contains) and a data type (which determines the kind of data the variable can store).

You can think of a variable as a placeholder in memory for an unknown value. For example, imagine you are creating a program for a fruit stand to track the sales of apples. You don't know the price of an apple or the quantity sold until the sale actually occurs. You can use two variables to hold the unknown values — let's name them ApplePrice and ApplesSold. Each time the program is run, the user supplies the values for the two variables. To calculate the total sales and display it in a Textbox named txtSales, your code would look like this:

txtSales.txt = ApplePrice * ApplesSold

The expression returns a different total each time, depending on what values the user provides. The variables allow you to make a calculation without having to know in advance what the actual inputs are.

In this example, the data type of ApplePrice is Currency; the data type of ApplesSold is an integer. Variables can represent many other values as well: text values, dates, various numeric types, even objects.

Storing and Retrieving Data in Variables

You use assignment statements to perform calculations and assign the result to a variable:

ApplesSold = 10      ' The value 10 is passed to the
                  ' variable.
ApplesSold = ApplesSold + 1      ' The variable is
                              ' incremented.

Note that the equal sign in this example is an assignment operator, not an equality operator; the value (10) is being assigned to the variable (ApplesSold).

Declaring Variables

To declare a variable is to tell the program about it in advance. You declare a variable with the Dim statement, supplying a name for the variable:

Dim variablename [As type]

Variables declared with the Dim statement within a procedure exist only as long as the procedure is executing. When the procedure finishes, the value of the variable disappears. In addition, the value of a variable in a procedure is local to that procedure — that is, you can't access a variable in one procedure from another procedure. These characteristics allow you to use the same variable names in different procedures without worrying about conflicts or accidental changes.

A variable name:

  • Must begin with a letter.

  • Can't contain an embedded period or embedded type-declaration character.

  • Must not exceed 255 characters.

  • Must be unique within the same scope, which is the range from which the variable can be referenced — a procedure, a form, and so on.

The optional As type clause in the Dim statement allows you to define the data type or object type of the variable you are declaring. Data types define the type of information the variable stores. Some examples of data types include String, Integer, and Currency. Variables can also contain objects from Visual Basic or other applications. Examples of Visual Basic object types, or classes, include Object, Form1, and TextBox.

For More Information For more information on objects, see "Programming with Objects" and "Programming with Components." Data types are discussed in detail in the section, "Data Types," later in this chapter.

There are other ways to declare variables:

  • Declaring a variable in the Declarations section of a form, standard, or class module, rather than within a procedure, makes the variable available to all the procedures in the module.

  • Declaring a variable using the Public keyword makes it available throughout your application.

  • Declaring a local variable using the Static keyword preserves its value even when a procedure ends.

Implicit Declaration

You don't have to declare a variable before using it. For example, you could write a function where you don't need to declare TempVal before using it:

Function SafeSqr(num)
   TempVal = Abs(num)
   SafeSqr = Sqr(TempVal)
End Function

Visual Basic automatically creates a variable with that name, which you can use as if you had explicitly declared it. While this is convenient, it can lead to subtle errors in your code if you misspell a variable name. For example, suppose that this was the function you wrote:

Function SafeSqr(num)
   TempVal = Abs(num)
   SafeSqr = Sqr(TemVal)
End Function

At first glance, this looks the same. But because the TempVal variable was misspelled on the next-to-last line, this function will always return zero. When Visual Basic encounters a new name, it can't determine whether you actually meant to implicitly declare a new variable or you just misspelled an existing variable name, so it creates a new variable with that name.

Explicit Declaration

To avoid the problem of misnaming variables, you can stipulate that Visual Basic always warn you whenever it encounters a name not declared explicitly as a variable.

To explicitly declare variables

  • Place this statement in the Declarations section of a class, form, or standard module:
    Option Explicit

    –or–

    From the Tools menu, choose Options, click the Editor tab and check the Require Variable Declaration option. This automatically inserts the Option Explicit statement in any new modules, but not in modules already created; therefore, you must manually add Option Explicit to any existing modules within a project.

Had this statement been in effect for the form or standard module containing the SafeSqr function, Visual Basic would have recognized TempVal and TemVal as undeclared variables and generated errors for both of them. You could then explicitly declare TempVal:

Function SafeSqr(num)
   Dim TempVal
   TempVal = Abs(num)
   SafeSqr = Sqr(TemVal)
End Function

Now you'd understand the problem immediately because Visual Basic would display an error message for the incorrectly spelled TemVal. Because the Option Explicit statement helps you catch these kinds of errors, it's a good idea to use it with all your code.

Note The Option Explicit statement operates on a per-module basis; it must be placed in the Declarations section of every form, standard, and class module for which you want Visual Basic to enforce explicit variable declarations. If you select Require Variable Declaration, Visual Basic inserts Option Explicit in all subsequent form, standard, and class modules, but does not add it to existing code. You must manually add Option Explicit to any existing modules within a project.


Understanding the Scope of Variables

The scope of a variable defines which parts of your code are aware of its existence. When you declare a variable within a procedure, only code within that procedure can access or change the value of that variable; it has a scope that is local to that procedure. Sometimes, however, you need to use a variable with a broader scope, such as one whose value is available to all the procedures within the same module, or even to all the procedures in your entire application. Visual Basic allows you to specify the scope of a variable when you declare it.

Scoping Variables

Depending on how it is declared, a variable is scoped as either a procedure-level (local) or module-level variable.

Scope | Private | Public
Procedure-level | Variables are private to the procedure in which they appear. | Not applicable. You cannot declare public variables within a procedure.
Module-level | Variables are private to the module in which they appear. | Variables are available to all modules.

Variables Used Within a Procedure

Procedure-level variables are recognized only in the procedure in which they're declared. These are also known as local variables. You declare them with the Dim or Static keywords. For example:

Dim intTemp As Integer

–or–

Static intPermanent As Integer

Values in local variables declared with Static exist the entire time your application is running while variables declared with Dim exist only as long as the procedure is executing.

Local variables are a good choice for any kind of temporary calculation. For example, you can create a dozen different procedures containing a variable called intTemp. As long as each intTemp is declared as a local variable, each procedure recognizes only its own version of intTemp. Any one procedure can alter the value in its local intTemp without affecting intTemp variables in other procedures.

Variables Used Within a Module

By default, a module-level variable is available to all the procedures in that module, but not to code in other modules. You create module-level variables by declaring them with the Private keyword in the Declarations section at the top of the module. For example:

Private intTemp As Integer

At the module level, there is no difference between Private and Dim, but Private is preferred because it readily contrasts with Public and makes your code easier to understand.

Variables Used by All Modules

To make a module-level variable available to other modules, use the Public keyword to declare the variable. The values in public variables are available to all procedures in your application. Like all module-level variables, public variables are declared in the Declarations section at the top of the module. For example:

Public intTemp As Integer

Note You can't declare public variables within a procedure, only within the Declarations section of a module.

For More Information For additional information about variables, see "Advanced Variable Topics."


Advanced Variable Topics

Using Multiple Variables with the Same Name

If public variables in different modules share the same name, it's possible to differentiate between them in code by referring to both the module and variable names. For example, if there is a public Integer variable intX declared in both Form1 and in Module1, you can refer to them as Module1.intX and Form1.intX to get the correct values.

To see how this works, insert two standard modules in a new project and draw three command buttons on a form.

One variable, intX, is declared in the first standard module, Module1. The Test procedure sets its value:

Public intX As Integer      ' Declare Module1's intX.
Sub Test()
   ' Set the value for the intX variable in Module1.
   intX = 1
End Sub

The second variable, which has the same name, intX, is declared in the second standard module, Module2. Again, a procedure named Test sets its value:

Public intX As Integer      ' Declare Module2's intX.
Sub Test()
   ' Set the value for the intX variable in Module2.
   intX = 2
End Sub

The third intX variable is declared in the form module. And again, a procedure named Test sets its value.

Public intX As Integer   ' Declare the form's intX
                        ' variable.
Sub Test()
   ' Set the value for the intX variable in the form.
   intX = 3
End Sub

Each of the three command buttons' Click event procedures calls the appropriate Test procedure and uses MsgBox to display the values of the three variables.

Private Sub Command1_Click()
   Module1.Test               ' Calls Test in Module1.
   MsgBox Module1.intX         ' Displays Module1's intX.
End Sub

Private Sub Command2_Click()
   Module2.Test               ' Calls Test in Module2.
   MsgBox Module2.intX         ' Displays Module2's intX.
End Sub

Private Sub Command3_Click()
   Test                     ' Calls Test in Form1.
   MsgBox intX               ' Displays Form1's intX.
End Sub

Run the application and click each of the three command buttons. You'll see the separate references to the three public variables. Notice in the third command button's Click event procedure, you don't need to specify Form1.Test when calling the form's Test procedure, or Form1.intX when calling the value of the form's Integer variable. If there are multiple procedures and variables with the same name, Visual Basic takes the value of the more local variable, which in this case, is the Form1 variable.

Public vs. Local Variables

You can also have a variable with the same name at a different scope. For example, you could have a public variable named Temp and then, within a procedure, declare a local variable named Temp. References to the name Temp within the procedure would access the local variable; references to Temp outside the procedure would access the public variable. The module-level variable can be accessed from within the procedure by qualifying the variable with the module name.

Public Temp As Integer
Sub Test()
   Dim Temp As Integer
   Temp = 2               ' Temp has a value of 2.
   MsgBox Form1.Temp      ' Form1.Temp has a value of 1.
End Sub

Private Sub Form_Load()
   Temp = 1               ' Set Form1.Temp to 1.
End Sub
Private Sub Command1_Click()
   Test
End Sub

In general, when variables have the same name but different scope, the more local variable always shadows (that is, it is accessed in preference to) less local variables. So if you also had a procedure-level variable named Temp, it would shadow the public variable Temp within that module.

Shadowing Form Properties and Controls

Due to the effect of shadowing, form properties, controls, constants, and procedures are treated as module-level variables in the form module. It is not legal to have a form property or control with the same name as a module-level variable, constant, user-defined type, or procedure because both are in the same scope.

Within the form module, local variables with the same names as controls on the form shadow the controls. You must qualify the control with a reference to the form or the Me keyword to set or get its value or any of its properties. For example:

Private Sub Form_Click ()
Dim Text1, BackColor
' Assume there is also a control on the form called
' Text1.
   Text1 = "Variable"      ' Variable shadows control.
   Me.Text1 = "Control"   ' Must qualify with Me to get
                        ' control.
   Text1.Top = 0         ' This causes an error!
   Me.Text1.Top = 0      ' Must qualify with Me to get
                        ' control.
   BackColor = 0         ' Variable shadows property.
   Me.BackColor = 0      ' Must qualify with Me to get
                        ' form property.
End Sub

Using Variables and Procedures with the Same Name

The names of your private module-level and public module-level variables can also conflict with the names of your procedures. A variable in the module cannot have the same name as any procedures or types defined in the module. It can, however, have the same name as public procedures, types, or variables defined in other modules. In this case, when the variable is accessed from another module, it must be qualified with the module name.

While the shadowing rules described above are not complex, shadowing can be confusing and lead to subtle bugs in your code; it is good programming practice to keep the names of your variables distinct from each other. In form modules, try to use variables names that are different from names of controls on those forms.


Static Variables

In addition to scope, variables have a lifetime, the period of time during which they retain their value. The values in module-level and public variables are preserved for the lifetime of your application. However, local variables declared with Dim exist only while the procedure in which they are declared is executing. Usually, when a procedure is finished executing, the values of its local variables are not preserved and the memory used by the local variables is reclaimed. The next time the procedure is executed, all its local variables are reinitialized.

However, you can preserve the value of a local variable by making the variable static. Use the Static keyword to declare one or more variables inside a procedure, exactly as you would with the Dim statement:

Static Depth

For example, the following function calculates a running total by adding a new value to the total of previous values stored in the static variable Accumulate:

Function RunningTotal(num)
   Static ApplesSold
   ApplesSold = ApplesSold + num
   RunningTotal = ApplesSold
End Function

If ApplesSold was declared with Dim instead of Static, the previous accumulated values would not be preserved across calls to the function, and the function would simply return the same value with which it was called.

You can produce the same result by declaring ApplesSold in the Declarations section of the module, making it a module-level variable. Once you change the scope of a variable this way, however, the procedure no longer has exclusive access to it. Because other procedures can access and change the value of the variable, the running totals might be unreliable and the code would be more difficult to maintain.

Declaring All Local Variables as Static

To make all local variables in a procedure static, place the Static keyword at the beginning of a procedure heading. For example:

Static Function RunningTotal(num)

This makes all the local variables in the procedure static regardless of whether they are declared with Static, Dim, Private, or declared implicitly. You can place Static in front of any Sub or Function procedure heading, including event procedures and those declared as Private.


Constants

Often you'll find that your code contains constant values that reappear over and over. Or you may find that the code depends on certain numbers that are difficult to remember — numbers that, in and of themselves, have no obvious meaning.

In these cases, you can greatly improve the readability of your code — and make it easier to maintain — by using constants. A constant is a meaningful name that takes the place of a number or string that does not change. Although a constant somewhat resembles a variable, you can't modify a constant or assign a new value to it as you can to a variable. There are two sources for constants:

  • Intrinsic or system-defined constants are provided by applications and controls. Visual Basic constants are listed in the Visual Basic (VB) and Visual Basic for applications (VBA) object libraries in the Object Browser. Other applications that provide object libraries, such as Microsoft Excel and Microsoft Project, also provide a list of constants you can use with their objects, methods, and properties. Constants are also defined in the object library for each ActiveX control. For details on using the Object Browser, see "Programming with Objects."

  • Symbolic or user-defined constants are declared using the Const statement. User-defined constants are described in the next section, "Creating Your Own Constants."

In Visual Basic, constant names are in a mixed-case format, with a prefix indicating the object library that defines the constant. Constants from the Visual Basic and Visual Basic for applications object libraries are prefaced with "vb" — for instance, vbTileHorizontal.

The prefixes are intended to prevent accidental collisions in cases where constants have identical names and represent different values. Even with prefixes, it's still possible that two object libraries may contain identical constants representing different values. Which constant is referenced in this case depends on which object library has the higher priority. For information on changing the priority of object libraries, see the "References Dialog Box."

To be absolutely sure you avoid constant name collisions, you can qualify references to constants with the following syntax:

[libname.][modulename.]constname

Libname is usually the class name of the control or library. Modulename is the name of the module that defines the constant. Constname is the name of the constant. Each of these elements is defined in the object library, and can be viewed in the Object Browser.

Creating Your Own Constants

The syntax for declaring a constant is:

[Public|Private] Const constantname[As type] = expression

The argument constantname is a valid symbolic name (the rules are the same as those for creating variable names), and expression is composed of numeric or string constants and operators; however, you can't use function calls in expression.

A Const statement can represent a mathematical or date/time quantity:

Const conPi = 3.14159265358979
Public Const conMaxPlanets As Integer = 9
Const conReleaseDate = #1/1/95#

The Const statement can also be used to define string constants:

Public Const conVersion = "07.10.A"
Const conCodeName = "Enigma"

You can place more than one constant declaration on a single line if you separate them with commas:

Public Const conPi = 3.14, conMaxPlanets = 9, _
conWorldPop = 6E+09

The expression on the right side of the equal sign ( = ) is often a number or literal string, but it can also be an expression that results in a number or string (although that expression can't contain calls to functions). You can even define constants in terms of previously defined constants:

Const conPi2 = conPi * 2

Once you define constants, you can place them in your code to make it more readable. For example:

Static SolarSystem(1 To conMaxPlanets)
If numPeople > conWorldPop Then Exit Sub

Scoping User-Defined Constants

A Const statement has scope like a variable declaration, and the same rules apply:

  • To create a constant that exists only within a procedure, declare it within that procedure.

  • To create a constant available to all procedures within a module, but not to any code outside that module, declare it in the Declarations section of the module.

  • To create a constant available throughout the application, declare the constant in the Declarations section of a standard module, and place the Public keyword before Const. Public constants cannot be declared in a form or class module.

For More Information For more information regarding scope, see "Understanding the Scope of Variables" earlier in this chapter.

Avoiding Circular References

Because constants can be defined in terms of other constants, you must be careful not to set up a cycle, or circular reference between two or more constants. A cycle occurs when you have two or more public constants, each of which is defined in terms of the other.

For example:

' In Module 1:
Public Const conA = conB * 2   ' Available throughout
                              ' application.
' In Module 2:
Public Const conB = conA / 2   ' Available throughout
                              ' application.

If a cycle occurs, Visual Basic generates an error when you attempt to run your application. You cannot run your code until you resolve the circular reference. To avoid creating a cycle, restrict all your public constants to a single module or, at most, a small number of modules.


Data Types

Variables are placeholders used to store values; they have names and data types. The data type of a variable determines how the bits representing those values are stored in the computer's memory. When you declare a variable, you can also supply a data type for it. All variables have a data type that determines what kind of data they can store.

By default, if you don't supply a data type, the variable is given the Variant data type. The Variant data type is like a chameleon — it can represent many different data types in different situations. You don't have to convert between these types of data when assigning them to a Variant variable: Visual Basic automatically performs any necessary conversion.

If you know that a variable will always store data of a particular type, however, Visual Basic can handle that data more efficiently if you declare a variable of that type. For example, a variable to store a person's name is best represented as a string data type, because a name is always composed of characters.

Data types apply to other things besides variables. When you assign a value to a property, that value has a data type; arguments to functions also have data types. In fact, just about anything in Visual Basic that involves data also involves data types.

You can also declare arrays of any of the fundamental types.

For More Information For more information, see the section, "Arrays," later in this chapter. Selecting data types to improve your application's performance is discussed in "Designing for Performance and Compatibility."

Declaring Variables with Data Types

Before using a non-Variant variable, you must use the Private, Public, Dim or Static statement to declare it As type. For example, the following statements declare an Integer, Double, String, and Currency type, respectively:

Private I As Integer
Dim Amt As Double
Static YourName As String
Public BillsPaid As Currency

A Declaration statement can combine multiple declarations, as in these statements:

Private I As Integer, Amt As Double
Private YourName As String, BillsPaid As Currency
Private Test, Amount, J As Integer

Note If you do not supply a data type, the variable is given the default type. In the preceding example, the variables Test and Amount are of the Variant data type. This may surprise you if your experience with other programming languages leads you to expect all variables in the same declaration statement to have the same specified type (in this case, Integer).

Numeric Data Types

Visual Basic supplies several numeric data types — Integer, Long (long integer), Single (single-precision floating point), Double (double-precision floating point), and Currency. Using a numeric data type generally uses less storage space than a variant.

If you know that a variable will always store whole numbers (such as 12) rather than numbers with a fractional amount (such as 3.57), declare it as an Integer or Long type. Operations are faster with integers, and these types consume less memory than other data types. They are especially useful as the counter variables in For...Next loops.

For More Information To read more about control structures, see "Introduction to Control Structures" later in this chapter.

If the variable contains a fraction, declare it as a Single, Double, or Currency variable. The Currency data type supports up to four digits to the right of the decimal separator and fifteen digits to the left; it is an accurate fixed-point data type suitable for monetary calculations. Floating-point (Single and Double) numbers have much larger ranges than Currency, but can be subject to small rounding errors.

Note Floating-point values can be expressed as mmm E eee or mmm D eee, in which mmm is the mantissa and eee is the exponent (a power of 10). The highest positive value of a Single data type is 3.402823E+38, or 3.4 times 10 to the 38th power; the highest positive value of a Double data type is 1.79769313486232D+308, or about 1.8 times 10 to the 308th power. Using D to separate the mantissa and exponent in a numeric literal causes the value to be treated as a Double data type. Likewise, using E in the same fashion treats the value as a Single data type.

The Byte Data Type

If the variable contains binary data, declare it as an array of the Byte data type. (Arrays are discussed in "Arrays" later in this chapter). Using Byte variables to store binary data preserves it during format conversions. When String variables are converted between ANSI and Unicode formats, any binary data in the variable is corrupted. Visual Basic may automatically convert between ANSI and Unicode when:

  • Reading from files

  • Writing to files

  • Calling DLLs

  • Calling methods and properties on objects

All operators that work on integers work with the Byte data type except unary minus. Since Byte is an unsigned type with the range 0-255, it cannot represent a negative number. So for unary minus, Visual Basic coerces the Byte to a signed integer first.

All numeric variables can be assigned to each other and to variables of the Variant type. Visual Basic rounds off rather than truncates the fractional part of a floating-point number before assigning it to an integer.

For More Information For details on Unicode and ANSI conversions, see "International Issues."

The String Data Type

If you have a variable that will always contain a string and never a numeric value, you can declare it to be of type String:

Private S As String

You can then assign strings to this variable and manipulate it using string functions:

S = "Database"
S = Left(S, 4)

By default, a string variable or argument is a variable-length string; the string grows or shrinks as you assign new data to it. You can also declare strings that have a fixed length. You specify a fixed-length string with this syntax:

String * size

For example, to declare a string that is always 50 characters long, use code like this:

Dim EmpName As String * 50

If you assign a string of fewer than 50 characters, EmpName is padded with enough trailing spaces to total 50 characters. If you assign a string that is too long for the fixed-length string, Visual Basic simply truncates the characters.

Because fixed-length strings are padded with trailing spaces, you may find the Trim and RTrim functions, which remove the spaces, useful when working with them.

Fixed-length strings in standard modules can be declared as Public or Private. In forms and class modules, fixed-length strings must be declared Private.

For More Information See "Ltrim, RTrim Function and Trim Functions" in the Language Reference.

Exchanging Strings and Numbers

You can assign a string to a numeric variable if the string represents a numeric value. It's also possible to assign a numeric value to a string variable. For example, place a command button, text box, and list box on a form. Enter the following code in the command button's Click event. Run the application, and click the command button.

Private Sub Command1_Click()
   Dim intX As Integer
   Dim strY As String
   strY = "100.23"
   intX = strY      ' Passes the string to a numeric
                  ' variable.
   List1.AddItem Cos(strY)   ' Adds cosine of number in
                        ' the string to the listbox.
   strY = Cos(strY)         ' Passes cosine to the
                           ' string variable.
   Text1.Text = strY      ' String variable prints in
                        ' the text box.
End Sub

Visual Basic will automatically coerce the variables to the appropriate data type. You should use caution when exchanging strings and numbers; passing a non-numeric value in the string will cause a run-time error to occur.

The Boolean Data Type

If you have a variable that will contain simple true/false, yes/no, or on/off information, you can declare it to be of type Boolean. The default value of Boolean is False. In the following example, blnRunning is a Boolean variable which stores a simple yes/no setting.

Dim blnRunning As Boolean
   ' Check to see if the tape is running.
   If Recorder.Direction = 1 Then
   blnRunning = True
End if

The Date Data Type

Date and time values can be contained both in the specific Date data type and in Variant variables. The same general characteristics apply to dates in both types.

For More Information See the section, "Date/Time Values Stored in Variants," in "Advanced Variant Topics."

When other numeric data types are converted to Date, values to the left of the decimal represent date information, while values to the right of the decimal represent time. Midnight is 0, and midday is 0.5. Negative whole numbers represent dates before December 30, 1899.

The Object Data Type

Object variables are stored as 32-bit (4-byte) addresses that refer to objects within an application or within some other application. A variable declared as Object is one that can subsequently be assigned (using the Set statement) to refer to any actual object recognized by the application.

Dim objDb As Object
Set objDb = OpenDatabase("c:\Vb5\Biblio.mdb")

When declaring object variables, try to use specific classes (such as TextBox instead of Control or, in the preceding case, Database instead of Object) rather than the generic Object. Visual Basic can resolve references to the properties and methods of objects with specific types before you run an application. This allows the application to perform faster at run time. Specific classes are listed in the Object Browser.

When working with other applications' objects, instead of using a Variant or the generic Object, declare objects as they are listed in the Classes list in the Object Browser. This ensures that Visual Basic recognizes the specific type of object you're referencing, allowing the reference to be resolved at run time.

For More Information For more information on creating and assigning objects and object variables, see "Creating Objects" later in this chapter.

Converting Data Types

Visual Basic provides several conversion functions you can use to convert values into a specific data type. To convert a value to Currency, for example, you use the CCur function:

PayPerWeek = CCur(hours * hourlyPay)

Conversion function | Converts an expression to
Cbool | Boolean
Cbyte | Byte
Ccur | Currency
Cdate | Date
CDbl | Double
Cint | Integer
CLng | Long
CSng | Single
CStr | String
Cvar | Variant
CVErr | Error

Note Values passed to a conversion function must be valid for the destination data type or an error occurs. For example, if you attempt to convert a Long to an Integer, the Long must be within the valid range for the Integer data type.

For More Information See the Language Reference for a specific conversion function.

The Variant Data Type

A Variant variable is capable of storing all system-defined types of data. You don't have to convert between these types of data if you assign them to a Variant variable; Visual Basic automatically performs any necessary conversion. For example:

Dim SomeValue      ' Variant by default.
SomeValue = "17"   ' SomeValue contains "17" (a two-
                  ' character string).
SomeValue = SomeValue - 15       ' SomeValue now contains
                              ' the numeric value 2.
SomeValue = "U" & SomeValue      ' SomeValue now contains
                     ' "U2" (a two- character string).

While you can perform operations on Variant variables without much concern for the kind of data they contain, there are some traps you must avoid.

  • If you perform arithmetic operations or functions on a Variant, the Variant must contain something that is a number. For details, see the section, "Numeric Values Stored in Variants," in "Advanced Variant Topics."

  • If you are concatenating strings, use the & operator instead of the + operator. For details, see the section, "Strings Stored in Variants," in "Advanced Variant Topics."

In addition to being able to act like the other standard data types, Variants can also contain three special values: Empty, Null, and Error.

The Empty Value

Sometimes you need to know if a value has ever been assigned to a created variable. A Variant variable has the Empty value before it is assigned a value. The Empty value is a special value different from 0, a zero-length string (""), or the Null value. You can test for the Empty value with the IsEmpty function:

If IsEmpty(Z) Then Z = 0

When a Variant contains the Empty value, you can use it in expressions; it is treated as either 0 or a zero-length string, depending on the expression.

The Empty value disappears as soon as any value (including 0, a zero-length string, or Null) is assigned to a Variant variable. You can set a Variant variable back to Empty by assigning the keyword Empty to the Variant.

The Null Value

The Variant data type can contain another special value: Null. Null is commonly used in database applications to indicate unknown or missing data. Because of the way it is used in databases, Null has some unique characteristics:

  • Expressions involving Null always result in Null. Thus, Null is said to "propagate" through expressions; if any part of the expression evaluates to Null, the entire expression evaluates to Null.

  • Passing Null, a Variant containing Null, or an expression that evaluates to Null as an argument to most functions causes the function to return Null.

  • Null values propagate through intrinsic functions that return Variant data types.

You can also assign Null with the Null keyword:

Z = Null

You can use the IsNull function to test if a Variant variable contains Null:

If IsNull(X) And IsNull(Y) Then
   Z = Null
Else
   Z = 0
End If

If you assign Null to a variable of any type other than Variant, a trappable error occurs. Assigning Null to a Variant variable doesn't cause an error, and Null will propagate through expressions involving Variant variables (though Null does not propagate through certain functions). You can return Null from any Function procedure with a Variant return value.

Variables are not set to Null unless you explicitly assign Null to them, so if you don't use Null in your application, you don't have to write code that tests for and handles it.

For More Information For information on how to use Null in expressions, see "Null" in the Language Reference.

The Error Value

In a Variant, Error is a special value used to indicate that an error condition has occurred in a procedure. However, unlike for other kinds of errors, normal application-level error handling does not occur. This allows you, or the application itself, to take some alternative based on the error value. Error values are created by converting real numbers to error values using the CVErr function.

For More Information For information on how to use the Error value in expressions, see "CVErr Function" in the Language Reference. For information on error handling, see "Debugging Your Code and Handling Errors." For additional information about the Variant data type, see "Advanced Variant Topics."


Advanced Variant Topics

Internal Representation of Values in Variants

Variant variables maintain an internal representation of the values that they store. This representation determines how Visual Basic treats these values when performing comparisons and other operations. When you assign a value to a Variant variable, Visual Basic uses the most compact representation that accurately records the value. Later operations may cause Visual Basic to change the representation it is using for a particular variable. (A Variant variable is not a variable with no type; rather, it is a variable that can freely change its type.) These internal representations correspond to the explicit data types discussed in "Data Types" earlier in this chapter.

Note A variant always takes up 16 bytes, no matter what you store in it. Objects, strings, and arrays are not physically stored in the Variant; in these cases, four bytes of the Variant are used to hold either an object reference, or a pointer to the string or array. The actual data are stored elsewhere.

Most of the time, you don't have to be concerned with what internal representation Visual Basic is using for a particular variable; Visual Basic handles conversions automatically. If you want to know what value Visual Basic is using, however, you can use the VarType function.

For example, if you store values with decimal fractions in a Variant variable, Visual Basic always uses the Double internal representation. If you know that your application does not need the high accuracy (and slower speed) that a Double value supplies, you can speed your calculations by converting the values to Single, or even to Currency:

If VarType(X) = 5 Then X = CSng(X)   ' Convert to Single.

With an array variable, the value of VarType is the sum of the array and data type return values. For example, this array contains Double values:

Private Sub Form_Click()
   Dim dblSample(2) As Double
   MsgBox VarType(dblSample)
End Sub

Future versions of Visual Basic may add additional Variant representations, so any code you write that makes decisions based on the return value of the VarType function should gracefully handle return values that are not currently defined.

For More Information For information about the VarType function, see "VarType Function" in the Language Reference. To read more about arrays, see "Arrays" later in this chapter. For details on converting data types, see "Data Types" earlier in this chapter.

Numeric Values Stored in Variants

When you store whole numbers in Variant variables, Visual Basic uses the most compact representation possible. For example, if you store a small number without a decimal fraction, the Variant uses an Integer representation for the value. If you then assign a larger number, Visual Basic will use a Long value or, if it is very large or has a fractional component, a Double value.

Sometimes you want to use a specific representation for a number. For example, you might want a Variant variable to store a numeric value as Currency to avoid round-off errors in later calculations. Visual Basic provides several conversion functions that you can use to convert values into a specific type (see "Converting Data Types" earlier in this chapter). To convert a value to Currency, for example, you use the CCur function:

PayPerWeek = CCur(hours * hourlyPay)

An error occurs if you attempt to perform a mathematical operation or function on a Variant that does not contain a number or something that can be interpreted as a number. For example, you cannot perform any arithmetic operations on the value U2 even though it contains a numeric character, because the entire value is not a valid number. Likewise, you cannot perform any calculations on the value 1040EZ; however, you can perform calculations on the values +10 or -1.7E6 because they are valid numbers. For this reason, you often want to determine if a Variant variable contains a value that can be used as a number. The IsNumeric function performs this task:

Do
   anyNumber = InputBox("Enter a number")
Loop Until IsNumeric(anyNumber)
MsgBox "The square root is: " & Sqr(anyNumber)

When Visual Basic converts a representation that is not numeric (such as a string containing a number) to a numeric value, it uses the Regional settings (specified in the Windows Control Panel) to interpret the thousands separator, decimal separator, and currency symbol.

Thus, if the country setting in the Windows Control Panel is set to United States, Canada, or Australia, these two statements would return true:

IsNumeric("$100")
IsNumeric("1,560.50")

While these two statements would return false:

IsNumeric("DM100")
IsNumeric("1.560,50")

However, the reverse would be the case — the first two would return false and the second two true — if the country setting in the Windows Control Panel was set to Germany.

If you assign a Variant containing a number to a string variable or property, Visual Basic converts the representation of the number to a string automatically. If you want to explicitly convert a number to a string, use the CStr function. You can also use the Format function to convert a number to a string that includes formatting such as currency, thousands separator, and decimal separator symbols. The Format function automatically uses the appropriate symbols according to the Regional Settings Properties dialog box in the Windows Control Panel.

For More Information See "Format Function" and topics about the conversion functions in the Language Reference. For information on writing code for applications that will be distributed in foreign markets, see "International Issues."

Strings Stored in Variants

Generally, storing and using strings in Variant variables poses few problems. As mentioned earlier, however, sometimes the result of the + operator can be ambiguous when used with two Variant values. If both of the Variants contain numbers, the + operator performs addition. If both of the Variants contain strings, then the + operator performs string concatenation. But if one of the values is represented as a number and the other is represented as a string, the situation becomes more complicated. Visual Basic first attempts to convert the string into a number. If the conversion is successful, the + operator adds the two values; if unsuccessful, it generates a Type mismatch error.

To make sure that concatenation occurs, regardless of the representation of the value in the variables, use the & operator. For example, the following code:

Sub Form_Click ()
   Dim X, Y
   X = "6"
   Y = "7"
   Print X + Y, X & Y
   X = 6
   Print X + Y, X & Y
End Sub

produces this result on the form:

67      67
13      67

Note Visual Basic stores strings internally as Unicode. For more information on Unicode, see "International Issues."

Date/Time Values Stored in Variants

Variant variables can also contain date/time values. Several functions return date/time values. For example, DateSerial returns the number of days left in the year:

Private Sub Form_Click ()
   Dim rightnow, daysleft, hoursleft, minutesleft
   rightnow = Now   ' Now returns the current date/time.
   daysleft = Int(DateSerial(Year(rightnow) _
   + 1, 1, 1) - rightnow)
   hoursleft = 24 - Hour(rightnow)
   minutesleft = 60 - Minute(rightnow)
   Print daysleft & " days left in the year."
   Print hoursleft & " hours left in the day."
   Print minutesleft & " minutes left in the hour."
End Sub

You can also perform math on date/time values. Adding or subtracting integers adds or subtracts days; adding or subtracting fractions adds or subtracts time. Therefore, adding 20 adds 20 days, while subtracting 1/24 subtracts one hour.

The range for dates stored in Variant variables is January 1, 0100, to December 31, 9999. Calculations on dates don't take into account the calendar revisions prior to the switch to the Gregorian calendar, however, so calculations producing date values earlier than the year in which the Gregorian calendar was adopted (1752 in Britain and its colonies at that time; earlier or later in other countries) will be incorrect.

You can use date/time literals in your code by enclosing them with the number sign (#), in the same way you enclose string literals with double quotation marks (""). For example, you can compare a Variant containing a date/time value with a literal date:

If SomeDate > #3/6/93# Then

Similarly, you can compare a date/time value with a complete date/time literal:

If SomeDate > #3/6/93 1:20pm# Then

If you do not include a time in a date/time literal, Visual Basic sets the time part of the value to midnight (the start of the day). If you do not include a date in a date/time literal, Visual Basic sets the date part of the value to December 30, 1899.

Visual Basic accepts a wide variety of date and time formats in literals. These are all valid date/time values:

SomeDate = #3-6-93 13:20#
SomeDate = #March 27, 1993 1:20am#
SomeDate = #Apr-2-93#
SomeDate = #4 April 1993#

For More Information For information on handling dates in international formats, see "International Issues."

In the same way that you can use the IsNumeric function to determine if a Variant variable contains a value that can be considered a valid numeric value, you can use the IsDate function to determine if a Variant contains a value that can be considered a valid date/time value. You can then use the CDate function to convert the value into a date/time value.

For example, the following code tests the Text property of a text box with IsDate. If the property contains text that can be considered a valid date, Visual Basic converts the text into a date and computes the days left until the end of the year:

Dim SomeDate, daysleft
If IsDate(Text1.Text) Then
   SomeDate = CDate(Text1.Text)
   daysleft = DateSerial(Year(SomeDate) + _
   1, 1, 1) - SomeDate
   Text2.Text = daysleft & " days left in the year."
Else
   MsgBox Text1.Text & " is not a valid date."
End If

For More Information For information about the various date/time functions, see "Date Function" in the Language Reference.

Objects Stored in Variants

Objects can be stored in Variant variables. This can be useful when you need to gracefully handle a variety of data types, including objects. For example, all the elements in an array must have the same data type. Setting the data type of an array to Variant allows you to store objects alongside other data types in an array.


Arrays

If you have programmed in other languages, you're probably familiar with the concept of arrays. Arrays allow you to refer to a series of variables by the same name and to use a number (an index) to tell them apart. This helps you create smaller and simpler code in many situations, because you can set up loops that deal efficiently with any number of cases by using the index number. Arrays have both upper and lower bounds, and the elements of the array are contiguous within those bounds. Because Visual Basic allocates space for each index number, avoid declaring an array larger than necessary.

Note The arrays discussed in this section are arrays of variables, declared in code. They are different from the control arrays you specify by setting the Index property of controls at design time. Arrays of variables are always contiguous; unlike control arrays, you cannot load and unload elements from the middle of the array.

All the elements in an array have the same data type. Of course, when the data type is Variant, the individual elements may contain different kinds of data (objects, strings, numbers, and so on). You can declare an array of any of the fundamental data types, including user-defined types (described in the section, "Creating Your Own Data Types," in "More About Programming") and object variables (described in "Programming with Objects").

In Visual Basic there are two types of arrays: a fixed-size array which always remains the same size, and a dynamic array whose size can change at run-time. Dynamic arrays are discussed in more detail in the section "Dynamic Arrays" later in this chapter.

Declaring Fixed-Size Arrays

There are three ways to declare a fixed-size array, depending on the scope you want the array to have:

  • To create a public array, use the Public statement in the Declarations section of a module to declare the array.

  • To create a module-level array, use the Private statement in the Declarations section of a module to declare the array.

  • To create a local array, use the Private statement in a procedure to declare the array.

Setting Upper and Lower Bounds

When declaring an array, follow the array name by the upper bound in parentheses. The upper bound cannot exceed the range of a Long data type (-2,147,483,648 to 2,147,483,647). For example, these array declarations can appear in the Declarations section of a module:

Dim Counters(14) As Integer         ' 15 elements.
Dim Sums(20) As Double            ' 21 elements.

To create a public array, you simply use Public in place of Dim:

Public Counters(14) As Integer
Public Sums(20) As Double

The same declarations within a procedure use Dim:

Dim Counters(14) As Integer
Dim Sums(20) As Double

The first declaration creates an array with 15 elements, with index numbers running from 0 to 14. The second creates an array with 21 elements, with index numbers running from 0 to 20. The default lower bound is 0.

To specify a lower bound, provide it explicitly (as a Long data type) using the To keyword:

Dim Counters(1 To 15) As Integer
Dim Sums(100 To 120) As String

In the preceding declarations, the index numbers of Counters range from 1 to 15, and the index numbers of Sums range from 100 to 120.

Arrays that Contain Other Arrays

It's possible to create a Variant array, and populate it with other arrays of different data types. The following code creates two arrays, one containing integers and the other strings. It then declares a third Variant array and populates it with the integer and string arrays.

Private Sub Command1_Click()
   Dim intX As Integer   ' Declare counter variable.
   ' Declare and populate an integer array.
   Dim countersA(5) As Integer
      For intX = 0 To 4
         countersA(intX) = 5
      Next intX
   ' Declare and populate a string array.
      Dim countersB(5) As String
         For intX = 0 To 4
            countersB(intX) = "hello"
         Next intX
   Dim arrX(2) As Variant   ' Declare a new two-member
                           ' array.
      arrX(1) = countersA()   ' Populate the array with
                              ' other arrays.
      arrX(2) = countersB()
      MsgBox arrX(1)(2)   ' Display a member of each
                        ' array.
      MsgBox arrX(2)(3)
End Sub

Multidimensional Arrays

Sometimes you need to keep track of related information in an array. For example, to keep track of each pixel on your computer screen, you need to refer to its X and Y coordinates. This can be done using a multidimensional array to store the values.

With Visual Basic, you can declare arrays of multiple dimensions. For example, the following statement declares a two-dimensional 10-by-10 array within a procedure:

Static MatrixA(9, 9) As Double

Either or both dimensions can be declared with explicit lower bounds:

Static MatrixA(1 To 10, 1 To 10) As Double

You can extend this to more than two dimensions. For example:

Dim MultiD(3, 1 To 10, 1 To 15)

This declaration creates an array that has three dimensions with sizes 4 by 10 by 15. The total number of elements is the product of these three dimensions, or 600.

Note When you start adding dimensions to an array, the total storage needed by the array increases dramatically, so use multidimensional arrays with care. Be especially careful with Variant arrays, because they are larger than other data types.

Using Loops to Manipulate Arrays

You can efficiently process a multidimensional array by using nested For loops. For example, these statements initialize every element in MatrixA to a value based on its location in the array:

Dim I As Integer, J As Integer
Static MatrixA(1 To 10, 1 To 10) As Double
For I = 1 To 10
   For J = 1 To 10
      MatrixA(I, J) = I * 10 + J
   Next J
Next I

For More Information For information about loops, see "Loop Structures" later in this chapter.


Dynamic Arrays

Sometimes you may not know exactly how large to make an array. You may want to have the capability of changing the size of the array at run time.

A dynamic array can be resized at any time. Dynamic arrays are among the most flexible and convenient features in Visual Basic, and they help you to manage memory efficiently. For example, you can use a large array for a short time and then free up memory to the system when you're no longer using the array.

The alternative is to declare an array with the largest possible size and then ignore array elements you don't need. However, this approach, if overused, might cause the operating environment to run low on memory.

To create a dynamic array

  1. Declare the array with a Public statement (if you want the array to be public) or Dim statement at the module level (if you want the array to be module level), or a Static or Dim statement in a procedure (if you want the array to be local). You declare the array as dynamic by giving it an empty dimension list.
    Dim DynArray()
  2. Allocate the actual number of elements with a ReDim statement.
    ReDim DynArray(X + 1)

The ReDim statement can appear only in a procedure. Unlike the Dim and Static statements, ReDim is an executable statement — it makes the application carry out an action at run time.

The ReDim statement supports the same syntax used for fixed arrays. Each ReDim can change the number of elements, as well as the lower and upper bounds, for each dimension. However, the number of dimensions in the array cannot change.

ReDim DynArray(4 to 12)

For example, the dynamic array Matrix1 is created by first declaring it at the module level:

Dim Matrix1() As Integer

A procedure then allocates space for the array:

Sub CalcValuesNow ()
   .
   .
   .
   ReDim Matrix1(19, 29)
End Sub

The ReDim statement shown here allocates a matrix of 20 by 30 integers (at a total size of 600 elements). Alternatively, the bounds of a dynamic array can be set using variables:

ReDim Matrix1(X, Y)

Note You can assign strings to resizable arrays of bytes. An array of bytes can also be assigned to a variable-length string. Be aware that the number of bytes in a string varies among platforms. On Unicode platforms the same string contains twice as many bytes as it does on a non-Unicode platform.

Preserving the Contents of Dynamic Arrays

Each time you execute the ReDim statement, all the values currently stored in the array are lost. Visual Basic resets the values to the Empty value (for Variant arrays), to zero (for numeric arrays), to a zero-length string (for string arrays), or to Nothing (for arrays of objects).

This is useful when you want to prepare the array for new data, or when you want to shrink the size of the array to take up minimal memory. Sometimes you may want to change the size of the array without losing the data in the array. You can do this by using ReDim with the Preserve keyword. For example, you can enlarge an array by one element without losing the values of the existing elements using the UBound function to refer to the upper bound:

ReDim Preserve DynArray(UBound(DynArray) + 1)

Only the upper bound of the last dimension in a multidimensional array can be changed when you use the Preserve keyword; if you change any of the other dimensions, or the lower bound of the last dimension, a run-time error occurs. Thus, you can use code like this:

ReDim Preserve Matrix(10, UBound(Matrix, 2) + 1)

But you cannot use this code:

ReDim Preserve Matrix(UBound(Matrix, 1) + 1, 10)

For More Information For information about dynamic arrays, see "ReDim Statement" in the Language Reference. To learn more about object arrays, see "Programming with Objects."


Introduction to Procedures

You can simplify programming tasks by breaking programs into smaller logical components. These components — called procedures — can then become building blocks that let you enhance and extend Visual Basic.

Procedures are useful for condensing repeated or shared tasks, such as frequently used calculations, text and control manipulation, and database operations.

There are two major benefits of programming with procedures:

  • Procedures allow you to break your programs into discrete logical units, each of which you can debug more easily than an entire program without procedures.

  • Procedures used in one program can act as building blocks for other programs, usually with little or no modification.

There are several types of procedures used in Visual Basic:

  • Sub procedures do not return a value.

  • Function procedures return a value.

  • Property procedures can return and assign values, and set references to objects.

For More Information Property procedures are discussed in "Programming with Objects."

To learn more about Sub and Function procedures, see the following topics:

  • Sub Procedures A discussion of Sub procedures and how to use them.
  • Function Procedures An introduction to Function procedures and how to use them.
  • Working with Procedures An introduction to calling procedures from within an application.
  • Passing Arguments to Procedures A discussion of passing data to procedures using arguments.


Sub Procedures

A Sub procedure is a block of code that is executed in response to an event. By breaking the code in a module into Sub procedures, it becomes much easier to find or modify the code in your application.

The syntax for a Sub procedure is:

[Private|Public][Static]Sub procedurename (arguments)
statements

End Sub

Each time the procedure is called, the statements between Sub and End Sub are executed. Sub procedures can be placed in standard modules, class modules, and form modules. Sub procedures are by default Public in all modules, which means they can be called from anywhere in the application.

The arguments for a procedure are like a variable declaration, declaring values that are passed in from the calling procedure.

In Visual Basic, it's useful to distinguish between two types of Sub procedures, general procedures and event procedures.

General Procedures

A general procedure tells the application how to perform a specific task. Once a general procedure is defined, it must be specifically invoked by the application. By contrast, an event procedure remains idle until called upon to respond to events caused by the user or triggered by the system.

Why create general procedures? One reason is that several different event procedures might need the same actions performed. A good programming strategy is to put common statements in a separate procedure (a general procedure) and have your event procedures call it. This eliminates the need to duplicate code and also makes the application easier to maintain. For example, the VCR sample application uses a general procedure called by the click events for several different scroll buttons. Figure 5.7 illustrates the use of a general procedure. Code in the Click events calls the ButtonManager Sub procedure, which runs its own code, and then returns control to the Click event procedure.

Figure 5.7 How general procedures are called by event procedures

Event Procedures

When an object in Visual Basic recognizes that an event has occurred, it automatically invokes the event procedure using the name corresponding to the event. Because the name establishes an association between the object and the code, event procedures are said to be attached to forms and controls.

  • An event procedure for a control combines the control's actual name (specified in the Name property), an underscore (_), and the event name. For instance, if you want a command button named cmdPlay to invoke an event procedure when it is clicked, use the procedure cmdPlay_Click.

  • An event procedure for a form combines the word "Form," an underscore, and the event name. If you want a form to invoke an event procedure when it is clicked, use the procedure Form_Click. (Like controls, forms do have unique names, but they are not used in the names of event procedures.) If you are using the MDI form, the event procedure combines the word "MDIForm," an underscore, and the event name, as in MDIForm_Load.

All event procedures use the same general syntax.

Syntax for a control event | Syntax for a form event
Private Sub controlname_eventname (arguments ) statements End Sub | Private Sub Form_eventname (arguments) statements End Sub

Although you can write event procedures from scratch, it's easier to use the code procedures provided by Visual Basic, which automatically include the correct procedure names. You can select a template in the Code Editor window by selecting an object from the Object box and then selecting a procedure from the Procedure box.

It's also a good idea to set the Name property of your controls before you start writing event procedures for them. If you change the name of a control after attaching a procedure to it, you must also change the name of the procedure to match the new name of the control. Otherwise, Visual Basic won't be able to match the control to the procedure. When a procedure name does not match a control name, it becomes a general procedure.

For More Information Visual Basic recognizes a variety of events for each kind of form and control. For explanations of all events, see the Language Reference.


Function Procedures

Visual Basic includes built-in, or intrinsic functions, like Sqr, Cos or Chr. In addition, you can use the Function statement to write your own Function procedures.

The syntax for a Function procedure is:

[Private|Public][Static]Function procedurename (arguments) [As type]
statements

End Function

Like a Sub procedure, a Function procedure is a separate procedure that can take arguments, perform a series of statements, and change the value of its arguments. Unlike a Sub procedure, a Function procedure can return a value to the calling procedure. There are three differences between Sub and Function procedures:

  • Generally, you call a function by including the function procedure name and arguments on the right side of a larger statement or expression (returnvalue = function()).

  • Function procedures have data types, just as variables do. This determines the type of the return value. (In the absence of an As clause, the type is the default Variant type.)

  • You return a value by assigning it to the procedurename itself. When the Function procedure returns a value, this value can then become part of a larger expression.

For example, you could write a function that calculates the third side, or hypotenuse, of a right triangle, given the values for the other two sides:

Function Hypotenuse (A As Integer, B As Integer) _
As String
   Hypotenuse = Sqr(A ^ 2 + B ^ 2)
End Function

You call a Function procedure the same way you call any of the built-in functions in Visual Basic:

Label1.Caption = Hypotenuse(CInt(Text1.Text), _
CInt(Text2.Text))
strX = Hypotenuse(Width, Height)

For More Information For additional details about the Function procedure, see "Function Statement" in the Language Reference. The techniques for calling all types of procedures are discussed in the section, "Calling Procedures," later in this chapter.


Working with Procedures

Creating New Procedures

To create a new general procedure

  • Type a procedure heading in the Code window and press ENTER. The procedure heading can be as simple as Sub or Function followed by a name. For example, you can enter either of the following:
    Sub UpdateForm ()
Function GetCoord ()

    Visual Basic responds by completing the template for the new procedure.

Selecting Existing Procedures

To view a procedure in the current module

  • To view an existing general procedure, select "(General)" from the Object box in the Code window, and then select the procedure in the Procedure box.

    –or–

    To view an event procedure, select the appropriate object from the Object box in the Code window, and then select the event in the Procedure box.

To view a procedure in another module

  1. From the View menu, choose Object Browser.

  2. Select the project from the Project/Library box.

  3. Select the module from the Classes list, and the procedure from the Members of list.

  4. Choose View Definition.

Calling Procedures

The techniques for calling procedures vary, depending on the type of procedure, where it's located, and how it's used in your application. The following sections describe how to call Sub and Function procedures.

Calling Sub Procedures

A Sub procedure differs from a Function procedure in that a Sub procedure cannot be called by using its name within an expression. A call to a Sub is a stand-alone statement. Also, a Sub does not return a value in its name as does a function. However, like a Function, a Sub can modify the values of any variables passed to it.

There are two ways to call a Sub procedure:

' Both of these statements call a Sub named MyProc.
Call MyProc (FirstArgument, SecondArgument)
MyProc FirstArgument, SecondArgument

Note that when you use the Call syntax, arguments must be enclosed in parentheses. If you omit the Call keyword, you must also omit the parentheses around the arguments.

Calling Function Procedures

Usually, you call a function procedure you've written yourself the same way you call an intrinsic Visual Basic function like Abs; that is, by using its name in an expression:

' All of the following statements would call a function
' named ToDec.
Print 10 * ToDec
X = ToDec
If ToDec = 10 Then Debug.Print "Out of Range"
X = AnotherFunction(10 * ToDec)

It's also possible to call a function just like you would call a Sub procedure. The following statements both call the same function:

Call Year(Now)
Year Now

When you call a function this way, Visual Basic throws away the return value.

Calling Procedures in Other Modules

Public procedures in other modules can be called from anywhere in the project. You might need to specify the module that contains the procedure you're calling. The techniques for doing this vary, depending on whether the procedure is located in a form, class, or standard module.

Procedures in Forms

All calls from outside the form module must point to the form module containing the procedure. If a procedure named SomeSub is in a form module called Form1, then you can call the procedure in Form1 by using this statement:

Call Form1.SomeSub(arguments)

Procedures in Class Modules

Like calling a procedure in a form, calling a procedure in a class module requires that the call to the procedure be qualified with a variable that points to an instance of the class. For example, DemoClass is an instance of a class named Class1:

Dim DemoClass as New Class1
DemoClass.SomeSub

However, unlike a form, the class name cannot be used as the qualifier when referencing an instance of the class. The instance of the class must be first be declared as an object variable (in this case, DemoClass) and referenced by the variable name.

For More Information You can find details on object variables and class modules in "Programming with Objects."

Procedures in Standard Modules

If a procedure name is unique, you don't need to include the module name in the call. A call from inside or outside the module will refer to that unique procedure. A procedure is unique if it appears only in one place.

If two or more modules contain a procedure with the same name, you may need to qualify it with the module name. A call to a common procedure from the same module runs the procedure in that module. For example, with a procedure named CommonName in Module1 and Module2, a call to CommonName from Module2 will run the CommonName procedure in Module2, not the CommonName procedure in Module1.

A call to a common procedure name from another module must specify the intended module. For example, if you want to call the CommonName procedure in Module2 from Module1, use:

Module2.CommonName(arguments)


Passing Arguments to Procedures

Usually the code in a procedure needs some information about the state of the program to do its job. This information consists of variables passed to the procedure when it is called. When a variable is passed to a procedure, it is called an argument.

Argument Data Types

The arguments for procedures you write have the Variant data type by default. However, you can declare other data types for arguments. For example, the following function accepts a string and an integer:

Function WhatsForLunch(WeekDay As String, Hour _
As Integer) As String
   ' Returns a lunch menu based on the day and time.
   If WeekDay = "Friday" then
      WhatsForLunch = "Fish"
   Else
      WhatsForLunch = "Chicken"
   End If
   If Hour > 4 Then WhatsForLunch = "Too late"
End Function

For More Information Details on Visual Basic data types are presented earlier in this chapter. You can also see the Language Reference for specific data types.

Passing Arguments By Value

Only a copy of a variable is passed when an argument is passed by value. If the procedure changes the value, the change affects only the copy and not the variable itself. Use the ByVal keyword to indicate an argument passed by value.

For example:

Sub PostAccounts(ByVal intAcctNum as Integer)
   .
   . ' Place statements here.
   .
End Sub

Passing Arguments By Reference

Passing arguments by reference gives the procedure access to the actual variable contents in its memory address location. As a result, the variable's value can be permanently changed by the procedure to which it is passed. Passing by reference is the default in Visual Basic.

If you specify a data type for an argument passed by reference, you must pass a value of that type for the argument. You can work around this by passing an expression, rather than a data type, for an argument. Visual Basic evaluates an expression and passes it as the required type if it can.

The simplest way to turn a variable into an expression is to enclose it in parentheses. For example, to pass a variable declared as an integer to a procedure expecting a string as an argument, you would do the following:

Sub CallingProcedure()
   Dim intX As Integer
   intX = 12 * 3
   Foo(intX)
End Sub

Sub Foo(Bar As String)
   MsgBox Bar   'The value of Bar is the string "36".
End Sub

Using Optional Arguments

You can specify arguments to a procedure as optional by placing the Optional keyword in the argument list. If you specify an optional argument, all subsequent arguments in the argument list must also be optional and declared with the Optional keyword. The two pieces of sample code below assume there is a form with a command button and list box.

For example, this code provides all optional arguments:

Dim strName As String
Dim strAddress As String

Sub ListText(Optional x As String, Optional y _
As String)
   List1.AddItem x
   List1.AddItem y
End Sub

Private Sub Command1_Click()
   strName = "yourname"
   strAddress = 12345   ' Both arguments are provided.
   Call ListText(strName, strAddress)
End Sub

This code, however, does not provide all optional arguments:

Dim strName As String
Dim varAddress As Variant

Sub ListText(x As String, Optional y As Variant)
   List1.AddItem x
   If Not IsMissing(y) Then
      List1.AddItem y
   End If
End Sub

Private Sub Command1_Click()
   strName = "yourname"   ' Second argument is not
                        ' provided.
   Call ListText(strName)
End Sub

In the case where an optional argument is not provided, the argument is actually assigned as a variant with the value of Empty. The example above shows how to test for missing optional arguments using the IsMissing function.

Providing a Default for an Optional Argument

It's also possible to specify a default value for an optional argument. The following example returns a default value if the optional argument isn't passed to the function procedure:

Sub ListText(x As String, Optional y As _
Integer = 12345)
   List1.AddItem x
   List1.AddItem y
End Sub

Private Sub Command1_Click()
   strName = "yourname"   ' Second argument is not
                        ' provided.
   Call ListText(strName)   ' Adds "yourname" and
                           ' "12345".
End Sub

Using an Indefinite Number of Arguments

Generally, the number of arguments in the procedure call must be the same as in the procedure specification. Using the ParamArray keyword allows you to specify that a procedure will accept an arbitrary number of arguments. This allows you to write functions like Sum:

Dim x As Integer
Dim y As Integer
Dim intSum As Integer

Sub Sum(ParamArray intNums())
   For Each x In intNums
      y = y + x
   Next x
   intSum = y
End Sub

Private Sub Command1_Click()
   Sum 1, 3, 5, 7, 8
   List1.AddItem intSum
End Sub

Creating Simpler Statements with Named Arguments

For many built-in functions, statements, and methods, Visual Basic provides the option of using named arguments as a shortcut for typing argument values. With named arguments, you can provide any or all of the arguments, in any order, by assigning a value to the named argument. You do this by typing the argument name plus a colon followed by an equal sign and the value ( MyArgument:= "SomeValue") and placing that assignment in any sequence delimited by commas. Notice that the arguments in the following example are in the reverse order of the expected arguments:

Function ListText(strName As String, Optional strAddress As String)
   List1.AddItem strName
   List2.AddItem strAddress
End Sub

Private Sub Command1_Click()
   ListText strAddress:=”12345”, strName:="Your Name"
End Sub

This is especially useful if your procedures have several optional arguments that you do not always need to specify.

Determining Support for Named Arguments

To determine which functions, statements, and methods support named arguments, use the AutoQuickInfo feature in the Code window, check the Object Browser, or see the Language Reference. Consider the following when working with named arguments:

  • Named arguments are not supported by methods on objects in the Visual Basic (VB) object library. They are supported by all language keywords in the Visual Basic for applications (VBA) object library.

  • In syntax, named arguments are shown as bold and italic. All other arguments are shown in italic only.

Important You cannot use named arguments to avoid entering required arguments. You can omit only the optional arguments. For Visual Basic (VB) and Visual Basic for applications (VBA) object libraries, the Object Browser encloses optional arguments with square brackets [ ].

For More Information See "ByVal," "ByRef," "Optional," and "ParamArray" in the Language Reference.


Introduction to Control Structures

Control structures allow you to control the flow of your program's execution. If left unchecked by control-flow statements, a program's logic will flow through statements from left to right, and top to bottom. While some very simple programs can be written with only this unidirectional flow, and while some flow can be controlled by using operators to regulate precedence of operations, most of the power and utility of any programming language comes from its ability to change statement order with structures and loops.

To learn more about specific control structures, see the following topics:

  • Decision Structures An introduction to decision structures used for branching.
  • Loop Structures An introduction to loop structures used to repeat processes.
  • Working with Control Structures The basics of working with control structures in your code.


Decision Structures

Visual Basic procedures can test conditions and then, depending on the results of that test, perform different operations. The decision structures that Visual Basic supports include:

  • If...Then

  • If...Then...Else

  • Select Case

If...Then

Use an If...Then structure to execute one or more statements conditionally. You can use either a single-line syntax or a multiple-line block syntax:

If condition Then statement

If condition Then
statements

End If

The condition is usually a comparison, but it can be any expression that evaluates to a numeric value. Visual Basic interprets this value as True or False; a zero numeric value is False, and any nonzero numeric value is considered True. If condition is True, Visual Basic executes all the statements following the Then keyword. You can use either single-line or multiple-line syntax to execute just one statement conditionally (these two examples are equivalent):

If anyDate < Now Then anyDate = Now

If anyDate < Now Then
   anyDate = Now
End If

Notice that the single-line form of If...Then does not use an End If statement. If you want to execute more than one line of code when condition is True, you must use the multiple-line block If...Then...End If syntax.

If anyDate < Now Then
   anyDate = Now
   Timer1.Enabled = False      ' Disable timer control.
End If

If...Then...Else

Use an If...Then...Else block to define several blocks of statements, one of which will execute:

If condition1 Then
[statementblock-1]
[ElseIf condition2 Then
[statementblock-2]] ...
[Else
[statementblock-n]]

End If

Visual Basic first tests condition1. If it's False, Visual Basic proceeds to test condition2, and so on, until it finds a True condition. When it finds a True condition, Visual Basic executes the corresponding statement block and then executes the code following the End If. As an option, you can include an Else statement block, which Visual Basic executes if none of the conditions are True.

If...Then…ElseIf is really just a special case of If...Then...Else. Notice that you can have any number of ElseIf clauses, or none at all. You can include an Else clause regardless of whether you have ElseIf clauses.

For example, your application could perform different actions depending on which control in a menu control array was clicked:

Private Sub mnuCut_Click (Index As Integer)
   If Index = 0 Then            ' Cut command.
      CopyActiveControl         ' Call general procedures.
      ClearActiveControl
   ElseIf Index = 1 Then      ' Copy command.
      CopyActiveControl
   ElseIf Index = 2 Then      ' Clear command.
      ClearActiveControl
   Else                        ' Paste command.
      PasteActiveControl
   End If
End Sub

Notice that you can always add more ElseIf parts to your If...Then structure. However, this syntax can get tedious to write when each ElseIf compares the same expression to a different value. For this situation, you can use a Select Case decision structure.

For More Information See "If...Then...Else Statement" in the Language Reference.

Select Case

Visual Basic provides the Select Case structure as an alternative to If...Then...Else for selectively executing one block of statements from among multiple blocks of statements. A Select Case statement provides capability similar to the If...Then...Else statement, but it makes code more readable when there are several choices.

A Select Case structure works with a single test expression that is evaluated once, at the top of the structure. Visual Basic then compares the result of this expression with the values for each Case in the structure. If there is a match, it executes the block of statements associated with that Case:

Select Case testexpression
[Case expressionlist1
[statementblock-1]]
[Case expressionlist2
[statementblock-2]]
.
.
.
[Case Else
[statementblock-n]]

End Select

Each expressionlist is a list of one or more values. If there is more than one value in a single list, the values are separated by commas. Each statementblock contains zero or more statements. If more than one Case matches the test expression, only the statement block associated with the first matching Case will execute. Visual Basic executes statements in the Case Else clause (which is optional) if none of the values in the expression lists matches the test expression.

For example, suppose you added another command to the Edit menu in the If...Then...Else example. You could add another ElseIf clause, or you could write the function with Select Case:

Private Sub mnuCut_Click (Index As Integer)
   Select Case Index
      Case 0                  ' Cut command.
         CopyActiveControl      ' Call general procedures.
         ClearActiveControl
      Case 1                  ' Copy command.
         CopyActiveControl
      Case 2                  ' Clear command.
         ClearActiveControl
      Case 3                  ' Paste command.
         PasteActiveControl
      Case Else
         frmFind.Show         ' Show Find dialog box.
   End Select
End Sub

Notice that the Select Case structure evaluates an expression once at the top of the structure. In contrast, the If...Then...Else structure can evaluate a different expression for each ElseIf statement. You can replace an If...Then...Else structure with a Select Case structure only if the If statement and each ElseIf statement evaluates the same expression.


Loop Structures

Loop structures allow you to execute one or more lines of code repetitively. The loop structures that Visual Basic supports include:

  • Do...Loop

  • For...Next

  • For Each...Next

Do...Loop

Use a Do loop to execute a block of statements an indefinite number of times. There are several variations of the Do...Loop statement, but each evaluates a numeric condition to determine whether to continue execution. As with If...Then, the condition must be a value or expression that evaluates to False (zero) or to True (nonzero).

In the following Do...Loop, the statements execute as long as the condition is True:

Do While condition
statements

Loop

When Visual Basic executes this Do loop, it first tests condition. If condition is False (zero), it skips past all the statements. If it's True (nonzero), Visual Basic executes the statements and then goes back to the Do While statement and tests the condition again.

Consequently, the loop can execute any number of times, as long as condition is nonzero or True. The statements never execute if condition is initially False. For example, this procedure counts the occurrences of a target string within another string by looping as long as the target string is found:

Function CountStrings (longstring, target)
   Dim position, count
   position = 1
   Do While InStr(position, longstring, target)
      position = InStr(position, longstring, target)_
       + 1
      count = count + 1
   Loop
   CountStrings = count
End Function

If the target string doesn't occur in the other string, then InStr returns 0, and the loop doesn't execute.

Another variation of the Do...Loop statement executes the statements first and then tests condition after each execution. This variation guarantees at least one execution of statements:

Do
statements

Loop While condition

Two other variations are analogous to the previous two, except that they loop as long as condition is False rather than True.

Loop zero or more times | Loop at least once
Do Until condition statements Loop | Do statements Loop Until condition

For...Next

Do loops work well when you don't know how many times you need to execute the statements in the loop. When you know you must execute the statements a specific number of times, however, a For…Next loop is a better choice. Unlike a Do loop, a For loop uses a variable called a counter that increases or decreases in value during each repetition of the loop. The syntax is:

For counter = start To end [Step increment]
statements

Next [counter]

The arguments counter, start, end, and increment are all numeric.

Note The increment argument can be either positive or negative. If increment is positive, start must be less than or equal to end or the statements in the loop will not execute. If increment is negative, start must be greater than or equal to end for the body of the loop to execute. If Step isn't set, then increment defaults to 1.

In executing the For loop, Visual Basic:

  1. Sets counter equal to start.

  2. Tests to see if counter is greater than end. If so, Visual Basic exits the loop.

    (If increment is negative, Visual Basic tests to see if counter is less than end.)

  3. Executes the statements.

  4. Increments counter by 1 or by increment, if it's specified.

  5. Repeats steps 2 through 4.

This code prints the names of all the available Screen fonts:

Private Sub Form_Click ()
   Dim I As Integer
   For i = 0 To Screen.FontCount
      Print Screen.Fonts(i)
   Next
End Sub

In the VCR sample application, the HighlightButton procedure uses a For...Next loop to step through the controls collection of the VCR form and show the appropriate Shape control:

Sub HighlightButton(MyControl As Variant)
   Dim i As Integer
   For i = 0 To frmVCR.Controls.Count - 1
      If TypeOf frmVCR.Controls(i) Is Shape Then
         If frmVCR.Controls(i).Name = MyControl Then
            frmVCR.Controls(i).Visible = True
         Else
            frmVCR.Controls(i).Visible = False
         End If
      End If
   Next
End Sub

For Each...Next

A For Each...Next loop is similar to a For...Next loop, but it repeats a group of statements for each element in a collection of objects or in an array instead of repeating the statements a specified number of times. This is especially helpful if you don't know how many elements are in a collection.

Here is the syntax for the For Each...Next loop:

For Each element In group
statements

Next element

For example, the following Sub procedure opens Biblio.mdb and adds the name of each table to a list box.

Sub ListTableDefs()
   Dim objDb As Database
   Dim MyTableDef as TableDef
   Set objDb = OpenDatabase("c:\vb\biblio.mdb", _
   True, False)
   For Each MyTableDef In objDb.TableDefs()
      List1.AddItem MyTableDef.Name
   Next MyTableDef
End Sub

Keep the following restrictions in mind when using For Each...Next:

  • For collections, element can only be a Variant variable, a generic Object variable, or an object listed in the Object Browser.

  • For arrays, element can only be a Variant variable.

  • You cannot use For Each...Next with an array of user-defined types because a Variant cannot contain a user-defined type.


Working with Control Structures

Nested Control Structures

You can place control structures inside other control structures (such as an If...Then block within a For...Next loop). A control structure placed inside another control structure is said to be nested.

Control structures in Visual Basic can be nested to as many levels as you want. It's common practice to make nested decision structures and loop structures more readable by indenting the body of the decision structure or loop.

For example, this procedure prints all the font names that are common to both the Printer and Screen:

Private Sub Form_Click()
   Dim SFont, PFont
   For Each SFont In Screen.Fonts()
      For Each PFont In Printer.Fonts()
         If SFont = PFont Then
            Print SFont
         End If
      Next PFont
   Next SFont
End Sub

Notice that the first Next closes the inner For loop and the last For closes the outer For loop. Likewise, in nested If statements, the End If statements automatically apply to the nearest prior If statement. Nested Do...Loop structures work in a similar fashion, with the innermost Loop statement matching the innermost Do statement.

Exiting a Control Structure

The Exit statement allows you to exit directly from a For loop, Do loop, Sub procedure, or Function procedure. The syntax for the Exit statement is simple: Exit For can appear as many times as needed inside a For loop, and Exit Do can appear as many times as needed inside a Do loop:

For counter = start To end [Step increment]
[statementblock]
[Exit For]
[statementblock]

Next [counter[, counter] [,...]]

Do [{While | Until} condition]
[statementblock]
[Exit Do]
[statementblock]

Loop

The Exit Do statement works with all versions of the Do loop syntax.

Exit For and Exit Do are useful because sometimes it's appropriate to quit a loop immediately, without performing any further iterations or statements within the loop. For example, in the previous example that printed the fonts common to both the Screen and Printer, the code continues to compare Printer fonts against a given Screen font even when a match has already been found with an earlier Printer font. A more efficient version of the function would exit the loop as soon as a match is found:

Private Sub Form_Click()
   Dim SFont, PFont
   For Each SFont In Screen.Fonts()
      For Each PFont In Printer.Fonts()
         If SFont = PFont Then
            Print Sfont
            Exit For            ' Exit inner loop.
         End If
      Next PFont
   Next SFont
End Sub

As this example illustrates, an Exit statement almost always appears inside an If statement or Select Case statement nested inside the loop.

When you use an Exit statement to break out of a loop, the value of the counter variable differs, depending on how you leave the loop:

  • When you complete a loop, the counter variable contains the value of the upper bound plus the step.

  • When you exit a loop prematurely, the counter variable retains its value subject to the usual rules on scope.

  • When you iterate off the end of a collection, the counter variable contains Nothing if it's an Object data type, and contains Empty if it's a Variant data type.

Exiting a Sub or Function Procedure

You can also exit a procedure from within a control structure. The syntax of Exit Sub and Exit Function is similar to that of Exit For and Exit Do in the previous section, "Exiting a Control Structure." Exit Sub can appear as many times as needed, anywhere within the body of a Sub procedure. Exit Function can appear as many times as needed, anywhere within the body of a Function procedure.

Exit Sub and Exit Function are useful when the procedure has done everything it needs to do and can return immediately. For example, if you want to change the previous example so it prints only the first common Printer and Screen font it finds, you would use Exit Sub:

Private Sub Form_Click()
   Dim SFont, PFont
   For Each SFont In Screen.Fonts()
      For Each PFont In Printer.Fonts()
         If SFont = PFont Then
            Print Sfont
            Exit Sub            ' Exit the procedure.
         End If
      Next PFont
   Next SFont
End Sub


Working with Objects

When you create an application in Visual Basic, you work with objects. You can use objects provided by Visual Basic — such as controls, forms, and data access objects. You can also control other applications' objects from within your Visual Basic application. You can even create your own objects, and define additional properties and methods for them.

The following topics discuss objects in detail:

  • What is an Object? An introduction to the concept of objects.
  • What Can You Do with Objects? A discussion of some ways that objects can be used in an application.
  • The Basics of Working with Objects An introduction to the properties and methods exposed by objects.
  • How are Objects Related to Each Other? A discussion of object hierarchies, collections, and containers.
  • Creating Objects A discussion of how objects can be created and used at run time.


What is an Object?

An object is a combination of code and data that can be treated as a unit. An object can be a piece of an application, like a control or a form. An entire application can also be an object. The following table describes examples of the types of objects you can use in Visual Basic.

Example | Description
Command button | Controls on a form, such as command buttons and frames, are objects.
Form | Each form in a Visual Basic project is a separate object.
Database | Databases are objects, and contain other objects, like fields and indexes.
Chart | A chart in Microsoft Excel is an object.

Where do Objects Come From?

Each object in Visual Basic is defined by a class. To understand the relationship between an object and its class, think of cookie cutters and cookies. The cookie cutter is the class. It defines the characteristics of each cookie — for instance, size and shape. The class is used to create objects. The objects are the cookies.

Two examples of the relationship between classes and objects in Visual Basic may make this clearer.

  • The controls on the Toolbox in Visual Basic represent classes. The object known as a control doesn't exist until you draw it on a form. When you create a control, you're creating a copy or instance of the control class. That instance of the class is the object you refer to in your application.

  • The form you work with at design time is a class. At run time, Visual Basic creates an instance of the form's class.

The Properties window displays the class and Name property of objects in your Visual Basic application, as shown in Figure 5.8.

Figure 5.8 Object and class names shown in the Properties window

All objects are created as identical copies of their class. Once they exist as individual objects, their properties can be changed. For example, if you draw three command buttons on a form, each command button object is an instance of the CommandButton class. Each object shares a common set of characteristics and capabilities (properties, methods, and events), defined by the class. However, each has its own name, can be separately enabled and disabled, can be placed in a different location on the form, and so on.

For simplicity, most of the material outside of this chapter won't make many references to an object's class. Just remember that the term "list box control," for example, means "an instance of the ListBox class."


What Can You Do with Objects?

An object provides code you don't have to write. For example, you could create your own File Open and File Save dialog boxes, but you don't have to. Instead, you can use the common dialog control (an object) provided by Visual Basic. You could write your own scheduling and resource management code, but you don't have to. Instead, you can use the Calendar, Resources, and Task objects provided by Microsoft Project.

Visual Basic Can Combine Objects from Other Sources

Visual Basic provides the tools to allow you to combine objects from different sources. You can now build custom solutions combining the most powerful features of Visual Basic and applications that support Automation (formerly known as OLE Automation). Automation is a feature of the Component Object Model (COM), an industry standard used by applications to expose objects to development tools and other applications.

You can build applications by tying together intrinsic Visual Basic controls, and you can also use objects provided by other applications. Consider placing these objects on a Visual Basic form:

  • A Microsoft Excel Chart object

  • A Microsoft Excel Worksheet object

  • A Microsoft Word Document object

You could use these objects to create a checkbook application like the one shown in Figure 5.9. This saves you time because you don't have to write the code to reproduce the functionality provided by the Microsoft Excel and Word objects.

Figure 5.9 Using objects from other applications


The Basics of Working with Objects

Visual Basic objects support properties, methods, and events. In Visual Basic, an object's data (settings or attributes) are called properties, while the various procedures that can operate on the object are called its methods. An event is an action recognized by an object, such as clicking a mouse or pressing a key, and you can write code to respond to that event.

You can change an object's characteristics by changing its properties. Consider a radio: One property of a radio is its volume. In Visual Basic, you might say that a radio has a "Volume" property that you can adjust by changing its value. Assume you can set the volume of the radio from 0 to 10. If you could control a radio with Visual Basic, you might write code in a procedure that changes the value of the "Volume" property from 3 to 5 to make the radio play louder:

Radio.Volume = 5

In addition to properties, objects have methods. Methods are a part of objects just as properties are. Generally, methods are actions you want to perform, while properties are the attributes you set or retrieve. For example, you dial a telephone to make a call. You might say that telephones have a "Dial" method, and you could use this syntax to dial the seven-digit number 5551111:

Phone.Dial 5551111

Objects also have events. Events are triggered when some aspect of the object is changed. For example, a radio might have a "VolumeChange" event. A telephone might have a "Ring" event.

Controlling Objects with Their Properties

Individual properties vary as to when you can set or get their values. Some properties can be set at design time. You can use the Properties window to set the value of these properties without writing any code at all. Some properties are not available at design time; therefore, you must write code to set those properties at run time.

Properties that you can set and get at run time are called read-write properties. Properties you can only read at run time are called read-only properties.

Setting Property Values

You set the value of a property when you want to change the appearance or behavior of an object. For example, you change the Text property of a text box control to change the contents of the text box.

To set the value of a property, use the following syntax:

object.property = expression

The following statements demonstrate how you set properties:

Text1.Top = 200   ' Sets the Top property to 200 twips.
Text1.Visible = True            ' Displays the text box.
Text1.Text = "hello"   ' Displays 'hello' in the text
                     ' box.

Getting Property Values

You get the value of a property when you want to find the state of an object before your code performs additional actions (such as assigning the value to another object). For example, you can return the Text property of a text box control to determine the contents of the text box before running code that might change the value.

In most cases, to get the value of a property, you use the following syntax:

variable = object.property

You can also get a property value as part of a more complex expression, without assigning the property to a variable. In the following code example, the Top property of the new member of a control array is calculated as the Top property of the previous member, plus 400:

Private Sub cmdAdd_Click()
   ' [statements]
   optButton(n).Top = optButton(n-1).Top + 400
   ' [statements]
End Sub

Tip If you're going to use the value of a property more than once, your code will run faster if you store the value in a variable.

Performing Actions with Methods

Methods can affect the values of properties. For example, in the radio analogy, the SetVolume method changes the Volume property. Similarly, in Visual Basic, list boxes have a List property, which can be changed with the Clear and AddItem methods.

Using Methods in Code

When you use a method in code, how you write the statement depends on how many arguments the method requires, and whether the method returns a value. When a method doesn't take arguments, you write the code using the following syntax:

object.method

In this example, the Refresh method repaints the picture box:

Picture1.Refresh   ' Forces a repaint of the control.

Some methods, such as the Refresh method, don't have arguments and don't return values.

If the method takes more than one argument, you separate the arguments with a comma. For example, the Circle method uses arguments specifying the location, radius, and color of a circle on a form:

' Draw a blue circle with a 1200-twip radius.
Form1.Circle (1600, 1800), 1200, vbBlue

If you keep the return value of a method, you must enclose the arguments in parentheses. For example, the GetData method returns a picture from the Clipboard:

Picture = Clipboard.GetData (vbCFBitmap)

If there is no return value, the arguments appear without parentheses. For example, the AddItem method doesn't return a value:

List1.AddItem "yourname"      ' Adds the text 'yourname'
                           ' to a list box.

For More Information See the Language Reference for the syntax and arguments for all methods provided by Visual Basic.


How are Objects Related to Each Other?

When you put two command buttons on a form, they are separate objects with distinct Name property settings (Command1 and Command2), but they share the same class — CommandButton.

They also share the characteristic that they're on the same form. You've seen earlier in this chapter that a control on a form is also contained by the form. This puts controls in a hierarchy. To reference a control you may have to reference the form first, in the same way you may have to dial a country code or area code before you can reach a particular phone number.

The two command buttons also share the characteristic that they're controls. All controls have common characteristics that make them different from forms and other objects in the Visual Basic environment. The following sections explain how Visual Basic uses collections to group objects that are related.

Object Hierarchies

An object hierarchy provides the organization that determines how objects are related to each other, and how you can access them. In most cases, you don't need to concern yourself with the Visual Basic object hierarchy. However:

  • When manipulating another application's objects, you should be familiar with that application's object hierarchy. For information on navigating object hierarchies, see "Programming with Components."

  • When working with data access objects, you should be familiar with the Data Access Object hierarchy.

There are some common cases in Visual Basic where one object contains others. These are described in the following sections.

Working with Collections of Objects

Collection objects have their own properties and methods. The objects in a collection object are referred to as members of the collection. Each member of the collection is numbered sequentially beginning at 0; this is the member's index number. For example, the Controls collection contains all the controls on a given form, as shown in Figure 5.10. You can use collections to simplify code if you need to perform the same operation on all the objects in a collection.

Figure 5.10 Controls collection

For example, the following code scrolls through the Controls collection and lists each member's name in a list box.

Dim MyControl as Control
For Each MyControl In Form1.Controls
   ' For each control, add its name to a list box.
   List1.AddItem MyControl.Name
Next MyControl

Applying Properties and Methods to Collection Members

There are two general techniques you can use to address a member of a collection object:

  • Specify the name of the member. The following expressions are equivalent:
    Controls("List1")
Controls!List1
  • Use the index number of the member:
    Controls(3)

Once you're able to address all the members collectively, and single members individually, you can apply properties and methods using either approach:

' Set the Top property of the list box control to 200.
Controls!List1.Top = 200

–or–

Dim MyControl as Control
For Each MyControl In Form1.Controls()
   ' Set the Top property of each member to 200.
   MyControl.Top = 200
Next MyControl

Objects That Contain Other Objects

Some objects in Visual Basic contain other objects. For example, a form usually contains one or more controls. The advantage of having objects as containers for other objects is that you can refer to the container in your code to make it clear which object you want to use. For example, Figure 5.11 illustrates two different forms you could have in an application — one for entering accounts payable transactions, and the other for entering accounts receivable transactions.

Figure 5.11 Two different forms can contain controls that have the same name

Both forms can have a list box named lstAcctNo. You can specify exactly which one you want to use by referring to the form containing the list box:

frmReceivable.lstAcctNo.AddItem 1201

–or–

frmPayable.lstAcctNo.AddItem 1201

Common Collections in Visual Basic

There are some common cases in Visual Basic where one object contains other objects. The following table briefly describes the most commonly used collections in Visual Basic.

Collection | Description
Forms | Contains loaded forms.
Controls | Contains controls on a form.
Printers | Contains the available Printer objects.

You can also implement object containment in Visual Basic.

For More Information "For information about object containment, see "Using Collections" in "More About Programming." For information on the Printers collection, see "Working with Text and Graphics." For details on the forms and controls collections, see the Language Reference.

The Container Property

You can use the Container property to change an object's container within a form. The following controls can contain other controls:

  • Frame control

  • Picture box control

  • Toolbar control (Professional and Enterprise editions only)

This example demonstrates moving a command button around from container to container on a form. Open a new project, and draw a frame control, picture box control and a command button on the form.

The following code in the form's click event increments a counter variable, and uses a Select Case loop to rotate the command button from container to container.

Private Sub Form_Click()
   Static intX as Integer
   Select Case intX
      Case 0
      Set Command1.Container = Picture1
      Command1.Top= 0
      Command1.Left= 0

      Case 1
      Set Command1.Container = Frame1
      Command1.Top= 0
      Command1.Left= 0

      Case 2
      Set Command1.Container = Form1
      Command1.Top= 0
      Command1.Left= 0

   End Select
   intX = intX + 1
End Sub

For More Information See "Container Property" in the Language Reference.

Communicating Between Objects

In addition to using and creating objects within Visual Basic, you can communicate with other applications and manipulate their objects from within your application. The ability to share data between applications is one of the key features of the Microsoft Windows operating system. With Visual Basic, you have great flexibility in how you can communicate with other applications.

For More Information For details on using and communicating with other applications' objects, see "Programming with Components."


Creating Objects

The easiest way to create an object is to double-click a control in the Toolbox. However, to realize the full benefit of all the objects available in Visual Basic and from other applications, you can use Visual Basic's programmability features to create objects at run time.

  • You can create references to an object with object variables.

  • You can create your own objects "from scratch" with class modules.

  • You can create your own collections with the Collection object.

For More Information Other chapters show you how to access objects. The CreateObject and GetObject functions, for example, are discussed in "Programming with Components."

Using Object Variables

In addition to storing values, a variable can refer to an object. You assign an object to a variable for the same reasons you assign any value to a variable:

  • Variable names are often shorter and easier to remember than the values they contain (or, in this case, the objects they refer to).

  • Variables can be changed to refer to other objects while your code is running.

  • Referring to a variable that contains an object is more efficient than repeatedly referring to the object itself.

Using an object variable is similar to using a conventional variable, but with one additional step — assigning an object to the variable:

  • First you declare it:

    Dim variable As class

  • Then you assign an object to it:

    Set variable = object

Declaring Object Variables

You declare an object variable in the same way you declare other variables, with Dim, ReDim, Static, Private, or Public. The only differences are the optional New keyword and the class argument; both of these are discussed later in this chapter. The syntax is:

{Dim | ReDim | Static | Private | Public} variable As [New] class

For example, you can declare an object variable that refers to a form in the application called frmMain:

Dim FormVar As New frmMain   ' Declare an object
                           ' variable of type frmMain.

You can also declare an object variable that can refer to any form in the application:

Dim anyForm As Form         ' Generic form variable.

Similarly, you can declare an object variable that can refer to any text box in your application:

Dim anyText As TextBox      ' Can refer to any text box
                           ' (but only a text box).

You can also declare an object variable that can refer to a control of any type:

Dim anyControl As Control   ' Generic control variable.

Notice that you can declare a form variable that refers to a specific form in the application, but you cannot declare a control variable that refers to a particular control. You can declare a control variable that can refer to a specific type of control (such as TextBox or ListBox), but not to one particular control of that type (such as txtEntry or List1). However, you can assign a particular control to a variable of that type. For example, for a form with a list box called lstSample, you could write:

Dim objDemo As ListBox
Set objDemo = lstSample

Assigning Object Variables

You assign an object to an object variable with the Set statement:

Set variable = object

Use the Set statement whenever you want an object variable to refer to an object.

Sometimes you may use object variables, and particularly control variables, simply to shorten the code you have to type. For example, you might write code like this:

If frmAccountDisplay!txtAccountBalance.Text < 0 Then
   frmAccountDisplay!txtAccountBalance.BackColor = 0   frmAccountDisplay!txtAccountBalance.ForeColor = 255
End If

You can shorten this code significantly if you use a control variable:

Dim Bal As TextBox
Set Bal = frmAccountDisplay!txtAccountBalance
If Bal.Text < 0 Then
   Bal.BackColor = 0
   Bal.ForeColor = 255
End If

Specific and Generic Object Types

Specific object variables must refer to one specific type of object or class. A specific form variable can refer to only one form in the application (though it can refer to one of many instances of that form). Similarly, a specific control variable can refer to only one particular type of control in your application, such as TextBox or ListBox. To see an example, open a new project and place a text box on a form. Add the following code to the form:

Private Sub Form_Click()
   Dim anyText As TextBox
   Set anyText = Text1
   anyText.Text = "Hello"
End Sub

Run the application, and click the form. The Text property of the text box will be changed to "Hello."

Generic object variables can refer to one of many specific types of objects. A generic form variable, for example, can refer to any form in an application; a generic control variable can refer to any control on any form in an application. To see an example, open a new project and place several frame, label, and command button controls on a form, in any order. Add the following code to the form:

Private Sub Form_Click()
   Dim anyControl As Control
   Set anyControl = Form1.Controls(3)
   anyControl.Caption = "Hello"
End Sub

Run the application, and click the form. The caption of the control you placed third in sequence on the form will be changed to "Hello."

There are four generic object types in Visual Basic:

Generic Object Type | Object referenced
Form | Any form in the application (including MDI children and the MDI form).
Control | Any control in your application.
MDIForm | The MDI form in the application (if your application has one).
Object | Any object.

Generic object variables are useful when you don't know the specific type of object a variable will refer to at run time. For example, if you want to write code that can operate on any form in the application, you must use a generic form variable.

Note Because there can be only one MDI form in the application, there is no need to use the generic MDIForm type. Instead, you can use the specific MDIForm type (MDIForm1, or whatever you specified for the Name property of the MDI form) whenever you need to declare a form variable that refers to the MDI form. In fact, because Visual Basic can resolve references to properties and methods of specific form types before you run your application, you should always use the specific MDIForm type.

The generic MDIForm type is provided only for completeness; should a future version of Visual Basic allow multiple MDI forms in a single application, it might become useful.

Forms as Objects

Forms are most often used to make up the interface of an application, but they're also objects that can be called by other modules in your application. Forms are closely related to class modules. The major difference between the two is that forms can be visible objects, whereas class modules have no visible interface.

Adding Custom Methods and Properties

You can add custom methods and properties to forms and access them from other modules in your application. To create a new method for a form, add a procedure declared using Public.

' Custom method on Form1
Public Sub LateJobsCount()
   .
. ' <statements>
   .
End Sub

You can call the LateJobsCount procedure from another module using this statement:

Form1.LateJobsCount

Creating a new property for a form can be as simple as declaring a public variable in the form module:

Public IDNumber As Integer

You can set and return the value of IDNumber on Form1 from another module using these two statements:

Form1.IDNumber = 3
Text1.Text = Form1.IDNumber

You can also use Property procedures to add custom properties to a form.

For More Information Details on Property procedures are provided in "Programming with Objects."

Note You can call a variable, a custom method, or set a custom property on a form without loading the form. This allows you to run code on a form without loading it into memory. Also, referencing a control without referencing one of its properties or methods does not load the form.

Using the New Keyword

Use the New keyword to create a new object as defined by its class. New can be used to create instances of forms, classes defined in class modules, and collections.

Using the New Keyword with Forms

Each form you create at design time is a class. The New keyword can be used to create new instances of that class. To see how this works, draw a command button and several other controls on a form. Set the form's Name property to Sample in the Properties window. Add the following code to your command button's Click event procedure:

Dim x As New Sample
x.Show

Run the application, and click the command button several times. Move the front-most form aside. Because a form is a class with a visible interface, you can see the additional copies. Each form has the same controls, in the same positions as on the form at design time.

Note To make a form variable and an instance of the loaded form persist, use a Static or Public variable instead of a local variable.

You can also use New with the Set statement. Try the following code in a command button's Click event procedure:

Dim f As Form1
Set f = New Form1
f.Caption = "hello"
f.Show

Using New with the Set statement is faster and is the recommended method.

Using the New Keyword with Other Objects

The New keyword can be used to create collections and objects from the classes you define in class modules. To see how this works, try the following example.

This example demonstrates how the New keyword creates instances of a class. Open a new project, and draw a command button on Form1. From the Project menu, choose Add Class Module to add a class module to the project. Set the class module's Name property to ShowMe.

The following code in the Form1 module creates a new instance of the class ShowMe, and calls the procedure contained in the class module.

Public clsNew As ShowMe
Private Sub Command1_Click()
   Set clsNew = New ShowMe
   clsNew.ShowFrm
End Sub

The ShowFrm procedure in the class module creates a new instance of the class Form1, shows the form, and then minimizes it.

Sub ShowFrm()
   Dim frmNew As Form1
   Set frmNew = New Form1
   frmNew.Show
   frmNew.WindowState = 1
End Sub

To use the example, run the application, and click the command button several times. You'll see a minimized form icon appear on your desktop as each new instance of the ShowMe class is created.

For More Information For information on using New to create objects, see "Programming with Components."

New Keyword Restrictions

The following table describes what you cannot do with the New keyword.

You can't use New to create | Example of code not allowed
Variables of fundamental data types. | Dim X As New Integer
A variable of any generic object type. | Dim X As New Control
A variable of any specific control type. | Dim X As New ListBox
A variable of any specific control. | Dim X As New lstNames

Freeing References to Objects

Each object uses memory and system resources. It is good programming practice to release these resources when you are no longer using an object.

  • Use Unload to unload a form or control from memory.

  • Use Nothing to release resources used by an object variable. Assign Nothing to an object variable with the Set statement.

For More Information See "Unload Event" and "Nothing" in the Language Reference.

Passing Objects to Procedures

You can pass objects to procedures in Visual Basic. In the following code example, it's assumed that there is a command button on a form:

Private Sub Command1_Click()
   ' Calls the Demo sub, and passes the form to it.
   Demo Form1
End Sub

Private Sub Demo(x As Form1)
   ' Centers the form on the screen.
   x.Left = (Screen.Width - x.Width) / 2
End Sub

It's also possible to pass an object to an argument by reference and then, inside the procedure, set the argument to a new object. To see how this works, open a project, and insert a second form. Place a picture box control on each form. The following table shows the property settings that need changes:

Object | Property | Setting
Picture box on Form2 | Name Picture | Picture2 c:\vb\icons\arrows\arw01dn.ico

The Form1_Click event procedure calls the GetPicture procedure in Form2, and passes the empty picture box to it.

Private Sub Form_Click()
Form2.GetPicture Picture1
End Sub

The GetPicture procedure in Form2 assigns the Picture property of the picture box on Form2 to the empty picture box on Form1.

Private objX As PictureBox
Public Sub GetPicture(x As PictureBox)
   ' Assign the passed-in picture box to an object
   ' variable.
   Set objX = x
   ' Assign the value of the Picture property to Form1
   ' picture box.
   objX.Picture = picture2.Picture
End Sub

To use the example, run the application, and click Form1. You'll see the icon from Form2 appear in the picture box on Form1.

For More Information The previous topics are intended to serve as an introduction to objects. To learn more, see "Programming with Objects" and "Programming with Components."


What Can You Do With Visual Basic?

Once you understand the basics of working with Visual Basic, you’re ready to move on to new and bigger challenges. So, what can you do with Visual Basic? It might be more appropriate to ask what can’t be done. The answer is: not much! From designing innovative user interfaces to taking advantage of other application’s objects, from manipulating text and graphics to working with databases, Visual Basic provides the tools that you’ll need to get the job done right.

Part 1 of the Programmer's Guide provided the foundation; Part 2 provides the bricks and mortar to build upon that foundation to create increasingly complex applications with Visual Basic.

Chapters

Creating a User Interface

In-depth coverage of interface styles, forms, menus, toolbars, and more.
Using Visual Basic's Standard Controls

Covers Visual Basic's intrinsic controls and how you can use them.
More About Programming

A discussion of more programming concepts, techniques, and additional tools available in Visual Basic.
Programming with Objects

In-depth coverage of objects, including creating classes and using ActiveX Designers.
Programming with Components

Covers techniques for using ActiveX components in your own applications.
Responding to Mouse and Keyboard Events

A discussion of processing user input, including drag and drop.
Working with Text and Graphics

Explains how to use Visual Basic’s text and graphics methods for display and printing.
Debugging Your Code and Handling Errors

Explains what to do when something doesn’t works as planned, either at design time or run time.
Processing Drives, Folders, and Files

Techniques for working with the file system.
Designing for Performance and Compatibility

Optimization techniques to make your applications faster or smaller.
International Issues

A discussion of considerations for multilingual and multicultural applications.
Distributing Your Application

Explains how to use the Package and Deployment Wizard to create installation programs.


Creating a User Interface

The user interface is perhaps the most important part of an application; it's certainly the most visible. To users, the interface is the application; they probably aren't aware of the code that is executing behind the scenes. No matter how much time and effort you put into writing and optimizing your code, the usability of your application depends on the interface.

When you design an application, a number of decisions need to be made regarding the interface. Should you use the single-document or multiple-document style? How many different forms will you need? What commands will your menus include, and will you use toolbars to duplicate menu functions? What about dialog boxes to interact with the user? How much assistance do you need to provide?

Before you begin designing the user interface, you need to think about the purpose of the application. The design for a primary application that will be in constant use should be different from one that is only used occasionally for short periods of time. An application with the primary purpose of displaying information has different requirements than one used to gather information.

The intended audience should also influence your design. An application aimed at a beginning user demands simplicity in its design, while one for experienced users may be more complex. Other applications used by your target audience may influence their expectations for an application's behavior. If you plan on distributing internationally, language and culture must be considered part of your design.

Designing a user interface is best approached as an iterative process — you will rarely come up with a perfect design on the first pass. This chapter introduces you to the process of designing an interface in Visual Basic, providing an introduction to the tools you need to create a great application for your users.

Topics

Interface Styles

A discussion of MDI, SDI, and other interface styles.
Multiple-Document Interface (MDI) Applications

Techniques for designing MDI applications.
More About Forms

Information about the advanced features of forms.
Using Menus in Your Application

In-depth coverage of menus, including pop-up menus.
Toolbars

How to add toolbars to your application.
Dialog Boxes

How to use dialog and message boxes to interact with users.
Designing for Different Display Types

A discussion of display considerations.
Designing with the User in Mind

A discussion of user-focused design and design techniques.

Sample applications

Mdinote.vpb, Sdinote.vbp

Many of the code samples in this chapter are taken from the Mdinote.vbp and Sdinote.vbp sample applications which are listed in the Samples directory.


Interface Styles

If you've been using Windows-based applications for a while, you've probably noticed that not all user interfaces look or behave the same. There are two main styles of user interface: the single-document interface (SDI) and the multiple-document interface (MDI). An example of the SDI interface is the WordPad application included with Microsoft Windows (Figure 6.1). In WordPad, only a single document may be open; you must close one document in order to open another.

Figure 6.1 WordPad, a single-document interface (SDI) application

Applications such as Microsoft Excel and Microsoft Word for Windows are MDI interfaces; they allow you to display multiple documents at the same time, with each document displayed in its own window (Figure 6.2). You can recognize a MDI application by the inclusion of a Window menu item with submenus for switching between windows or documents.

Figure 6.2 Microsoft Excel, a multiple-document interface (MDI) application

In determining which interface style is best, you need to look at the purpose of the application. An application for processing insurance claims might lend itself to the MDI style — a clerk is likely to be working on more than one claim at a time or might need to compare two claims. On the other hand, a calendar application would be best suited to the SDI style — it's not likely that you would need more than one calendar open at a time; in the rare event that you did, you could open a second instance of the SDI application.

The SDI style is the more common; most of the examples in the Programmer's Guide assume an SDI application. There are a number of considerations and techniques unique to creating MDI applications, which are addressed in "Multiple-Document Interface (MDI) Applications" later in this chapter.

In addition to the two most common interface styles, SDI and MDI, a third interface style is becoming more popular: the explorer-style interface (Figure 6.3). The explorer-style interface is a single window containing two panes or regions, usually consisting of a tree or hierarchical view on the left and a display area on the right, as in the Microsoft Windows Explorer. This type of interface lends itself to navigating or browsing large numbers of documents, pictures, or files.

Figure 6.3 The Windows Explorer, an explorer-style interface

In addition to the MDI and SDI application examples that accompany this chapter, the Application Wizard provides a good way to compare the different interface styles. You can use the Wizard to generate a framework for each style and view the forms and code that it generates.

For More Information To learn more about MDI applications, see "Multiple-Document Interface (MDI) Applications." The basics of working with forms are covered in "Forms, Controls, and Menus." For information on accessing the Application Wizard, see "Using Wizards and Add-Ins" in "Managing Projects."


Multiple-Document Interface (MDI) Applications

The multiple-document interface (MDI) allows you to create an application that maintains multiple forms within a single container form. Applications such as Microsoft Excel and Microsoft Word for Windows have multiple-document interfaces.

An MDI application allows the user to display multiple documents at the same time, with each document displayed in its own window. Documents or child windows are contained in a parent window, which provides a workspace for all the child windows in the application. For example, Microsoft Excel allows you to create and display multiple-document windows of different types. Each individual window is confined to the area of the Excel parent window. When you minimize Excel, all of the document windows are minimized as well; only the parent window's icon appears in the task bar.

A child form is an ordinary form that has its MDIChild property set to True. Your application can include many MDI child forms of similar or different types.

At run time, child forms are displayed within the workspace of the MDI parent form (the area inside the form's borders and below the title and menu bars). When a child form is minimized, its icon appears within the workspace of the MDI form instead of on the taskbar, as shown in Figure 6.4.

Figure 6.4 Child forms displayed within the workspace of the MDI form

Note Your application can also include standard, non-MDI forms that are not contained in the MDI form. A typical use of a standard form in an MDI application is to display a modal dialog box.

An MDI form is similar to an ordinary form with one restriction. You can't place a control directly on a MDI form unless that control has an Align property (such as a picture box control) or has no visible interface (such as a timer control).

Creating an MDI Application

Use the following procedure to create an MDI form and its child forms.

To create an MDI application

  1. Create an MDI form.

    From the Project menu, choose Add MDI Form.

    Note An application can have only one MDI form. If a project already has an MDI form, the Add MDI Form command on the Project menu is unavailable.

  2. Create the application's child forms.

    To create an MDI child form, create a new form (or open an existing one) and set its MDIChild property to True.

To learn more about MDI applications, see the following topics:

  • Working with MDI Child Forms at Design Time A discussion of the design time behavior of child forms.

  • Run-Time Features of MDI Forms A discussion of the run time behavior of child forms.

  • The MDI NotePad Application Details of the MDI Notepad sample application.

  • Working with MDI Forms and Child Forms A discussion of the interaction between MDI parent and child forms.


Working with MDI Child Forms at Design Time

At design time, child forms are not restricted to the area inside the MDI form. You can add controls, set properties, write code, and design the features of child forms just as you would with any other Visual Basic form.

You can determine whether a form is an MDI child by looking at its MDIChild property, or by examining the Project Explorer. If the form's MDIChild property is set to True, it is a child form. Visual Basic displays special icons in the Project Explorer for the MDI and MDI child forms, as shown in Figure 6.5.

Figure 6.5 Icons in the Project Explorer identify MDI child, standard, and MDI forms


Run-Time Features of MDI Forms

At run time, an MDI form and all of its child forms take on special characteristics:

  • All child forms are displayed within the MDI form's workspace. The user can move and size child forms like any other form; however, they are restricted to this workspace.

  • When a child form is minimized, its icon appears on the MDI form instead of the taskbar. When the MDI form is minimized, the MDI form and all of its child forms are represented by a single icon. When the MDI form is restored, the MDI form and all the child forms are displayed in the same state they were in before being minimized.

  • When a child form is maximized, its caption is combined with the caption of the MDI form and is displayed in the MDI form's title bar (see Figure 6.6).

  • By setting the AutoShowChildren property, you can display child forms automatically when forms are loaded (True), or load child forms as hidden (False).

  • The active child form's menus (if any) are displayed on the MDI form's menu bar, not on the child form.

    Figure 6.6 A child form caption combined with the caption of an MDI form


The MDI NotePad Application

The MDI NotePad sample application is a simple text editor similar to the NotePad application included with Microsoft Windows. The MDI NotePad application, however, uses a multiple-document interface (MDI). At run time, when the user requests a new document (implemented with the New command on the application's File menu), the application creates a new instance of the child form. This allows the user to create as many child forms, or documents, as necessary.

To create a document-centered application in Visual Basic, you need at least two forms — an MDI form and a child form. At design time, you create an MDI form to contain the application and a single child form to serve as a template for the application's document.

To create your own MDI NotePad application

  1. From the File menu, choose New Project.

  2. From the Project menu, choose Add MDI Form to create the container form.

    The project should now contain an MDI form (MDIForm1) and a standard form (Form1).

  3. Create a text box (Text1) on Form1.

  4. Set properties for the two forms and the text box as follows.
    Object | Property | Setting
    MDIForm1 | Caption | MDI NotePad
    Form1 Caption
    MDIChild     		Untitled
    True
    Text1 MultiLine
    Text
    Left
    Top     		True
    (Empty)
    0
    0

  5. Using the Menu Editor (from the Tools menu), create a File menu for MDIForm1.
    Caption | Name | Indented
    &File | mnuFile | No
    &New | mnuFileNew | Yes

  6. Add the following code to the mnuFileNew_Click procedure:
    Private Sub mnuFileNew_Click ()
' Create a new instance of Form1, called NewDoc.
Dim NewDoc As New Form1
' Display the new form.
NewDoc.Show
End Sub

    This procedure creates and then displays a new instance (or copy) of Form1, called NewDoc. Each time the user chooses New from the File menu, an exact duplicate (instance) of Form1 is created, including all the controls and code that it contains.

  7. Add the following code to the Form_Resize procedure for Form1:
    Private Sub Form_Resize ()
' Expand text box to fill the current child form.
Text1.Height = ScaleHeight
Text1.Width = ScaleWidth
End Sub

    The code for the Form_Resize event procedure, like all the code in Form1, is shared by each instance of Form1. When several copies of a form are displayed, each form recognizes its own events. When an event occurs, the code for that event procedure is called. Because the same code is shared by each instance, you might wonder how to reference the form that has called the code — especially since each instance has the same name (Form1). This is discussed in "Working with MDI Forms and Child Forms," later in this chapter.

  8. Press F5 to run the application.

Tip The Mdinote.vbp sample application contains examples of many MDI techniques besides those mentioned in this chapter. Take some time to step through the example code to discover these techniques. The Sdinote.vbp sample application is an implementation of the same application converted to the SDI style; compare the two samples to learn the differences between MDI and SDI techniques.


Working with MDI Forms and Child Forms

When users of your MDI application open, save, and close several child forms in one session, they should be able to refer to the active form and maintain state information on child forms. This topic describes coding techniques you can use to specify the active child form or control, load and unload MDI and child forms, and maintain state information for a child form.

Specifying the Active Child Form or Control

Sometimes you want to provide a command that operates on the control with the focus on the currently active child form. For example, suppose you want to copy selected text from the child form's text box onto the Clipboard. In the Mdinote.vbp sample application, the Click event of the Copy item on the Edit menu calls EditCopyProc, a procedure that copies selected text onto the Clipboard.

Because the application can have many instances of the same child form, EditCopyProc needs to know which form to use. To specify this, use the MDI form's ActiveForm property, which returns the child form that has the focus or that was most recently active.

Note At least one MDI child form must be loaded and visible when you access the ActiveForm property, or an error is returned.

When you have several controls on a form, you also need to specify which control is active. Like the ActiveForm property, the ActiveControl property returns the control with the focus on the active child form. Here's an example of a copy routine that can be called from a child form menu, a menu on the MDI form, or a toolbar button:

Private Sub EditCopyProc ()
   ' Copy selected text onto Clipboard.
   ClipBoard.SetText _
      frmMDI.ActiveForm.ActiveControl.SelText
End Sub

If you're writing code that will be called by multiple instances of a form, it's a good idea to not use a form identifier when accessing the form's controls or properties. For example, refer to the height of the text box on Form1 as Text1.Height instead of Form1.Text1.Height. This way, the code always affects the current form.

Another way to specify the current form in code is to use the Me keyword. You use Me to reference the form whose code is currently running. This keyword is useful when you need to pass a reference to the current form instance as an argument to a procedure.

For More Information For information on creating multiple instances of a form using the New keyword with the Dim statement, see "Introduction to Variables, Constants and Data Types" in "Programming Fundamentals" and "Dim Statement" in the Language Reference.

Loading MDI Forms and Child Forms

When you load a child form, its parent form (the MDI form) is automatically loaded and displayed. When you load the MDI form, however, its children are not automatically loaded.

In the MDI NotePad example, the child form is the default startup form, so both the child and MDI forms are loaded when the application is run. If you change the startup form in the MDI NotePad application to frmMDI (on the General tab of Project Properties) and then run the application, only the MDI form is loaded. The first child form is loaded when you choose New from the File menu.

You can use the AutoShowChildren property to load MDI child windows as hidden, and leave them hidden until you display them using the Show method. This allows you to update various details such as captions, position, and menus before a child form becomes visible.

You can't show an MDI child form or the MDI form modally (using the Show method with an argument of vbModal). If you want to use a modal dialog box in an MDI application, use a form with its MDIChild property set to False.

Setting Child Form Size and Position

When an MDI child form has a sizable border (BorderStyle = 2), Microsoft Windows determines its initial height, width, and position when it is loaded. The initial size and position of a child form with a sizable border depends on the size of the MDI form, not on the size of the child form at design time. When an MDI child form's border is not sizable (BorderStyle = 0, 1, or 3), it is loaded using its design-time Height and Width properties.

If you set AutoShowChildren to False, you can change the position of the MDI child after you load it, but before you make it visible.

For More Information See "AutoShowChildren Property and "Show Method" in the Language Reference.

Maintaining State Information for a Child Form

A user deciding to quit the MDI application must have the opportunity to save work. To make this possible, the application needs to be able to determine, at all times, whether the data in the child form has changed since the last time it was saved.

You can do this by declaring a public variable on each child form. For example, you can declare a variable in the Declarations section of a child form:

Public boolDirty As Boolean

Each time the text changes in Text1, the child form's text box Change event sets boolDirty to True. You can add this code to indicate that the contents of Text1 have changed since the last time it was saved:

Private Sub Text1_Change ()
   boolDirty = True
End Sub

Conversely, for each time the user saves the contents of the child form, the text box's Change event sets boolDirty to False to indicate that the contents of Text1 no longer need to be saved. In the following code, it is assumed that there is a menu command called Save (mnuFileSave) and a procedure called FileSave that saves the contents of the text box:

Sub mnuFileSave_Click ()
   ' Save the contents of Text1.
   FileSave
   ' Set the state variable.
   boolDirty = False
End Sub

Unloading MDI Forms with QueryUnload

The boolDirty flag becomes useful when the user decides to exit the application. This can occur when the user chooses Close from the MDI form's Control menu, or through a menu item you provide, such as Exit on the File menu. If the user closes the application using the MDI form's Control menu, Visual Basic will attempt to unload the MDI form.

When an MDI form is unloaded, the QueryUnload event is invoked first for the MDI form and then for every child form that is open. If none of the code in these QueryUnload event procedures cancels the Unload event, then each child is unloaded and finally, the MDI form is unloaded.

Because the QueryUnload event is invoked before a form is unloaded, you can give the user the opportunity to save a form before unloading it. The following code uses the boolDirty flag to determine if the user should be prompted to save the child before it is unloaded. Notice that you can access the value of a public form-level variable anywhere in the project. This code assumes that there is a procedure, FileSave, that saves the contents of Text1 in a file.

Private Sub mnuFExit_Click()
   ' When the user chooses File Exit in an MDI
   ' application, unload the MDI form, invoke
   ' the QueryUnload event for each open child.
   Unload frmMDI
   End
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, _
   UnloadMode As Integer)
   If boolDirty Then
      ' Call routine to query the user and save
      ' file if necessary.
      FileSave
   End If
End Sub

For More Information See "QueryUnload Event" in the Language Reference.


More About Forms

In addition to the basics of form design, you need to think about the beginning and end of your application. There are several techniques available for determining what the user will see when your application starts. It's also important to be aware of the processes that occur when an application is unloaded.

Setting the Startup Form

By default, the first form in your application is designated as the startup form. When your application starts running, this form is displayed (so the first code to execute is the code in the Form_Initialize event for that form). If you want a different form to display when your application starts, you must change the startup form.

To change the startup form

  1. From the Project menu, choose Project Properties.

  2. Choose the General tab.

  3. In the Startup Object list box, select the form you want as the new startup form.

  4. Choose OK.

Starting Without a Startup Form

Sometimes you might want your application to start without any form initially loaded. For example, you might want to execute code that loads a data file and then displays one of several different forms depending on what is in the data file. You can do this by creating a Sub procedure called Main in a standard module, as in the following example:

Sub Main()
   Dim intStatus As Integer
   ' Call a function procedure to check user status.
   intStatus = GetUserStatus
   ' Show a startup form based on status.
   If intStatus = 1 Then
      frmMain.Show
   Else
      frmPassword.Show
   End If

This procedure must be a Sub procedure, and it cannot be in a form module. To set the Sub Main procedure as the startup object, from the Project menu, choose Project Properties, select the General tab, and select Sub Main from the Startup Object box.

Displaying a Splash Screen on Startup

If you need to execute a lengthy procedure on startup, such as loading a large amount of data from a database or loading several large bitmaps, you might want to display a splash screen on startup. A splash screen is a form, usually displaying information such as the name of the application, copyright information, and a simple bitmap. The screen that is displayed when you start Visual Basic is a splash screen.

To display a splash screen, use a Sub Main procedure as your startup object and use the Show method to display the form:

Private Sub Main()
   ' Show the splash screen.
   frmSplash.Show
   ' Add your startup procedures here.
   …
   ' Show the main form and unload the splash screen.
   frmMain.Show
   Unload frmSplash
End Sub

The splash screen occupies the user's attention while your startup routines are executing, giving the illusion that the application is loading faster. When the startup routines are completed, you can load your first form and unload the splash screen.

In designing a splash screen, it's a good idea to keep it simple. If you use large bitmaps or a lot of controls, the splash screen itself may be slow to load.

Ending an Application

An event-driven application stops running when all its forms are closed and no code is executing. If a hidden form still exists when the last visible form is closed, your application will appear to have ended (because no forms are visible), but will in fact continue to run until all the hidden forms are closed. This situation can arise because any access to an unloaded form's properties or controls implicitly loads that form without displaying it.

The best way to avoid this problem when closing your application is to make sure all your forms are unloaded. If you have more than one form, you can use the Forms collection and the Unload statement. For example, on your main form you could have a command button named cmdQuit that lets a user exit the program. If your application has only one form, the Click event procedure could be as simple as this:

Private Sub cmdQuit_Click ()
   Unload Me
End Sub

If your application uses multiple forms, you can unload the forms by putting code in the Unload event procedure of your main form. You can use the Forms collection to make sure you find and close all your forms. The following code uses the forms collection to unload all forms:

Private Sub Form_Unload (Cancel As Integer)
   Dim i as integer
   ' Loop through the forms collection and unload
   ' each form.
   For i = Forms.Count – 1 to 0 Step - 1
      Unload Forms(i)
   Next
End Sub

There may be cases where you need to end your application without regard for the state of any existing forms or objects. Visual Basic provides the End statement for this purpose.

The End statement ends an application immediately: no code after the End statement is executed, and no further events occur. In particular, Visual Basic will not execute the QueryUnload, Unload or Terminate event procedures for any forms. Object references will be freed, but if you have defined your own classes, Visual Basic will not execute the Terminate events of objects created from your classes.

In addition to the End statement, the Stop statement halts an application. However, you should use the Stop statement only while debugging, because it does not free references to objects.

For More Information For information on the Stop statement, see "Using Break Mode" in "Error Handling and Debugging," and "Stop Statement" in the Language Reference. For information on the forms collection or freeing references to objects, see "Programming with Objects."


Using Menus in Your Application

Many simple applications consist of one form and several controls, but you can enhance your Visual Basic applications by adding menus. This section shows you how to create menus and use them in an application.

  • Creating Menus with the Menu Editor How to use the Menu Editor.

  • Menu Title and Naming Guidelines A discussion of naming conventions.

  • Creating Submenus A discussion of submenus.

  • Creating a Menu Control Array Using arrays to eliminate redundant menu code.

  • Creating and Modifying Menus at Run Time Techniques for working with menus.

  • Writing Code for Menu Controls A discussion of coding techniques for menus.

  • Displaying Pop-up Menus Techniques for working with pop-up menus.

  • Menus in MDI Applications A discussion of MDI menu techniques.


Creating Menus with the Menu Editor

You can use the Menu Editor to create new menus and menu bars, add new commands to existing menus, replace existing menu commands with your own commands, and change and delete existing menus and menu bars.

To display the Menu Editor

  • From the Tools menu, choose Menu Editor.

    –or–

    Click the Menu Editor button on the toolbar.

This opens the Menu Editor, shown in Figure 6.7.

Figure 6.7 The Menu Editor

While most menu control properties can be set using the Menu Editor, all menu properties are available in the Properties window. The two most important properties for menu controls are:

  • Name — This is the name you use to reference the menu control from code.

  • Caption — This is the text that appears on the control.

Other properties in the Menu Editor, including Index, Checked, and NegotiatePosition, are described later in this chapter.

Using the List Box in the Menu Editor

The menu control list box (the lower portion of the Menu Editor) lists all the menu controls for the current form. When you type a menu item in the Caption text box, that item also appears in the menu control list box. Selecting an existing menu control from the list box allows you to edit the properties for that control.

For example, Figure 6.7 shows the menu controls for a File menu in a typical application. The position of the menu control in the menu control list box determines whether the control is a menu title, menu item, submenu title, or submenu item:

  • A menu control that appears flush left in the list box is displayed on the menu bar as a menu title.

  • A menu control that is indented once in the list box is displayed on the menu when the user clicks the preceding menu title.

  • An indented menu control followed by menu controls that are further indented becomes a submenu title. Menu controls indented below the submenu title become items of that submenu.

  • A menu control with a hyphen (-) as its Caption property setting appears as a separator bar. A separator bar divides menu items into logical groups.

Note A menu control cannot be a separator bar if it is a menu title, has submenu items, is checked or disabled, or has a shortcut key.

To create menu controls in the Menu Editor

  1. Select the form.

  2. From the Tools menu, choose Menu Editor.

    –or–

    Click the Menu Editor button on the toolbar.

  3. In the Caption text box, type the text for the first menu title that you want to appear on the menu bar. Also, place an ampersand (&) before the letter you want to be the access key for that menu item. This letter will automatically be underlined in the menu.

    The menu title text is displayed in the menu control list box.

  4. In the Name text box, type the name that you will use to refer to the menu control in code. See "Menu Title and Naming Guidelines" later in this chapter.

  5. Click the left arrow or right arrow buttons to change the indentation level of the control.

  6. Set other properties for the control, if you choose. You can do this in the Menu Editor or later, in the Properties window.

  7. Choose Next to create another menu control.

    –or–

    Click Insert to add a menu control between existing controls.

    You can also click the up arrow and down arrow buttons to move the control among the existing menu controls.

  8. Choose OK to close the Menu Editor when you have created all the menu controls for that form.

    The menu titles you create are displayed on the form. At design time, click a menu title to drop down its corresponding menu items.

Separating Menu Items

A separator bar is displayed as a horizontal line between items on a menu. On a menu with many items, you can use a separator bar to divide items into logical groups. For example, the File menu in Visual Basic uses separator bars to divide its menu items into three groups, as shown in Figure 6.8.

Figure 6.8 Separator bars

To create a separator bar in the Menu Editor

  1. If you are adding a separator bar to an existing menu, choose Insert to insert a menu control between the menu items you want to separate.

  2. If necessary, click the right arrow button to indent the new menu item to the same level as the menu items it will separate.

  3. Type a hyphen (-) in the Caption text box.

  4. Set the Name property.

  5. Choose OK to close the Menu Editor.

Note Although separator bars are created as menu controls, they do not respond to the Click event, and users cannot choose them.

Assigning Access Keys and Shortcut Keys

You can improve keyboard access to menu commands by defining access keys and shortcut keys.

Access Keys

Access keys allow the user to open a menu by pressing the ALT key and typing a designated letter. Once a menu is open, the user can choose a control by pressing the letter (the access key) assigned to it. For example, ALT+E might open the Edit menu, and P might select the Paste menu item. An access-key assignment appears as an underlined letter in the menu control's caption, as shown in Figure 6.9.

Figure 6.9 Access keys

To assign an access key to a menu control in the Menu Editor

  1. Select the menu item to which you want to assign an access key.

  2. In the Caption box, type an ampersand (&) immediately in front of the letter you want to be the access key.

For example, if the Edit menu shown in Figure 6.9 is open, the following Caption property settings respond to the corresponding keys.

Menu control caption | Caption property | Access keys
Cut | Cu&t | t
Copy | C&opy | o
Paste Delete Select All Time/Date | &Paste De&lete Select &All Time/&Date | p l a d

Note Do not use duplicate access keys on menus. If you use the same access key for more than one menu item, the key will not work. For example, if C is the access key for both Cut and Copy, when you select the Edit menu and press C, the Copy command will be selected, but the application will not carry out the command until the user presses ENTER. The Cut command will not be selected at all.

Shortcut Keys

Shortcut keys run a menu item immediately when pressed. Frequently used menu items may be assigned a keyboard shortcut, which provides a single-step method of keyboard access, rather than a three-step method of pressing ALT, a menu title access character, and then a menu item access character. Shortcut key assignments include function key and control key combinations, such as CTRL+F1 or CTRL+A. They appear on the menu to the right of the corresponding menu item, as shown in Figure 6.10.

Figure 6.10 Shortcut keys

To assign a shortcut key to a menu item

  1. Open the Menu Editor.

  2. Select the menu item.

  3. Select a function key or key combination in the Shortcut combo box.

    To remove a shortcut key assignment, choose "(none)" from the top of the list.

Note Shortcut keys appear automatically on the menu; therefore, you do not have to enter CTRL+key in the Caption box of the Menu Editor.


Menu Title and Naming Guidelines

To maintain consistency with other applications, it's a good idea to follow established naming guidelines when creating menus.

Setting the Caption Property

When assigning captions for menu items, you should try to follow these guidelines:

  • Item names should be unique within a menu, but may be repeated in different menus to represent similar actions.

  • Item names may be single, compound, or multiple words.

  • Each item name should have a unique mnemonic access character for users who choose commands with keyboards. The access character should be the first letter of the menu title, unless another letter offers a stronger mnemonic link; no two menu titles should use the same access character. For more information about assigning access and shortcut keys, see "Creating Menus with the Menu Editor" earlier in this chapter.

  • An ellipsis (…) should follow names of commands that require more information before they can be completed, such as commands that display a dialog (Save As…, Preferences…).

  • Keep the item names short. If you are localizing your application, the length of words tends to increase approximately thirty percent in foreign versions, and you may not have enough space to adequately list all of your menu items. For more details on localizing your application, see "International Issues."

Menu Naming Conventions

To make your code more readable and easier to maintain, it's a good idea to follow established naming conventions when setting the Name property in the Menu Editor. Most naming convention guidelines suggest a prefix to identify the object (that is, mnu for a menu control) followed by the name of the top-level menu (for example, File). For submenus, this would be followed by the caption of the submenu (for example, mnuFileOpen).

For More Information For an example of suggested naming conventions, see "Visual Basic Coding Conventions."


Creating Submenus

Each menu you create can include up to five levels of submenus. A submenu branches off another menu to display its own menu items. You may want to use a submenu when:

  • The menu bar is full.

  • A particular menu control is seldom used.

  • You want to emphasize one menu control's relationship to another.

If there is room on the menu bar, however, it's better to create an additional menu title instead of a submenu. That way, all the controls are visible to the user when the menu is dropped down. It's also good programming practice to restrict the use of submenus so users don't get lost trying to navigate your application's menu interface. (Most applications use only one level of submenus.)

In the Menu Editor, any menu control indented below a menu control that is not a menu title is a submenu control. In general, submenu controls can include submenu items, separator bars, and submenu titles.

To create a submenu

  1. Create the menu item that you want to be the submenu title.

  2. Create the items that will appear on the new submenu, and indent them by clicking the right arrow button.

    Each indent level is preceded by four dots (....) in the Menu Editor. To remove one level of indentation, click the left arrow button.

Note If you're considering using more than a single level of submenus, think about using a dialog box instead. Dialog boxes allow users to specify several choices in one place. For information on using dialog boxes, see "Dialog Boxes" later in this chapter.


Creating a Menu Control Array

A menu control array is a set of menu items on the same menu that share the same name and event procedures. Use a menu control array to:

  • Create a new menu item at run time when it must be a member of a control array. The MDI Notepad sample, for example, uses a menu control array to store a list of recently opened files.

  • Simplify code, because common blocks of code can be used for all menu items.

Each menu control array element is identified by a unique index value, indicated in the Index property box on the Menu Editor. When a member of a control array recognizes an event, Visual Basic passes its Index property value to the event procedure as an additional argument. Your event procedure must include code to check the value of the Index property, so you can determine which control you're using.

For More Information For more information on control arrays, see "Working with Control Arrays" in "Using Visual Basic's Standard Controls."

To create a menu control array in the Menu Editor

  1. Select the form.

  2. From the Tools menu, choose Menu Editor.

    –or–

    Click the Menu Editor button on the toolbar.

  3. In the Caption text box, type the text for the first menu title that you want to appear on the menu bar.

    The menu title text is displayed in the menu control list box.

  4. In the Name text box, type the name that you will use to refer to the menu control in code. Leave the Index box empty.

  5. At the next indentation level, create the menu item that will become the first element in the array by setting its Caption and Name.

  6. Set the Index for the first element in the array to 0.

  7. Create a second menu item at the same level of indentation as the first.

  8. Set the Name of the second element to the same as the first element and set its Index to 1.

  9. Repeat steps 5 – 8 for subsequent elements of the array.

Important Elements of a menu control array must be contiguous in the menu control list box and must be at the same level of indentation. When you're creating menu control arrays, be sure to include any separator bars that appear on the menu.


Creating and Modifying Menus at Run Time

The menus you create at design time can also respond dynamically to run-time conditions. For example, if a menu item action becomes inappropriate at some point, you can prevent users from selecting that menu item by disabling it. In the MDI NotePad application, for example, if the clipboard doesn't contain any text, the Paste menu item is dimmed on the Edit menu, and users cannot select it.

You can also dynamically add menu items, if you have a menu control array. This is described in "Adding Menu Controls at Run Time," later in this topic.

You can also program your application to use a check mark to indicate which of several commands was last selected. For example, the Options, Toolbar menu item from the MDI NotePad application displays a check mark if the toolbar is displayed. Other menu control features described in this section include code that makes a menu item visible or invisible and that adds or deletes menu items.

Enabling and Disabling Menu Commands

All menu controls have an Enabled property, and when this property is set to False, the menu is disabled and does not respond to user actions. Shortcut key access is also disabled when Enabled is set to False. A disabled menu control appears dimmed, like the Paste menu item in Figure 6.11.

Figure 6.11 A disabled menu item

For example, this statement disables the Paste menu item on the Edit menu of the MDI NotePad application:

mnuEditPaste.Enabled = False

Disabling a menu title in effect disables the entire menu, because the user cannot access any menu item without first clicking the menu title. For example, the following code would disable the Edit menu of the MDI Notepad application:

mnuEdit.Enabled = False

Displaying a Check Mark on a Menu Control

Using the Checked property, you can place a check mark on a menu to:

  • Tell the user the status of an on/off condition. Choosing the menu command alternately adds and removes the check mark.

  • Indicate which of several modes is in effect. The Options menu of the MDI Notepad application uses a check mark to indicate the state of the toolbar, as shown in Figure 6.12.

Figure 6.12 A checked menu item

You create check marks in Visual Basic with the Checked property. Set the initial value of the Checked property in the Menu Editor by selecting the check box labeled Checked. To add or remove a check mark from a menu control at run time, set its Checked property from code. For example:

Private Sub mnuOptions_Click ()
   ' Set the state of the check mark based on
   ' the Visible property.
   mnuOptionsToolbar.Checked = picToolbar.Visible
End Sub

Making Menu Controls Invisible

In the Menu Editor, you set the initial value of the Visible property for a menu control by selecting the check box labeled Visible. To make a menu control visible or invisible at run time, set its Visible property from code. For example:

mnuFileArray(0).Visible = True   ' Make the control
                                 ' visible.

mnuFileArray(0).Visible = False   ' Make the control
                                 ' invisible.

When a menu control is invisible, the rest of the controls in the menu move up to fill the empty space. If the control is on the menu bar, the rest of the controls on the menu bar move left to fill the space.

Note Making a menu control invisible effectively disables it, because the control is inaccessible from the menu, access or shortcut keys. If the menu title is invisible, all the controls on that menu are unavailable.

Adding Menu Controls at Run Time

A menu can grow at run time. In Figure 6.13, for example, as files are opened in the SDI NotePad application, menu items are dynamically created to display the path names of the most recently opened files.

Figure 6.13 Menu control array elements created and displayed at run time

You must use a control array to create a control at run time. Because the mnuRecentFile menu control is assigned a value for the Index property at design time, it automatically becomes an element of a control array — even though no other elements have yet been created.

When you create mnuRecentFile(0), you actually create a separator bar that is invisible at run time. The first time a user saves a file at run time, the separator bar becomes visible, and the first file name is added to the menu. Each time you save a file at run time, additional menu controls are loaded into the array, making the menu grow.

Controls created at run time can be hidden by using the Hide method or by setting the control's Visible property to False. If you want to remove a control in a control array from memory, use the Unload statement.


Writing Code for Menu Controls

When the user chooses a menu control, a Click event occurs. You need to write a Click event procedure in code for each menu control. All menu controls except separator bars (and disabled or invisible menu controls) recognize the Click event.

The code that you write in a menu event procedure is no different than that which you would write in any other control's event procedure. For example, the code in a File, Close menu's Click event might look like this:

Sub mnuFileClose_Click()
   Unload Me
End Sub

Visual Basic displays a menu automatically when the menu title is chosen; therefore, it is not necessary to write code for a menu title's Click event procedure unless you want to perform another action, such as disabling certain menu items each time the menu is displayed.

Note At design time, the menus you create are displayed on the form when you close the Menu Editor. Choosing a menu item on the form displays the Click event procedure for that menu control.


Displaying Pop-up Menus

A pop-up menu is a floating menu that is displayed over a form, independent of the menu bar. The items displayed on the pop-up menu depend on where the pointer was located when the right mouse button was pressed; therefore, pop-up menus are also called context menus. In Microsoft Windows 95, you activate context menus by clicking the right mouse button.

Any menu that has at least one menu item can be displayed at run time as a pop-up menu. To display a pop-up menu, use the PopupMenu method. This method uses the following syntax:

[object.]PopupMenu menuname [, flags [,x [, y [, boldcommand ]]]]

For example, the following code displays a menu named mnuFile when the user clicks a form with the right mouse button. You can use the MouseUp or MouseDown event to detect when the user clicks the right mouse button, although the standard is to use the MouseUp event:

Private Sub Form_MouseUp (Button As Integer, Shift As _
   Integer, X As Single, Y As Single)
   If Button = 2 Then   ' Check if right mouse button
                        ' was clicked.
      PopupMenu mnuFile   ' Display the File menu as a
                        ' pop-up menu.
   End If
End Sub

Any code following a call to the PopupMenu method is not run until the user selects an item in the menu or cancels the menu.

Note Only one pop-up menu can be displayed at a time. While a pop-up menu is displayed, calls to the PopupMenu method are ignored. Calls to the PopupMenu method are also ignored whenever a menu control is active.

Often you want a pop-up menu to access options that are not usually available on the menu bar. To create a menu that will not display on the menu bar, make the top-level menu item invisible at design time (make sure the Visible check box in the Menu Editor is not checked). When Visual Basic displays a pop-up menu, the Visible property of the specified top-level menu is ignored.

The Flags Argument

You use the flags argument in the PopupMenu method to further define the location and behavior of a pop-up menu. The following table lists the flags available to describe a pop-up menu's location.

Location constants | Description
vbPopupMenuLeftAlign | Default. The specified x location defines the left edge of the pop-up menu.
vbPopupMenuCenterAlign | The pop-up menu is centered around the specified x location.
vbPopupMenuRightAlign | The specified x location defines the right edge of the pop-up menu.

The following table lists the flags available to describe a pop-up menu's behavior.

Behavior constants | Description
vbPopupMenuLeftButton | Default. The pop-up menu is displayed when the user clicks a menu item with the left mouse button only.
vbPopupMenuRightButton | The pop-up menu is displayed when the user clicks a menu item with either the right or left mouse button.

To specify a flag, you combine one constant from each group using the Or operator. The following code displays a pop-up menu with its top border centered on a form when the user clicks a command button. The pop-up menu triggers Click events for menu items that are clicked with either the right or left mouse button.

Private Sub Command1_Click ()
   ' Dimension X and Y variables.
   Dim xloc, yloc

   ' Set X and Y variables to center of form.
   xloc = ScaleWidth / 2
   yloc = ScaleHeight / 2

   ' Display the pop-up menu.
   PopupMenu mnuEdit, vbPopupMenuCenterAlign Or _
   vbPopupMenuRightButton, xloc, yloc
End Sub

The Boldcommand Argument

You use the boldcommand argument to specify the name of a menu control in the displayed pop-up menu that you want to appear in bold. Only one menu control in the pop-up menu can be bold.


Menus in MDI Applications

In an MDI application, the menus for each child are displayed on the MDI form, rather than on the child forms themselves. When a child form has the focus, that child's menu (if any) replaces the MDI form's menu on the menu bar. If there are no child forms visible, or if the child with the focus does not have a menu, the MDI form's menu is displayed (see Figures 6.14 and 6.15).

It is common for MDI applications to use several sets of menus. When the user opens a document, the application displays the menu associated with that type of document. Usually, a different menu is displayed when no child forms are visible. For example, when there are no files open, Microsoft Excel displays only the File and Help menus. When the user opens a file, other menus are displayed (File, Edit, View, Insert, Format, Tools, Data, Window, and so on).

Creating Menus for MDI Applications

You can create menus for your Visual Basic application by adding menu controls to the MDI form and to the child forms. One way to manage the menus in your MDI application is to place the menu controls you want displayed all of the time, even when no child forms are visible, on the MDI form. When you run the application, the MDI form's menu is automatically displayed when there are no child forms visible, as shown in Figure 6.14.

Figure 6.14 The MDI form menu is displayed when no child forms are loaded

Place the menu controls that apply to a child form on the child form. At run time, as long as there is at least one child form visible, these menu titles are displayed in the menu bar of the MDI form.

Some applications support more than one type of document. For example, in Microsoft Access, you can open tables, queries, forms, and other document types. To create an application such as this in Visual Basic, use two child forms. Design one child with menus that perform spreadsheet tasks and the other with menus that perform charting tasks.

At run time, when an instance of a spreadsheet form has the focus, the spreadsheet menu is displayed, and when the user selects a chart, that form's menu is displayed. If all the spreadsheets and charts are closed, the MDI form's menu is displayed. For more information on creating menus, see "Using Menus in Your Application" earlier in this chapter.

Creating a Window Menu

Most MDI applications (for example, Microsoft Word for Windows and Microsoft Excel) incorporate a Window menu. This is a special menu that displays the captions of all open child forms, as shown in Figure 6.15. In addition, some applications place commands on this menu that manipulate the child windows, such as Cascade, Tile, and Arrange Icons.

Figure 6.15 The Window menu displays the name of each open child form

Any menu control on an MDI form or MDI child form can be used to display the list of open child forms by setting the WindowList property for that menu control to True. At run time, Visual Basic automatically manages and displays the list of captions and displays a check mark next to the one that had the focus most recently. In addition, a separator bar is automatically placed above the list of windows.

To set the WindowList property

  1. Select the form where you want the menu to appear, and from the Tools menu, choose Menu Editor.

    Note The WindowList property applies only to MDI forms and MDI child forms. It has no effect on standard (non-MDI) forms.

  2. In the Menu Editor list box, select the menu where you want the list of open child forms to display.

  3. Select the WindowList check box.

At run time, this menu displays the list of open child forms. In addition, the WindowList property for this menu control returns as True.

For More Information See "WindowList Property" in the Language Reference.

Arranging Child Forms

As was mentioned earlier, some applications list actions such as Tile, Cascade, and Arrange Icons on a menu, along with the list of open child forms. Use the Arrange method to rearrange child forms in the MDI form. You can display child forms as cascading, as horizontally tiled, or as child form icons arranged along the lower portion of the MDI form. The following example shows the Click event procedures for the Cascade, Tile, and Arrange Icons menu controls.

Private Sub mnuWCascade_Click ()
   ' Cascade child forms.
   frmMDI.Arrange vbCascade
End Sub

Private Sub mnuWTile_Click ()
   ' Tile child forms (horizontal).
   frmMDI.Arrange vbTileHorizontal
End Sub

Private Sub mnuWArrange_Click ()
   ' Arrange all child form icons.
   frmMDI.Arrange vbArrangeIcons
End Sub

Note The intrinsic constants vbCascade, vbTileHorizontal, and vbArrangeIcons are listed in the Visual Basic (VB) object library of the Object Browser.

When you tile or cascade child forms that have a fixed border style, each child form is positioned as if it had a sizable border. This can cause child forms to overlap.


Toolbars

You can further enhance your application's menu interface with toolbars. Toolbars contain toolbar buttons, which provide quick access to the most frequently used commands in an application. For example, the Visual Basic toolbar contains toolbar buttons to perform commonly used commands, such as opening existing projects or saving the current project.

  • Creating a Toolbar Techniques for creating your own toolbars.

  • Negotiating Menu and Toolbar Appearance Working with toolbars and menus for insertable objects.


Creating a Toolbar

The toolbar (also called a ribbon or control bar) has become a standard feature in many Windows-based applications. A toolbar provides quick access to the most frequently used menu commands in an application. Creating a toolbar is easy and convenient using the toolbar control, which is available with the Professional and Enterprise editions of Visual Basic. If you are using the Learning Edition of Visual Basic, you can create toolbars manually as described in "Negotiating Menu and Toolbar Appearance" later in this chapter.

The following example demonstrates creating a toolbar for an MDI application; the procedure for creating a toolbar on a standard form is basically the same.

To manually create a toolbar

  1. Place a picture box on the MDI form.

    The width of the picture box automatically stretches to fill the width of the MDI form's workspace. The workspace is the area inside a form's borders, not including the title bar, menu bar, or any toolbars, status bars, or scroll bars that may be on the form.

    Note You can place only those controls that support the Align property directly on an MDI form (the picture box is the only standard control that supports this property).

  2. Inside the picture box, place any controls you want to display on the toolbar.

    Typically, you create buttons for the toolbar using command buttons or image controls. Figure 6.16 shows a toolbar containing image controls.

    To add a control inside a picture box, click the control button in the toolbox, and then draw it inside the picture box.

    Note When an MDI form contains a picture box, the internal area of the MDI form does not include the area of the picture box. For example, the ScaleHeight property of the MDI form returns the internal height of the MDI form, which does not include the height of the picture box.

    Figure 6.16 You can create buttons for the toolbar using image controls

  3. Set design-time properties.

    One advantage of using a toolbar is that you can present the user with a graphical representation of a command. The image control is a good choice as a toolbar button because you can use it to display a bitmap. Set its Picture property at design time to display a bitmap; this provides the user with a visual cue of the command performed when the button is clicked. You can also use ToolTips, which display the name of the toolbar button when a user rests the mouse pointer over a button, by setting the ToolTipText property for the button.

  4. Write code.

    Because toolbar buttons are frequently used to provide easy access to other commands, most of the time you call other procedures, such as a corresponding menu command, from within each button's Click event.

Tip You can use controls that are invisible at run time (such as the timer control) with an MDI form without displaying a toolbar. To do this, place a picture box on the MDI form, place the control in the picture box, and set the picture box's Visible property to False.

Writing Code for Toolbars

Toolbars are used to provide the user with a quick way to access some of the application's commands. For example, the first button on the toolbar in Figure 6.16 is a shortcut for the File New command. There are now three places in the MDI NotePad sample application where the user can request a new file:

  • On the MDI form (New on the MDI form File menu)

  • On the child form (New on the child form File menu)

  • On the toolbar (File New button)

Rather than duplicate this code three times, you can take the original code from the child form's mnuFileNew_Click event and place it in a public procedure in the child form. You can call this procedure from any of the preceding event procedures. Here's an example:

' This routine is in a public procedure.
Public Sub FileNew ()
   Dim frmNewPad As New frmNotePad
   frmNewPad.Show
End Sub

' The user chooses New on the child form File menu.
Private Sub mnuchildFileNew_Click ()
   FileNew
End Sub

' The user chooses New on the MDI form File menu.
Private Sub mnumdiFileNew_Click ()
   frmNotePad.FileNew
End Sub

' The user clicks the File New button on the toolbar.
Private Sub btnFileNew_Click ()
   frmNotePad.FileNew
End Sub


Negotiating Menu and Toolbar Appearance

When an object supplied by another application is activated on a form, there are a number of ways that object's menus and toolbars may appear on the container form; therefore, you need to specify how they will be displayed. This process is called user-interface negotiation because Visual Basic and the object you have linked or embedded must negotiate for space in the container form.

Controlling Menu Appearance

You can determine whether a linked or embedded object's menu will appear in the container form by setting a form's NegotiateMenus property. If the child form's NegotiateMenus property is set to True (default) and the container has a menu bar defined, the object's menus are placed on the container's menu bar when the object is activated. If the container has no menu bar, or the NegotiateMenus property is set to False, the object's menus will not appear when it is activated.

Note The NegotiateMenus property does not apply to MDI Forms.

Controlling Toolbar Appearance

The MDI form's NegotiateToolbars property determines whether the linked or embedded object's toolbars will be floating palettes or placed on the parent form. This behavior does not require toolbars to be present on the MDI parent form. If the MDI form's NegotiateToolbars property is True, the object's toolbar appears on the MDI parent form. If NegotiateToolbars is False, the object's toolbar will be a floating palette.

Note The NegotiateToolbars property applies only to MDI forms.

If an MDI form includes a toolbar, it is usually contained in a picture box control on the parent form. The picture box's Negotiate property determines whether the container's toolbar is still displayed or is replaced by the object's toolbar when activated. If Negotiate is True, the object's toolbar is displayed in addition to the container's toolbar. If Negotiate is False, the object's toolbar replaces the container's toolbar.

Note Menu and toolbar negotiation will occur only for insertable objects that support in-place activation. For more information on in-place activation, see "Programming with ActiveX Components."

You can see how these three properties interact by using the following procedure.

To perform menu and toolbar negotiation

  1. Add a toolbar to an MDI form. This is described in "Creating a Toolbar" earlier in this chapter.

  2. Place an insertable object on a child form.

  3. Set the NegotiateMenus, NegotiateToolbars, and Negotiate properties.

  4. Run the application, and double-click the object.


Dialogs

Dialog Boxes

In Windows-based applications, dialog boxes are used to:

  • Prompt the user for data needed by the application to continue.

  • Display information to the user.

In Visual Basic, for example, you use the File Open dialog box to display existing projects. The About dialog box in Visual Basic is also an example of how you can use a dialog box to display information. When the user clicks the Help, About Visual Basic menu item on the menu bar, the About dialog box is displayed.

The following topics discuss dialog boxes:

  • Modal and Modeless Dialog Boxes A discussion of dialog box types.

  • Using Predefined Dialog Boxes Techniques for using message and input boxes.

  • Using Forms as Custom Dialog Boxes Creating your own dialog boxes.


Modal and Modeless Dialog Boxes

Dialog boxes are either modal or modeless. A modal dialog box must be closed (hidden or unloaded) before you can continue working with the rest of the application. For example, a dialog box is modal if it requires you to click OK or Cancel before you can switch to another form or dialog box.

The About dialog box in Visual Basic is modal. Dialog boxes that display important messages should always be modal — that is, the user should always be required to close the dialog box or respond to its message before proceeding.

Modeless dialog boxes let you shift the focus between the dialog box and another form without having to close the dialog box. You can continue to work elsewhere in the current application while the dialog box is displayed. Modeless dialog boxes are rare. From the Edit menu, the Find dialog box in Visual Basic is an example of a modeless dialog box. Use modeless dialog boxes to display frequently used commands or information.

To display a form as a modal dialog box

  • Use the Show method with a style argument of vbModal (a constant for the value 1).

    For example:

    ' Display frmAbout as a modal dialog.
frmAbout.Show vbModal

To display a form as a modeless dialog box

  • Use the Show method without a style argument.

    For example:

    ' Display frmAbout as a modeless dialog.
frmAbout.Show

Note If a form is displayed as modal, the code following the Show method is not executed until the dialog box is closed. However, when a form is shown as modeless, the code following the Show method is executed immediately after the form is displayed.

The Show method has another optional argument, owner, that can be used to specify a parent-child relationship for a form. You can pass the name of a form to this argument to make that form the owner of the new form.

To display a form as a child of another form

  • Use the Show method with both style and owner arguments.

    For example:

    ' Display frmAbout as a modeless child of frmMain.
frmAbout.Show vbModeless, frmMain

Using the owner argument with the Show method ensures that the dialog box will be minimized when it’s parent is minimized, or unloaded should the parent form be closed.


Using Predefined Dialog Boxes

The easiest way to add a dialog box to your application is to use a predefined dialog, because you don't have to worry about designing, loading, or showing the dialog box. However, your control over its appearance is limited. Predefined dialog boxes are always modal.

The following table lists the functions you can use to add predefined dialog boxes to your Visual Basic application.

Use this function | To do this
InputBox function | Display a command prompt in a dialog box, and return whatever is entered by the user.
MsgBox function | Display a message in a dialog box, and return a value indicating the command button was clicked by the user.

Prompting for Input with InputBox

Use the InputBox function to solicit data from the user. This function displays a modal dialog box that asks the user to enter some data. The text input box shown in Figure 6.17 prompts the user for the name of the file to open.

Figure 6.17 A dialog box using the InputBox function

The following code displays the input box shown in Figure 6.17:

FileName = InputBox("Enter file to open:", "File Open")

Note Remember that when you use the InputBox function, you have little control over the components of the dialog box. You can change only the text in the title bar, the command prompt displayed to the user, the position of the dialog box on the screen, and whether or not it displays a Help button.

For More Information See "InputBox Function" in the Language Reference.

Displaying Information with MsgBox

Use the MsgBox function to get yes or no responses from users, and to display brief messages, such as errors, warnings, or alerts in a dialog box. After reading the message, the user chooses a button to close the dialog box.

An application named Text Editor might display the message dialog box shown in Figure 6.18 if a file cannot be opened.

Figure 6.18 An error message dialog box created using the MsgBox function

The following code displays the message box shown in Figure 6.18:

MsgBox "Error encountered while trying to open file, _
   please retry.", vbExclamation, "Text Editor"

Note Modality can either be limited to the application or the system. If a message box's modality is limited to the application (default), then users cannot switch to another part of the application until the dialog box is dismissed, but they can switch to another application. A system modal message box does not allow the user to switch to another application until the message box is dismissed.

For More Information See "MsgBox Function" in the Language Reference.


Using Forms as Custom Dialog Boxes

A custom dialog box is a form you create containing controls — including command buttons, option buttons, and text boxes — that lets the user supply information to the application. You customize the appearance of the form by setting property values. You also write code to display the dialog box at run time.

To create a custom dialog box, you can start with a new form or customize an existing dialog box. Over time, you can build up a collection of dialog boxes that can be used in many applications.

To customize an existing dialog box

  1. From the Project menu, choose Add Form to add an existing form to your project.

  2. From the File menu, choose Save filename As and enter a new file name. (This prevents you from making changes to the existing version of the form).

  3. Customize the appearance of the form as needed.

  4. Customize event procedures in the Code window.

To create a new dialog box

  1. From the Project menu, choose Add Form.

    –or–

    Click the Form button on the toolbar to create a new form.

  2. Customize the appearance of the form as needed.

  3. Customize event procedures in the Code window.

You have considerable freedom to define the appearance of a custom dialog box. It can be fixed or movable, modal or modeless. It can contain different types of controls; however, dialog boxes do not usually include menu bars, window scroll bars, Minimize and Maximize buttons, status bars, or sizable borders. The remainder of this topic discusses ways to create typical dialog box styles.

Adding a Title

A dialog box should always have a title that identifies it. To create a title, set the form's Caption property to the text string that will appear in the title bar. Usually, this is done at design time using the Properties window, but you can also do this from code. For example:

frmAbout.Caption = "About"

Tip If you want to remove the title bar completely, set the form's ControlBox, MinButton, and MaxButton properties to False; set the BorderStyle to a nonsizable setting (0, 1, or 3); and set the Caption equal to an empty string ("").

Setting Standard Dialog Box Properties

Generally, the user responds to a dialog box by providing information and then closing the dialog box with an OK or Cancel command button. Because a dialog box is temporary, users usually don't need to move, size, maximize, or minimize it. As a result, the sizable border style, Control menu box, Maximize button, and Minimize button that come with a new form are unnecessary on most dialog boxes.

You can remove these items by setting the BorderStyle, ControlBox, MaxButton, and MinButton properties. For example, an About dialog box might use the following property settings.

Property | Setting | Effect
BorderStyle | 1 | Changes the border style to fixed single, thus preventing the dialog box from being sized at run time.
ControlBox | False | Removes the Control menu box.
MaxButton | False | Removes the Maximize button, thus preventing the dialog box from being maximized at run time.
MinButton | False | Removes the Minimize button, thus preventing the dialog box from being minimized at run time.

Remember that if you remove the Control menu box (ControlBox = False), you must provide the user with another way to exit the dialog box. This is commonly done by adding an OK, Cancel, or Exit command button to the dialog box and adding code in the Click event for the button that hides or unloads the dialog.

Adding and Placing Command Buttons

Modal dialog boxes must contain at least one command button to exit the dialog box. Usually, two command buttons are used: one button to let the user start an action, and one button to close the dialog box without making any changes. Typically, the Caption property settings for these buttons are OK and Cancel. In this scenario, the OK command button has its Default property set to True, and the Cancel button has its Cancel property set to True. Although OK and Cancel are the most commonly used buttons, other button caption combinations work as well.

Dialog boxes that display messages usually use a label control to display the error message or command prompt, and one or two command buttons to perform an action. For example, you might assign the error message or command prompt to the Caption property of the label, and Yes and No to the Caption property of two command button controls. When users choose Yes, one action takes place; when they choose No, another action occurs.

Command buttons on this type of dialog are usually placed on the bottom or right side of the dialog box, with the top or left button being the default button, as shown in Figure 6.19.

Figure 6.19 Command button placement on dialog boxes

Setting Default, Cancel, and Focus

Command button controls provide the following properties:

  • Default

  • Cancel

  • TabIndex

  • TabStop

The Default button is selected when the user presses ENTER. Only one command button on a form can have its Default property set to True. Pressing the ENTER key invokes the Click event for the default command button. This feature works in conjunction with an edit control, such as a text box. For example, the user can type data in a text box and then press ENTER to generate a Click event instead of choosing an OK button.

The Cancel button is selected when the user presses ESC. Only one command button on a form can have its Cancel property set to True. Pressing the ESC key invokes the Click event for the Cancel command button. The Cancel button can also be the default command button. To specify the Cancel button for a dialog box, set the command button's Cancel property to True.

Tip In general, the button that indicates the most likely or safest action should be the default action. For example, in a Text Replace dialog box, Cancel should be the default button, not Replace All.

You can also specify the button that will have the focus when the dialog is displayed. The control with the lowest TabIndex setting receives the focus when the form is displayed. Pressing the ENTER key invokes the Click event for the default command button or for the command button that has the focus. To give a command button the focus when the form is displayed, set the command button's TabIndex to 0 and its TabStop property to True. You can also use the SetFocus method to give a specific control the focus when a form is displayed.

For More Information See "TabIndex Property" and "TabStop Property" in the Language Reference.

Disabling Controls on a Dialog Box

Sometimes controls need to be disabled because their actions would be inappropriate in the current context. For example, when the Visual Basic Find dialog box is first displayed, the Find Next button is disabled, as shown in Figure 6.20. You can disable a control on a dialog by setting its Enabled property to False.

Figure 6.20 Disabled controls on a dialog box

To disable a control on a dialog box

  • Set each control's Enabled property to False. For example:
    cmdFindNext.Enabled = False
cmdReplace.Enabled = False

Displaying a Custom Dialog Box

You display a dialog box in the same way you display any other form in an application. The startup form loads automatically when the application is run. When you want a second form or dialog box to appear in the application, you write code to load and display it. Similarly, when you want the form or dialog box to disappear, you write code to unload or hide it.

The following code displays the About dialog box when the user selects the Help, About menu item:

Private Sub mnuHelpAbout_Click ()
   ' The Show method with style = vbModal is used here
   ' to display the dialog as modal.
   frmAbout.Show vbModal
End Sub

Display Options

The code you write determines how a dialog box is loaded into memory and displayed. The following table describes various form displaying tasks and the keywords that are used to perform them.

Task | Keyword
Load a form into memory, but do not display it. | Use the Load statement, or reference a property or control on the form.
Load and display a modeless form. | Use the Show method.
Load and display a modal form. | Use the Show method with style = vbModal.
Display a loaded form. | Set its Visible property to True, or use the Show method.
Hide a form from view. | Set its Visible property to False, or use the Hide method.
Hide a form from view and unload from memory. | Use the Unload statement.

The Show method loads the form and sets its Visible property to True. The argument passed to the Show method indicates the style of the dialog box. If the style argument is omitted or set to vbModeless or 0 (default), the dialog box is modeless; if it is vbModal or 1, the dialog box is modal.

To exit the dialog box when the user chooses OK or Cancel, use either the Unload statement or the Hide method. For example:

Unload frmAbout

–or–

frmAbout.Hide

The Unload statement removes the dialog box from memory, while the Hide method merely removes the dialog box from view by setting its Visible property to False. When you unload a form, the form itself and its controls are unloaded from memory (including any controls that were loaded at run time). When you hide a form, the form and its controls remain in memory.

When you need to save space in memory, it's better to unload a form, because unloading a form frees memory. If you use the dialog box often, you can choose to hide the form. Hiding a form retains any data attached to it, including property values, print output, and dynamically created controls. By hiding a form, you can continue to refer to the properties and controls of a hidden form in code.


Designing for Different Display Types

Microsoft Windows is device-independent — a windows-based application can be run on many different computers with different display resolutions and color depths. The applications that you write in Visual Basic are likely to be run on different display types as well; you need to be aware of this when designing an application.

Designing Resolution-independent Forms

By default, Microsoft Visual Basic doesn't change your form and control sizes as you change screen resolutions. What this means is that a form that you design at 1024 by 768 resolution may extend past the edges of the screen when run at 640 by 480 resolution. If you want to create forms and controls that have the same proportions no matter what screen resolution you use, you must either design your forms at the lowest resolution, or add code to your program that changes the forms.

The easiest way to avoid sizing problems is to design your forms at 640 by 480 resolution. If you prefer to work at a higher resolution, you still need to be aware of how your form will appear at a lower resolution. One way to do this is to use the Form Layout window to preview the size and location of your form. You can use Resolution Guides to see what portion of the screen will be visible at a lower resolution. To toggle the Resolution Guides, right click in the Form Layout window and select the Resolution Guides menu item from the popup menu.

Visual Basic also places your form at run time based on its location at design time. If you are running at 1024 by 768 resolution at design time and you place a form in the lower right-hand corner of the screen, it may not be visible when run at a lower resolution. To avoid this, set the startup position of your form at design time by selecting the Startup Position menu item from the Form Layout window popup menu.

Alternatively, you can set a form’s position at runtime with code in the Form Load event:

Private Sub Form_Load()
   Me.Move 0, 0
End Sub

This has the same effect as setting both the Left and Top properties of the form to 0, but the Move method accomplishes it in a single step.

Visual Basic uses a device-independent unit of measurement, a twip, for calculating size and position. Two properties of the Screen object, TwipsPerPixelX and TwipsPerPixelY, can be used to determine the size of the display at run time. Using these properties, you can write code to adjust the size and position of your forms and controls:

Private Sub SetControls()
   Dim X As Integer
   Dim Y As Integer

   X = Screen.TwipsPerPixelX
   Y = Screen.TwipsPerPixelY
   Select Case X, Y
      Case 15, 15
         ' Resize and move controls.
         txtName.Height = 200
         txtName.Width = 500
         txtName.Move 200, 200
      ' Add code for other resolutions.
      …
End Sub

You also need to be aware of the position of Visual Basic's own windows at design time. If you position the Project window to the right side of the screen at high resolution, you may find that it is no longer accessible when you open your project at a lower resolution.

Designing for Different Color Depths

In designing an application, you also need to consider the color display capabilities of the computers that may be running your application. Some computers can display 256 or more colors, others are limited to 16. If you design a form using a 256-color palette, dithering (a process used to simulate colors that are not available) may cause some of the elements on the form to disappear when displayed at 16 colors.

To avoid this situation, it's best to limit the colors used in your application to the 16 standard Windows colors. These are represented by the Visual Basic color constants (vbBlack, vbBlue, vbCyan, and so on). If it's necessary to use more than 16 colors in your application, you should still stick with the standard colors for text, buttons, and other interface elements.


Designing with the User in Mind

Unless you're creating Visual Basic applications strictly for your own use, the value of your creations is going to be judged by others. The user interface of your application has the greatest impact on the user's opinion — no matter how technically brilliant or well optimized your code may be, if the user finds your application difficult to use, it won't be well received.

As a programmer, you are undoubtedly familiar with the technological aspects of computers. It's easy to forget that most users don't understand (and probably don't care) about the technology behind an application. They see an application as a means to an end: a way to accomplish a task, ideally more easily than they would without the aid of a computer. A well-designed user interface insulates the user from the underlying technology, making it easy to perform the intended task.

In designing the user interface for your application, you need to keep the user in mind. How easily can a user discover the various features of your application without instruction? How will your application respond when errors occur? What will you provide in terms of help or user assistance? Is the design aesthetically pleasing to the user? The answers to these and other questions relating to user-focused design are covered in this section.

  • The Basics of Interface Design A discussion of graphic design principles.

  • Designing for Usability Tips for creating a user-focused design.

  • When Things Go Wrong: Interacting with Users Techniques for gracefully handling user and system errors.

  • Designing a User Assistance Model Providing help for learning and using your application.


The Basics of Interface Design

You don't need to be an artist to create a great user interface — most of the principles of user interface design are the same as the basic design principles taught in any elementary art class. The elementary design principles of composition, color, and so forth apply equally well to a computer screen as they do to a sheet of paper or a canvas.

Although Visual Basic makes it easy to create a user interface by simply dragging controls onto a form, a little planning up front can make a big difference in the usability of your application. You might consider drawing your form designs on paper first, determining which controls are needed, the relative importance of the different elements, and the relationships between controls.

Composition: The Look and Feel of an Application

The composition or layout of your form not only influences its aesthetic appeal, it also has a tremendous impact on the usability of your application. Composition includes such factors as positioning of controls, consistency of elements, affordances, use of white space, and simplicity of design.

Positioning of Controls

In most interface designs, not all elements are of equal importance. Careful design is necessary to ensure that the more important elements are readily apparent to the user. Important or frequently accessed elements should be given a position of prominence; less important elements should be relegated to less prominent locations.

In most languages, we are taught to read from left to right, top to bottom of a page. The same holds true for a computer screen — most user's eyes will be drawn to the upper left portion of the screen first, so the most important element should go there. For example, if the information on a form is related to a customer, the name field should be displayed where it will be seen first. Buttons, such as OK or Next, should be placed in the lower right portion of the screen; the user normally won't access these until they have finished working with the form.

Grouping of elements and controls is also important. Try to group information logically according to function or relationship. Because their functions are related, buttons for navigating a database should be grouped together visually rather than scattered throughout a form. The same applies to information; fields for name and address are generally grouped together, as they are closely related. In many cases, you can use frame controls to help reinforce the relationships between controls.

Consistency of Interface Elements

Consistency is a virtue in user interface design. A consistent look and feel creates harmony in an application — everything seems to fit together. A lack of consistency in your interface can be confusing, and can make an application seem chaotic, disorganized, and cheap, possibly even causing the user to doubt the reliability of an application.

For visual consistency, establish a design strategy and style conventions before you begin development. Design elements such as the types of controls, standards for size and grouping of controls, and font choices should be established in advance. You can create prototypes of possible designs to help you make design decisions.

The wide variety of controls available for use in Visual Basic make it tempting to use them all. Avoid this temptation; choose a subset of controls that best fit your particular application. While list box, combo box, grid, and tree controls can all be used to present lists of information, it's best to stick with a single style where possible.

Also, try to use controls appropriately; while a text box control can be set to read-only and used to display text, a label control is usually more appropriate for that purpose. Remain consistent in the setting of properties for your controls — if you use a white back color for editable text in one place, don't use grey in another unless there's a good reason.

Consistency between different forms in your application is important to usability. If you use a grey background and three-dimensional effects on one form and a white background on another, the forms will appear to be unrelated. Pick a style and stick with it throughout your application, even if it means redesigning some features.

Affordances: Form Follows Function

Affordances are visual clues to the function of an object. Although the term may be unfamiliar, examples of affordances are all around you. A handgrip on a bicycle has depressions where you place your fingers, an affordance that makes it obvious that it is meant to be gripped. Push buttons, knobs, and light switches are all affordances — just by looking at them you can discern their purpose.

A user interface also makes use of affordances. For instances, the three-dimensional effects used on command buttons make them look like they are meant to be pushed. If you were to design a command button with a flat border, you would lose this affordance and it wouldn't be clear to the user that it is a command button. There are cases where flat buttons might be appropriate, such as games or multimedia applications; this is okay as long as you remain consistent throughout your application.

Text boxes also provide a sort of affordance — users expect that a box with a border and a white background will contain editable text. While it's possible to display a text box with no border (BorderStyle = 0), this will make it look like a label and it won't be obvious to the user that it is editable.

Use of White Space

The use of white space in your user interface can help to emphasize elements and improve usability. White space doesn't necessarily have to be white — it refers to the use of blank space between and around controls a form. Too many controls on a form can lead to a cluttered interface, making it difficult to find an individual field or control. You need to incorporate white space in your design in order to emphasize your design elements.

Consistent spacing between controls and alignment of vertical and horizontal elements can make your design more usable as well. Just as text in a magazine is arranged in orderly columns with even spacing between lines, an orderly interface makes your interface easy to read.

Visual Basic provides several tools that make it easy to adjust the spacing, alignment, and size of controls. Align, Make Same Size, Horizontal Spacing, Vertical Spacing, and Center in Form commands can all be found under the Format menu.

Keep It Simple

Perhaps the most important principle of interface design is one of simplicity. When it comes to applications, if the interface looks difficult, it probably is. A little forethought can help you to create an interface that looks (and is) simple to use. Also, from an aesthetic standpoint, a clean, simple design is always preferable.

A common pitfall in interface design is to try and model your interface after real-world objects. Imagine, for instance, that you were asked to create an application for completing insurance forms. A natural reaction would be to design an interface that exactly duplicates the paper form on screen. This creates several problems: the shape and dimensions of a paper form are different than those of the screen, duplicating a form pretty much limits you to text boxes and check boxes, and there's no real benefit to the user.

It's far better to design your own interface, perhaps providing a printed duplicate (with print preview) of the original paper form. By creating logical groupings of fields from the original form and using a tabbed interface or several linked forms, you can present all of the information without requiring the user to scroll. You can also use additional controls, such as a list box preloaded with choices, which reduce the amount of typing required of the user.

You can also simplify many applications by taking infrequently used functions and moving them to their own forms. Providing defaults can sometimes simplify an application; if nine out of ten users select bold text, make the text bold by default rather than forcing the user to make a choice each time (don't forget to provide an option to override the default). Wizards can also help to simplify complex or infrequent tasks.

The best test of simplicity is to observe your application in use. If a typical user can't immediately accomplish a desired task without assistance, a redesign may be in order.

Using Color and Images

The use of color in your interface can add visual appeal, but it's easy to overuse it. With many displays capable of displaying millions of colors, it's tempting to use them all. Color, like the other basic design principles, can be problematic if not carefully considered in your initial design.

Preference for colors varies widely; the user's taste may not be the same as your own. Color can evoke strong emotions, and if you're designing for international audiences, certain colors may have cultural significance. It's usually best to stay conservative, using softer, more neutral colors.

Of course, your choice of colors may also be influenced by the intended audience and the tone or mood you are trying to convey. Bright reds, greens, and yellows may be appropriate for a children's application, but would hardly evoke an impression of fiscal responsibility in a banking application.

Small amounts of bright color can be used effectively to emphasize or draw attention to an important area. As a rule of thumb, you should try and limit the number of colors in an application, and your color scheme should remain consistent. It's best to stick to a standard 16-color palette if possible; dithering can cause some other colors to disappear when viewed on a 16-color display.

Another consideration in the use of color is that of colorblindness. Many people are unable to tell the difference between different combinations of primary colors such as red and green. To someone with this condition, red text on a green background would be invisible.

Images and Icons

The use of pictures and icons can also add visual interest to your application, but again, careful design is essential. Images can convey information compactly without the need for text, but images are often perceived differently by different people.

Toolbars with icons to represent various functions are a useful interface device, but if the user can't readily identify the function represented by the icon, they can be counterproductive. In designing toolbar icons, look at other applications to see what standards are already established. For example, many applications use a sheet of paper with a folded corner to represent a New File icon. There may be a better metaphor for this function, but representing it differently could confuse the user.

It's also important to consider the cultural significance of images. Many programs use a picture of a rural-style mailbox with a flag (Figure 6.21) to represent mail functions. This is primarily an American icon; users in other countries or cultures probably won't recognize it as a mailbox.

Figure 6.21 An icon representing a mailbox

In designing your own icons and images, try to keep them simple. Complex pictures with a lot of colors don't degrade well when displayed as a 16-by-16 pixel toolbar icon, or when displayed at high screen resolutions.

Choosing Fonts

Fonts are also an important part of your user interface, because they often communicate important information to the user. You need to select fonts that will be easily readable at different resolutions and on different types of displays. It's best to stick with simple sans serif or serif fonts where possible. Script and other decorative fonts generally look better in print than on screen, and can be difficult to read at smaller point sizes.

Unless you plan on distributing fonts along with your application, you should stick to standard Windows fonts such as Arial, New Times Roman, or System. If the user's system doesn't include a specified font, the system will make a substitution, resulting in a completely different appearance than what you intended. If you're designing for an international audience, you'll need to investigate what fonts are available in the intended languages. Also, you'll need to consider text expansion when designing for other languages — text strings can take up to 50% more space in some languages.

Again, design consistency is important in choosing fonts. In most cases, you shouldn't use more than two fonts at two or three different point sizes in a single application. Too many fonts can leave your application looking like a ransom note.


Designing for Usability

The usability of any application is ultimately determined by the user. Interface design is an iterative process; rarely is the first pass at designing an interface for your application going to yield a perfect interface. By getting users involved early in the design process, you can create a better, more usable interface with less effort.

What is a Good Interface?

The best place to start when designing a user interface is to look at some of the best-selling applications from Microsoft or other companies; after all, they probably didn't get to be best-sellers because of poor interfaces. You'll find many things in common, such as toolbars, status bars, ToolTips, context-sensitive menus, and tabbed dialogs. It's no coincidence that Visual Basic provides the capabilities for adding all of these to your own applications.

You can also borrow from your own experience as a user of software. Think about some of the applications that you have used; what works, what doesn't, and how you would fix it. Remember, however, that your personal likes and dislikes may not match those of your users; you'll need to validate your ideas with them.

You may have also noticed that most successful applications provide choices to accommodate varying user preferences. For instance, the Microsoft Windows Explorer allows users to copy files with menus, keyboard commands, or by drag and drop. Providing options will broaden the appeal of your application; as a minimum you should make all functions accessible by both mouse and keyboard.

Windows Interface Guidelines

One of the main advantages of the Windows operating system is that it presents a common interface across all applications. A user that knows how to use one Windows-based application should be able to easily learn any other. Unfortunately, applications that stray too far from the established interface guidelines aren't as easily learned.

Menus are a good example of this — most Windows-based applications follow the standard of a File menu on the left, then optional menus such as Edit and Tools, followed by Help on the right. It could be argued that Documents would be a better name than File, or that the Help menu should come first. There's nothing to prevent you from doing this, but by doing so you will confuse your users and decrease the usability of your application. Users will have to stop and think every time they switch between your application and another.

The placement of submenus is also important. Users expect to find Copy, Cut and Paste beneath the Edit menu; moving them to the File menu would be confusing at best. Don't deviate from the established guidelines unless you have a good reason to do so.

Testing for Usability

The best way to test the usability of your interface is to involve users throughout the design phase. Whether you're designing a major shrink-wrap application or a small application for limited use, the design process should be pretty much the same. Using your established design guidelines, you'll want to start by designing the interface on paper.

The next step is to create one or more prototypes, designing your forms in Visual Basic. You'll need to add just enough code to make the prototype functional: displaying forms, filling list boxes with sample data, and so forth. Then you're ready to start usability testing.

Usability testing can be an informal process, reviewing your design with a few users, or a formal process in an established usability lab. Either way, the purpose is the same — learning first-hand from the users where your design works and where it needs improvement. Rather than questioning the user, it's more effective to simply turn the user loose with the application and observe them. Have the user verbalize their thought process as they attempt to perform a set of tasks: "I want to open a new document, so I will look under the File menu." Make note of where the interface design doesn't respond to their thought processes. Test with multiple users; if you see several users having difficulty with a particular task, that task probably needs more attention.

Next, you'll want to review your notes and consider how you can change the interface to make it more usable. Make the changes to your interface and test it again. Once you are satisfied that your application is usable, you're ready to start coding. You'll also want to test occasionally during the development process to make sure that the assumptions for the prototype were valid.

Discoverability of Features

One of the key concepts in usability testing is that of discoverability. If a user can't discover how to use a feature (or even that a feature exists), that feature is of little use. For example, the majority of Windows 3.1 users were never aware that the ALT, TAB key combination could be used to switch between open applications. There was no clue anywhere in the interface to help users discover this feature.

To test the discoverability of a feature, ask the user to perform a task without explaining how to do it (for example, "Create a new document using a Form Letter Template"). If they can't accomplish the task, or if it takes several attempts, the discoverability of that feature needs work.


When Things Go Wrong: Interacting with Users

In an ideal world, software and hardware would always work flawlessly, and users would never make mistakes. Reality dictates that mistakes can and will happen. A part of user interface design involves deciding how the application will respond when things go wrong.

A common response is to display a dialog box, asking for user input as to how the application should deal with the problem. A less common (but preferable) response would be to simply resolve the problem without bothering the user. After all, the user is primarily concerned with performing a task, not with technical details. In designing your user interface, think about the potential errors and determine which ones require user interaction and which ones can be resolved programmatically.

Creating Intelligent Dialog Boxes

Occasionally an error occurs in your application and it's necessary to make a decision in order to resolve the situation. This usually occurs as a branch in your code — an If…Then statement or a Case statement. If the decision requires user interaction, the question is usually posed to the user with a dialog box. Dialog boxes are a part of your user interface, and like the other parts of the interface, their design plays a role in the usability of your application.

Sometimes it seems as if many dialog boxes were designed by programmers who have never had an intelligent conversation with another human being. A message such as "A sector of fixed disk C: is corrupted or inaccessible. Abort, Retry, Ignore?" (see Figure 6.22) has little meaning to the average user. It's kind of like a waitress asking you "We're out of soup or the kitchen is on fire. Abort, Retry, Ignore?" How would you answer? It's important to phrase questions (and choices) in a manner that the user can understand. In the prior example, a better message might be "There is a problem saving your file on drive C. Save file on drive A, Don't save the file?"

Figure 6.22 Which dialog box presents the clearest message?

When creating dialog boxes for your application, keep the user in mind. Does the message convey useful information to the user? Is it easily understandable? Do the command buttons present clear choices? Are the choices appropriate for the given situation? Keep in mind that it only takes one annoying message box to give a user a bad impression of your application.

If you're designing your own custom dialog forms, try to stick to a standard style. If you vary too far from the standard message box layout, users may not recognize it as a dialog box.

For More Information To learn more about dialogs, see "Dialog Boxes" earlier in this chapter.

Handling Errors Without Dialog Boxes

It isn't always necessary to interrupt the user when an error occurs. Sometimes it's preferable to handle the error in code without notifying the user, or to warn the user in a way that doesn't stop their work flow. A good example of this technique is the AutoCorrect feature in Microsoft Word: if a common word is mistyped, Word fixes it automatically; if a less common word is misspelled, it is underlined in red so the user can correct it later.

There are a number of techniques that you can use; it's up to you to decide which techniques are appropriate for your own application. Here are a few suggestions:

  • Add an Undo function to the Edit menu. Rather than interrupting the user with a confirmation dialog for deletions and so forth, trust that they are making the right decision and provide a Undo function in case they change their mind later.

  • Display a message on a status bar or icon. If the error doesn't affect the user's current task, don't stop the application. Use a status bar or a brightly colored warning icon to warn the user — they can handle the problem when they are ready.

  • Correct the problem. Sometimes the solution to an error is obvious. For instance, if a disk is full when the user tries to save a file, check the system for space on other drives. If space is available, save the file; put a message on the status bar to let the user know what you did.

  • Save the message until later. Not all errors are critical or demand immediate attention; consider logging these to a file and displaying them to the user when they exit the application or at another convenient time. If the user makes a possible entry error (for example, Mian St. instead of Main St.), log it. Add a Review Entries button and a function to display the discrepancies so the user can correct them.

  • Don't do anything. Sometimes an error isn't important enough to warrant a warning. For instance, the fact that a printer on LPT1 is out of paper doesn't mean much until you're ready to print. Wait until the message is appropriate to the current task.

For More Information To learn more about error handling techniques, see "Debugging Your Code and Handling Errors."


Designing a User Assistance Model

No matter how great your user interface, there will be times that a user needs assistance. The user assistance model for your application includes such things as online Help and printed documentation; it may also contain user assistance devices such as ToolTips, Status Bars, What's This help, and Wizards.

The user assistance model should be designed just like any other part of your application: before you start developing. The contents of your model will vary depending on the complexity of the application and the intended audience.

Help and Documentation

Online Help is an important part of any application — it's usually the first place a user will look when they have a question. Even a simple application should provide Help; failing to provide it is like assuming that your users will never have questions.

In designing your Help system, keep in mind that its primary purpose is to answer questions. Try to think in terms of the user when creating topic names and index entries; for example, "How do I format a page?" rather than "Edit, Page Format menu" will make your topic easier to locate. Don't forget about context sensitivity; it's frustrating to most users if they press the F1 key for help on a specific field and find themselves at the Contents topic.

Conceptual documentation, whether printed and/or provided on compact disc, is helpful for all but the simplest applications. It can provide information that may be difficult to convey in the shorter Help topic. At the very least, you should provide documentation in the form of a ReadMe file that the user can print if desired.

User Assistance Devices

Within the user interface, there are several techniques for providing assistance to the user. Visual Basic makes it easy to add ToolTips, What's This help, Status displays, and Wizards to your applications. It's up to you to decide which of these devices are appropriate for your application.

ToolTips

ToolTips (Figure 6.23) are a great way to display information to the user as they navigate the user interface. A ToolTip is a small label that is displayed when the mouse pointer is held over a control for a set length of time, usually containing a description of the control's function. Normally used in conjunction with toolbars, ToolTips also work well in most any part of the interface.

Figure 6.23 A ToolTip for the Visual Basic toolbar

Most Visual Basic controls contain a single property for displaying ToolTips: ToolTipText. The following code would implement a ToolTip for a command button named cmdPrint:

cmdPrint.ToolTipText = "Prints the current document"

As with other parts of the interface, make sure that the text clearly conveys the intended message to the user.

For More Information To learn more about ToolTips, see "ToolTipText Property" in the Language Reference.

What's This Help

What's this Help provides a link to a pop-up Help topic (see Figure 6.24) when the user selects What's This Help and clicks the What's This cursor on a control. What's This Help can be initiated from a toolbar button, a menu item, or a button on the title bar of a dialog box.

Figure 6.24 A What's This Help pop-up window

To enable What's This Help from a menu or toolbar

  1. Select the control for which you wish to provide help .

  2. In the Properties window, select the WhatsThisHelpID property.

  3. Enter a context ID number for the associated pop-up Help topic.

  4. Repeat steps 1 through 3 for any additional controls.

  5. Select the form.

  6. In the Properties window, set the Form's WhatsThisHelp property to True.

  7. In the Click event of the menu or toolbar button, enter the following:
    formname.WhatsThisHelp

    When the user clicks the button or menu, the mouse pointer will change to the What's This pointer.

To enable What's This Help on the title bar of a custom dialog form, set the form's WhatsThisButton and WhatsThisHelp properties to True.

For More Information To learn more about What's This Help, see "WhatsThisHelp Property" and WhatsThisButton Property" in the Language Reference.

Status Displays

A status display can also be used to provide user assistance in much the same way as a ToolTip. Status displays are a good way to provide instructions or messages that may not fit easily into a ToolTip. The status bar control included in the Professional and Enterprise editions of Visual Basic works well for displaying messages; a label control can also be used as a status display.

The text displayed in a status display can be updated in one of two ways: in the GotFocus event of a control or form, or in the MouseMove event. If you want to use the display as a learning device, add an item to the Help menu to toggle its Visible property on and off.

To add a status display

  1. Add a label control to your form.

  2. Select the control for which you wish to display a message.

  3. Add the following code to the control's MouseMove (or GotFocus) event:
    Labelname.Caption = "Enter the customer's ID number in this field"

    When the user moves the mouse over the control, the message will be displayed in the label control.

  4. Repeat steps 2 and 3 for any additional controls.

Wizards

A wizard is a user assistance device that takes the user step by step through a procedure, working with the user's actual data. Wizards are usually used to provide task-specific assistance. They help a user accomplish a task that would otherwise require a considerable (and undesirable) learning curve; they provide expert information to a user that has not yet become an expert.

The Professional and Enterprise editions of Visual Basic include the Wizard Manager, a tool for creating wizards.

For More Information To learn more about wizards, see "Using Wizards and Add-Ins" in "Managing Projects."


Using Visual Basic's Standard Controls

You use controls to get user input and to display output. Some of the controls you can use in your applications include text boxes, command buttons, and list boxes. Other controls let you access other applications and process data as if the remote application was part of your code. Each control has its own set of properties, methods, and events. This chapter introduces you to the standard controls in Visual Basic.

For More Information See "Using ActiveX Controls" in the Component Tools Guide for more information about the ActiveX controls available in the Professional and Enterprise versions of Visual Basic.

Topics

Introduction to Visual Basic Controls

Introduces you to the standard controls available in Visual Basic.
Validating Control Data By Restricting Focus

Explains how to use the Validate event with the CausesValidation property.
Working with Control Arrays

Describes control arrays and how they are used in Visual Basic.
Using the ADO Data Control

Use the ADO Data Control to create simple database applications without writing any code.
Using the Check Box Control

Use check box controls in groups to display multiple choices from which the user can select one or more.
Using the Combo Box Control

Use the combined features of a text box and a list box to allow the user to select an item either by typing text into the combo box, or by selecting it from the list.
Using the Command Button Control

Use the command button control to begin, interrupt, or end a process. When clicked, it invokes the command written into its Click event procedure.
Using the Common Dialog Control

Use the common dialog control to provide a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts.
Using the Data Control

Use the intrinsic Data control to connect to desktop databases, such as Access.
Using the DataCombo and DataList Controls

Use the DataCombo and DataList controls with the ADO Data Control to display information from a database.
Using the File-System Controls (Directory List Box, Drive List Box, and File List Box)

Use the drive list box, the directory list box, and the file list box controls to allow users of your applications to explore the file system.
Using the Frame Control

Use the frame control to provide an identifiable grouping for other controls.
Using the Microsoft Hierarchical FlexGrid Control

Use the hierarchical flexgrid control for displaying data in a grid.

Using the Horizontal and Vertical Scroll Bar Controls

Use the horizontal and vertical scroll bar controls to provide scrolling to applications or controls when they are not provided automatically.
Using the Image Control

Use the image control to display graphics in the following formats: bitmap, icon, metafile, enhanced metafile, JPEG, or GIF files.
Using the Label Control

Use the label control to display text that cannot be edited by the user or to provide a description of what a certain control will do if clicked.
Using the Line Control

Use the line control to create simple line segments on a form, a frame, or in a picture box.
Using the List Box Control

Use the list box control to display a list of items from which the user can select one or more.
Using the OLE Container Control

Use the OLE container control to display and manipulate data from other Windows–based applications, such as Microsoft Excel and Microsoft Word for Windows.
Using the Option Button Control

Use the option button control to display options in option button groups from which the user can choose one.
Using the Picture Box Control

Use the picture box control to display graphics, to act as a container for other controls, and to display output from graphics methods or text using the Print method.
Using the RemoteData Control

Use the RemoteData Control to connect to large scale databases.
Using the Shape Control

Use the Shape control to draw predefined shapes on forms, frames, or picture boxes.
Using the Text Box Control

Use the text box control to display information entered by the user at run time, or assigned to the Text property of the control at design or run time.
Using the Timer Control

Use the timer control to execute timer events at specified time intervals.

Sample Applications

Alarm.vbp, Calc.vbp, Controls.vbp, Flex.vbp, Winseek.vbp

Many of the code examples in this chapter are taken from the Alarm.vbp, Calc.vbp, Controls.vbp, Flex.vbp, and Winseek.vbp sample applications which are listed in the Samples directory.


Introduction to Visual Basic Controls

The Visual Basic toolbox contains the tools you use to draw controls on your forms.

Figure 7.1 The Visual Basic toolbox

Control Categories

There are three broad categories of controls in Visual Basic:

  • Intrinsic controls, such as the command button and frame controls. These controls are contained inside the Visual Basic .exe file. Intrinsic controls are always included in the toolbox, unlike ActiveX controls and insertable objects, which can be removed from or added to the toolbox.

  • ActiveX controls, which exist as separate files with a .ocx file name extension. These include controls that are available in all editions of Visual Basic (DataCombo, DataList controls, and so on) and those that are available only in the Professional and Enterprise editions (such as Listview, Toolbar, Animation, and Tabbed Dialog). Many third-party ActiveX controls are also available.

    Note Controls with the .vbx file name extension use older technology and are found in applications written in earlier versions of Visual Basic. When Visual Basic opens a project containing a .vbx control, the default behavior is to replace the .vbx control with an .ocx control, but only if an .ocx version of the control is available. See "Updating Older Versions of Visual Basic Controls" later in this chapter for information on updating controls to the .ocx format.

  • Insertable Objects, such as a Microsoft Excel Worksheet object containing a list of all your company's employees, or a Microsoft Project Calendar object containing the scheduling information for a project. Since these can be added to the toolbox, they can be considered controls. Some of these objects also support Automation (formerly called OLE Automation), which allows you to program another application's objects from within a Visual Basic application. See "Programming with Objects," for more information on Automation.

For More Information See "Using ActiveX Controls" in the Component Tools Guide for more information about the ActiveX controls available in the Professional and Enterprise versions of Visual Basic.


Intrinsic Controls

The following table summarizes the intrinsic controls found in the Visual Basic toolbox.

Icon | Control name | Class name | Description
 | Check box | CheckBox | Displays a True/False or Yes/No option. You can check any number of check boxes on a form at one time.
 | Combo box | ComboBox | Combines a text box with a list box. Allows a user to type in a selection or select an item from a drop-down list.
 | Command button | CommandButton | Carries out a command or action when a user chooses it.
 | Data | Data | Enables you to connect to an existing database and display information from it on your forms.
 | Directory list box | DirListBox | Displays and allows a user to select directories and paths.
 | Drive list box | DriveListBox | Displays and allows a user to select valid disk drives.
 | File list box | FileListBox | Displays and allows a user to select from a list of files.
 | Frame | Frame | Provides a visual and functional container for controls.
 | Horizontal and vertical scroll bars | HScrollBar and VScrollBar | Allow a user to add scroll bars to controls that do not automatically provide them. (These are not the same as the built-in scroll bars found with many controls.)
 | Image | Image | Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files; acts like a command button when clicked.
 | Label | Label | Displays text a user cannot interact with or modify.
 | Line | Line | Adds a straight-line segment to a form.
 | List box | ListBox | Displays a list of items that a user can choose from.
 | OLE container | OLE | Embeds data into a Visual Basic application.
 | Option button | OptionButton | The Option Button control, as part of an option group with other option buttons, displays multiple choices, from which a user can choose only one.
 | Picture box | PictureBox | Displays bitmaps, icons, or Windows metafiles, JPEG, or GIF files. It also displays text or acts as a visual container for other controls.
 | Shape | Shape | Adds a rectangle, square, ellipse, or circle to a form, frame, or picture box.
 | Text box | TextBox | Provides an area to enter or display text.
 | Timer | Timer | Executes timer events at specified time intervals.

Note The pointer tool (the first tool in the toolbox) provides a way to move and resize forms and controls. It is not a control.


Standard ActiveX Controls

The Learning edition of Visual Basic contains a number of ActiveX controls (referred to as standard ActiveX controls) that allow you to add advanced features to your applications. ActiveX controls have the file name extension .ocx and can be used in your project by manually adding them to the toolbox.

The following table summarizes the standard ActiveX controls available in the Learning edition of Visual Basic.

Icon | Control name | Class name | Description
 | ADO Data Control | ADODC | Creates a connection to a database using ADO. Assignable to the DataSource property of other controls such as the DataGrid.
 | Common dialog | CommonDialog | Provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts.
 | DataCombo | DataCombo | Provides most of the features of the standard combo box control, plus increased data access capabilities.
 | DataGrid | DataGrid | A grid control that allows can be data-bound to a data source such as the ADO Data Control. Reading and editing the recordset is possible.
 | DataList | DataList | Provides most of the features of the standard list box control, plus increased data access capabilities.
 | Microsoft Hierarchical FlexGrid | MSHFlexGrid | A read-only grid control that can be bound the Data Environment designer to show hierarchical recordsets.

For More Information See "Using ActiveX Controls" in the Component Tools Guide for more information about the ActiveX controls available in the Professional and Enterprise versions of Visual Basic.

Other Controls

Other ActiveX controls included with all versions of Visual Basic are documented in the Component Tools Guide, and include the following: ImageCombo, ImageList, ListView, ProgressBar, Slider, Statusbar, TabStrip, Toolbar, and TreeView.

Adding and Removing ActiveX Controls

You move ActiveX controls to and from the toolbox using the following procedures.

To add an ActiveX control to the toolbox

  1. From the Project menu, choose Components.

  2. Select the check box next to the name of the .ocx control, and then choose OK. Once a control is placed in the toolbox, you can add it to a form just as you would an intrinsic control.

To remove an ActiveX control

  1. Remove all instances of the control from the forms in your project. Delete any references to the control in the project's code. If references to a deleted control are left in your code, an error message will display when you compile the application.

  2. From the Project menu, choose Components.

    Clear the check box next to the name of the .ocx control, and then choose OK. An error message will display if there are remaining instances of the control in your project.

For More Information See "Adding Controls to a Project" in "Managing Projects" for more information about adding and removing controls and insertable objects to and from the Toolbox.


Updating Older Versions of Visual Basic Controls

Older 16-bit versions of Visual Basic controls with the file extension .vbx are incompatible with this version of Visual Basic. If you attempt to load an older project containing .vbx controls, Visual Basic will warn you that the controls are unavailable or incompatible. You have the option of continuing to load the project without the .vbx controls but, of course, the application will not function properly.

If you have older Visual Basic projects that contain third-party .vbx controls, contact the control's manufacturer to inquire about .ocx replacements.


Control Naming Conventions

When you first create an object (form or control), Visual Basic sets its Name property to a default value. For example, all command buttons have their Name property initially set to Commandn, where n is 1, 2, 3, and so on. Visual Basic names the first command button drawn on a form Command1, the second Command2, and the third Command3.

You may choose to keep the default name; however, when you have several controls of the same type, it makes sense to change their Name properties to something more descriptive. Because it may be difficult to distinguish the Command1 button on MyForm from the Command1 button on YourForm, a naming convention can help. This is especially true when an application consists of several form, standard, and class modules.

You can use a prefix to describe the class, followed by a descriptive name for the control. Using this naming convention makes the code more self-descriptive and alphabetically groups similar objects in the Object list box. For example, you might name a Check Box control like this:

chkReadOnly

The names you give to forms and controls:

  • must begin with a letter.

  • must contain only letters, numbers, and the underscore character (_); punctuation characters and spaces are not allowed.

  • must be no longer than 40 characters.

For More Information See "Visual Basic Coding Conventions" for more information on naming conventions.


Using the Value of a Control

All controls have a property that you can use to store or retrieve values just by referring to the control, without using the property name. This is called the value of the control and is usually the most important or most commonly used property of the control. The following table lists the property that is considered to be the value for each control.

Control | Value
Check box | Value
Combo box | Text
Command button | Value
Common dialog | Action
Data | Caption
DataCombo | Text
DataGrid | Text
DataList | Text
Directory list box | Path
Drive list box | Drive
File list box | FileName
FlexGrid | Text
Frame | Caption
Horizontal scroll bar | Value
Image | Picture
Label | Caption
Line | Visible
List box | Text
Option button | Value
Picture box | Picture
Shape | Shape
Text box | Text
Timer | Enabled
Vertical scroll bar | Value

Whenever you want to refer to a property on a control that happens to be the value of that control, you can do so without specifying the property name in your code. For example, this line sets the value of the Text property of a text box control:

Text1 = "This text is assigned to the Text property _
of Text1"

In this example, the Caption property of Label1 is set to the FileName property of File1 whenever the user clicks a file in the file list box:

Private Sub File1_Click ()
   Label1 = File1
End Sub

Note Because using the value of a control makes your code somewhat less readable, the examples in this guide do not use it but instead refer explicitly to the properties on all controls. You may want to try writing your code both ways, and decide to use the value of controls in your code if you have no trouble reading it.


Validating Control Data by Restricting Focus

The Validate event and CausesValidation property are used in tandem to verify the input to a control before allowing the user to shift focus away from that control. For example, imagine an application with several text boxes and a Help button. When each text box receives the focus, you want to prevent the user from shifting the focus until the text box's particular validation criteria are met; however, you also want to allow users to click the Help button at any time. To do this, set the validation criteria in the Validate event and set the CausesValidation property of the Help button to False. If the property is set to True (the default setting), the Validate event will occur on the first control. If the property is set to False, the Validate event on the first control will be preempted from occurring.

The Validate event is more suited to validating data entry than the LostFocus event because the LostFocus event (by definition) occurs after the focus has shifted. In contrast, by using the Validate event you can prevent the focus from ever shifting to another control until all validation rules have been met.

Possible Uses

  • A data entry application needs to perform more sophisticated data entry validation than can be provided by the Masked Edit control, or the validation occurs in a business rule.

  • A form needs to prevent users from moving off a control using the TAB key or an accelerator until data has been entered in a field.

  • An ActiveX document running inside Internet Explorer needs a way for the user to finish an operation on the form before the script moves the focus programmatically.

Control the Focus in the Validate Event

The Validate event includes a keepfocus argument. When the argument is set to True, the control will retain the focus. This effectively prevents the user from clicking any other control.


Working with Control Arrays

A control array is a group of controls that share the same name and type. They also share the same event procedures. A control array has at least one element and can grow to as many elements as your system resources and memory permit; its size also depends on how much memory and Windows resources each control requires. The maximum index you can use in a control array is 32767. Elements of the same control array have their own property settings. Common uses for control arrays include menu controls and option button groupings.

Note Visual Basic includes the ability to dynamically add unreferenced controls to the Controls collection at run time. This topic refers only to referenced controls added at design time by cutting and pasting a control onto the form. For more information about adding controls at run time, see the reference topic "Add Method (Controls Collection)" and "Add Method (Licenses Collection)."

Why Use Control Arrays?

Adding controls with control arrays uses fewer resources than simply adding multiple controls of the same type to a form at design time. Control arrays are also useful if you want several controls to share code. For example, if three option buttons are created as a control array, the same code is executed regardless of which button was clicked.

If you want to create a new instance of a control at run time, that control must be a member of a control array. With a control array, each new element inherits the common event procedures of the array.

Using the control array mechanism, each new control inherits the common event procedures already written for the array. For example, if your form has several text boxes that each receive a date value, a control array can be set up so that all of the text boxes share the same validation code.

Sample Application: Calc.vbp

The Calculator sample application (which is listed in the Samples directory) shown in Figure 7.2 contains two control arrays — the number buttons and the operator buttons.

Figure 7.2 Control array example

The Name and Index property values for the control arrays in the Calculator example are listed in the following table.

Number(n) | Operator(n)
0 = Number(0) | + = Operator(1)
1 = Number(1) | – = Operator(2)
2 = Number(2) | X = Operator(3)
3 = Number(3) | / = Operator(4)
4 = Number(4) | = = Operator(5)
5 = Number(5) |
6 = Number(6) |
7 = Number(7) |
8 = Number(8) |
9 = Number(9) |

Notice how each control is referred to with the syntax object(index). You specify the index of a control when you create it. In fact, specifying any index for a control at design time makes that control part of an array.

The Index property distinguishes one element of the control array from another. When one of the controls in the array recognizes an event, Visual Basic calls a common event procedure and passes an argument (the value of the Index property) to identify which control actually recognizes the event.

For example, the first line of the Number_Click event procedure is:

Private Sub Number_Click (Index As Integer)

If Number(0) recognizes the event, Visual Basic passes 0 as the index argument, and if Number(1) recognizes the event, Visual Basic passes 1 as the index argument. Other than the index value, the remainder of the Number_Click code that is executed is the same for both Number(0) through Number(9).


Creating a Control Array at Design Time

There are three ways to create a control array at design time:

  • Assign the same name to more than one control.

  • Copy an existing control and then paste it onto the form.

  • Set the control's Index property to a value that is not Null.

    Note You must create menu control arrays in the Menu Editor. For details on how to do this, see "Creating and Modifying Menus at Run Time" in "Creating a User Interface."

To add a control array element by changing its name

  1. Draw the controls you want to be in the control array. (The controls must all be of the same type.) Decide which control will become the first element in the array.

  2. Select one of the controls and change its Name setting to the Name setting for the first element in the array.

  3. When you type an existing name for a control in the array, Visual Basic displays a dialog box asking you to confirm that you want to create a control array. Choose Yes to confirm the action.

For example, if the name of the first element in a control array is cmdCtlArr, you would choose a command button to add to the array and then set its name to cmdCtlArr. The message "You already have a control named 'cmdCtlArr.' Do you want to create a control array?" is displayed. Choose Yes to confirm the operation.

Controls added this way share only their Name property and control type; all other properties remain the same as when the control was originally drawn.

To add a control array element by copying an existing control

  1. Draw a control in the control array.

  2. While the control has the focus, choose Copy from the Edit menu.

  3. From the Edit menu, choose Paste. Visual Basic displays a dialog box asking you to confirm that you want to create a control array. Choose Yes to confirm the action.

    This control is assigned an index value of 1. The first control you drew has a value of 0.

The index value of each new array element corresponds to the order in which the element was added to the control array. When controls are added this way, most of the visual properties, such as height, width, and color, are copied from the first control in the control array to the new controls.


Adding to a Control Array at Run Time

You can add and remove controls in a control array at run time using the Load and Unload statements. However, the control to be added must be an element of an existing control array. You must have created a control at design time with the Index property set, in most cases, to 0. Then, at run time, use this syntax:

Load object(index%)

Unload object(index%)

Argument | Description
object | Name of the control to add to or delete from the control array.
index% | The control's index value in the array.

When you load a new element of a control array, most of the property settings are copied from the lowest existing element in the array — in this example, the element with the 0 index value. The Visible, Index, and TabIndex property settings are not automatically copied to new elements of a control array, so to make the newly added control visible, you must set its Visible property to True.

Note Visual Basic generates an error if you attempt to use the Load statement with an index number already in use in the array.

Important You can use the Unload statement to remove any control created with Load. However, you cannot use Unload to remove controls created at design time, regardless of whether or not they are part of a control array.


Controls Scenario: Adding and Deleting Controls in a Control Array

The control array example demonstrates how controls — in this case, option buttons — are added and deleted at run time. The example allows the user to add option buttons that change the background color of a picture box.

Start with a form, and then draw a picture box, a label, two option buttons, and three command buttons, as shown in Figure 7.3.

Figure 7.3 Adding controls at run time

The following table lists the property settings for the objects in the application.

Object | Property | Setting
Form | Caption | Control Array Example
Picture box | Name | picDisplay
Label | Caption | Select an option button to display a new color
Option1 | Name Index | optButton 0
Option2 | Name Index | optButton 1
First command button | Name Caption | cmdAdd &Add
Second command button | Name Caption | cmdDelete &Delete
Third command button | Name Caption | cmdClose &Close

Events in the Control Array Application

Next, you need to add the event procedures for the option buttons and command buttons. Start by adding the form declaration:

Dim MaxId As Integer

The Click event procedure is shared by all the option buttons:

Private Sub optButton_Click (Index As Integer)
   picDisplay.BackColor = QBColor(Index + 1)
End Sub

New option buttons are added by the Click event procedure for the Add command button. In this example, the code checks that no more than ten option buttons are loaded before the Load statement is executed. Once a control is loaded, its Visible property must be set to True.

Private Sub cmdAdd_Click ()
   If MaxId = 0 Then MaxId = 1   ' Set total option
                                 ' buttons.
   If MaxId > 8 Then Exit Sub   ' Only ten buttons
                                 ' allowed.
   MaxId = MaxId + 1         ' Increment button count.
   Load optButton(MaxId)      ' Create new button.
   optButton(0).SetFocus      ' Reset button selection.
   ' Set new button under previous button.
   optButton(MaxId).Top = optButton(MaxId - 1)._
   Top + 400
   optButton(MaxId).Visible = True   ' Display new
                                 ' button.
   optButton(MaxId).Caption = "Option" & MaxId + 1
End Sub

Option buttons are removed by the Click event procedure for the Delete command button:

Private Sub cmdDelete_Click ()
   If MaxId <= 1 Then Exit Sub   ' Keep first two
                                 ' buttons.
   Unload optButton(MaxId)      ' Delete last button.
   MaxId = MaxId - 1            ' Decrement button count.
   optButton(0).SetFocus      ' Reset button selection.
End Sub

The Close button Click event ends the application:

Private Sub cmdClose_Click ()

   Unload Me
End Sub


Using the ADO Data Control

The ADO Data control uses Microsoft ActiveX Data Objects (ADO) to quickly create connections between data-bound controls and data providers. Data-bound controls are any controls that feature a DataSource property. Data providers can be any source written to the OLE DB specification. You can also easily create your own data provider using Visual Basic's class module.

Although you can use the ActiveX Data Objects directly in your applications, the ADO Data control has the advantage of being a graphic control (with Back and Forward buttons) and an easy-to-use interface that allows you to create database applications with a minimum of code.

Figure 7.4 The ADO Data Control

Several of the controls found in Visual Basic's Toolbox can be data-bound, including the CheckBox, ComboBox, Image, Label, ListBox, PictureBox, and TextBox controls. Additionally, Visual Basic includes several data-bound ActiveX controls such as the DataGrid, DataCombo, Chart, and DataList controls. You can also create your own data-bound ActiveX controls, or purchase controls from other vendors.

Previous versions of Visual Basic featured the intrinsic Data control and the Remote Data control (RDC) for data access. Both controls are still included with Visual Basic for backward compatibility. However, because of the flexibility of ADO, it's recommended that new database applications be created using the ADO Data Control.

For More Information A complete list of data-bound controls can be found in "Controls That Bind to the ADO Data Control." To find out how to use the intrinsic Data control or the Remote Data control, see "Using the Data Control" or "Using the Remote Data Control." For details about creating a data provider, see "Creating Data-Aware Classes."

Possible Uses

  • Connect to a local or remote database.

  • Open a specified database table or define a set of records based on a Structured Query Language (SQL) query or stored procedure or view of the tables in that database.

  • Pass data field values to data-bound controls, where you can display or change the values.

  • Add new records or update a database based on any changes you make to data displayed in the bound controls.

To create a client, or front-end database application, add the ADO Data control to your forms just as you would any other Visual Basic control. You can have as many ADO Data controls on your form as you need. Be aware, however, that the control is a comparatively "expensive" method of creating connections, using at least two connections for the first control, and one more for each subsequent control.

Creating a Front-end Database Application with Minimal Code

It's possible to create a database application using a minimum of code by setting a few properties at design time. If you are using an OLE DB data source, the Microsoft Data Link Name (.UDL) must be created on your machine. See "Creating the Northwind OLE DB Data Link" for a step-by-step example.

To create a simple front-end database application

  1. Draw an ADO Data Control on a form. (The icon's ToolTip is "ADODC.")

    If the control is not available in the Toolbox, press CTRL+T to display the Components dialog box. In the Components dialog, click Microsoft ADO Data Control.

  2. On the Toolbox, click the ADO Data Control to select it. Then press F4 to display the Properties window.

  3. In the Properties window, click ConnectionString to display the ConnectionString dialog box.

  4. If you have created a Microsoft Data Link file (.UDL), select Use OLE DB File and click Browse to find the file on the computer. If you use a DSN, click Use ODBC Data Source Name and select a DSN from the box, or click New to create one. If you wish to use create a connection string, select Use ConnectionString, and then click Build, and use the Data Link Properties dialog box to create a connection string. After creating the connection string, click OK. The ConnectionString property will be filled with a string like:
    driver={SQL Server};server=bigsmile;uid=sa;pwd=pwd;database=pubs
  5. In the Properties window, set the RecordSource property to a SQL statement. For example,
    SELECT * FROM Titles WHERE AuthorID = 72

    You should always include a WHERE clause when accessing a table. Failing to do so will lock the entire table, which would be a major hindrance to other users.

  6. Draw a TextBox control on the form to display the database information.

  7. In the Properties window, set the DataSource property for Text1 to the name of the ADO Data control (ADODC1). This binds the text box to the ADO Data control.

  8. In the Properties window, click DataField and a list of available fields will drop down. Click the name of the field you want to display.

  9. Repeat steps 6, 7, and 8 for each additional field you want to access.

  10. Press F5 to run the application. You can use the four arrow buttons on the ADO Data control to move to the beginning of the data, to the end of the data, or from record to record through the data.

Setting the ConnectionString, Source, DataSource, and DataField Programmatically

The code below shows how to set these four properties programmatically. Note that setting the DataSource property requires the Set statement.

Private Sub Form_Load()
   With ADODC1
      .ConnectionString = "driver={SQL Server};" & _
      "server=bigsmile;uid=sa;pwd=pwd;database=pubs"
      .RecordSource = "Select * From Titles Where AuthorID = 7"
   End With
   Set Text1.DataSource = ADODC1
   Text1.DataField = "Title"
End Sub

ADO Data Control Events

The ADO Data control features several events that you can program. The table below shows the events and when they occur; however the table is not meant to be a complete list all of the conditions when the events occur. For complete information, see the reference topic for the individual event.

Event | Occurs
WillMove | On Recordset.Open, Recordset.MoveNext, Recordset.Move, Recordset.MoveLast, Recordset.MoveFirst, Recordset.MovePrevious, Recordset.Bookmark, Recordset.AddNew, Recordset.Delete, Recordset.Requery, Recordset.Resync
MoveComplete | After WillMove
WillChangeField | Before the Value property changes
FieldChangeComplete | After WillChangeField
WillChangeRecord | On Recordset.Update, Recordset.Delete, Recordset.CancelUpdate, Recordset.UpdateBatch, Recordset.CancelBatch
RecordChangeComplete | After WillChangeRecord
WillChangeRecordset | On Recordset.Requery, Recordset.Resync, Recordset.Close, Recordset.Open, Recordset.Filter
RecordsetChangeComplete | After WillChangeRecordset
InfoMessage | When the data provider returns a result

For More Information To try other step-by-step procedures using the ADO Data control, see "Creating a Simple Database Application with the DataGrid and ADO Data Control," "Creating a Simple DataCombo Application," and "Creating a DataGrid Linked to a DataList Control."


Setting Database-Related Properties of the ADO Data Control

When creating a connection, you can use one of three sources: a Connection String, an OLE DB file (.UDL), or an ODBC Data Source Name (DSN). When using a DSN, it's likely you will not have to alter any of the other properties of the control.

However, if you are familiar with database technology, you can alter some of the other properties exposed on the ADO Data Control. The following list describes database-related properties of the control. The list also suggests a logical order for setting the properties.

Note Database technology is complex, and the suggestions below are not meant to be taken as rules.

  1. ConnectionString — The ConnectionString property is a string that can contain all the settings necessary to make a connection. The parameters passed in the string are driver-dependent. For example, ODBC drivers allow the string to contain driver, provider, default database, server, username, and password.

  2. UserName — The name of the user, necessary if the database is password-protected. Like the Provider property, this property can be specified in the ConnectionString. If you provide both a ConnectionString and a UserName, the ConnnectionString value will override the UserName property's value.

  3. Password — Also needed when accessing a protected database. Like Provider and UserName, this property's value will be overridden if the value is specified in the ConnectionString.

  4. RecordSource — This property generally contains a statement that determines what will be retrieved from the database.

  5. CommandType — The CommandType property instructs the data provider if the Source is a SQL statement, a table name, a stored procedure, or an unknown type.

  6. CursorLocation — This property specifies where the cursor is located, on the client or on the server. The consequences of this decision affect the next few properties you set.

  7. CursorType — The CursorType property determines if the recordset is static, dynamic, or a keyset cursor type.

  8. LockType — The LockType determines how the data is locked when others attempt to change data you are editing. How you set the LockType is a complex decision, dependent on many factors.

  9. Mode — The Mode property determines what you intend to do with the recordset. For example, you can achieve some performance gains by setting it to read-only, if you are only interested in creating reports.

  10. MaxRecords — This property determines how large the cursor will be. How you determine this depends on the size of the records you are retrieving and the resources available on your computer (memory). A large record (one with many columns and large strings) would take more resources than a smaller record. Consequently the MaxRecords property should be no larger than necessary.

  11. ConnectionTimeout — Set the ConnectionTimeout to the number of seconds to wait while establishing the connection. An error is returned if the connection times out.

  12. CacheSize — The CacheSize property specifies how many records can be retrieved from the cursor. If you've set the CursorLocation to client side, then this property can be set to a smaller number (as small as 1) with no adverse effects. If it's on the server side, you should optimize this figure to suit the number of rows you want to view at any one time. For example, if you use the DataGrid control to view 30 rows, set the CacheSize to 60, allowing you to scroll without retrieving more data.

  13. BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).


Controls that Bind to the ADO Data Control

Any control that has the DataSource property can be bound to an ADO Data Control. The following intrinsic controls can be bound to the ADO Data Control:

  • CheckBox

  • ComboBox

  • Image

  • Label

  • ListBox

  • PictureBox

  • TextBox

The following data-bound ActiveX Controls are also supplied with all versions of Visual Basic:

  • DataList

  • DataCombo

  • DataGrid

  • Microsoft Hierarchical FlexGrid

  • RichTextBox

  • Microsoft Chart

  • DateTimePicker

  • ImageCombo

  • MonthView

Finally, you can create your own ActiveX controls that are data-bound using the DataBinding object.

For More Information See "Creating Data-Bound User Controls" for details about creating your own data-bound controls.


Using the Check Box Control

The check box control displays a check mark when it is selected. It is commonly used to present a Yes/No or True/False selection to the user. You can use check box controls in groups to display multiple choices from which the user can select one or more.

Figure 7.5 The check box control

The check box control is similar to the option button control in that each is used to indicate a selection that is made by the user. They differ in that only one option button in a group can be selected at a time. With the check box control, however, any number of check boxes may be selected.

For More Information See "Selecting Individual Options with Check Boxes" in "Forms, Controls, and Menus" for a simple demonstration of the check box control.

The Value Property

The Value property of the check box control indicates whether the check box is checked, unchecked, or unavailable (dimmed). When selected, the value is set to 1. For example:

The following table lists the values and equivalent Visual Basic constants that are used to set the Value property.

Setting | Value | Constant
Unchecked | 0 | vbUnchecked
Checked | 1 | vbChecked
Unavailable | 2 | vbGrayed

The user clicks the check box control to indicate a checked or unchecked state. You can then test for the state of the control and program your application to perform some action based on this information.

By default, the check box control is set to vbUnchecked. If you want to preselect several check boxes in a series of check boxes, you can do so by setting the Value property to vbChecked in the Form_Load or Form_Initialize procedures.

You can also set the Value property to vbGrayed to disable the check box. For example, you may want to disable a check box until a certain condition is met.

The Click Event

Whenever the user clicks on the check box control, the Click event is triggered. You can then program your application to perform some action depending upon the state of the check box. In the following example, the check box control's Caption property changes each time the control is clicked, indicating a checked or unchecked state.

Private Sub Check1_Click()
   If Check1.Value = vbChecked Then
      Check1.Caption = "Checked"
   ElseIf Check1.Value = vbUnchecked Then
      Check1.Caption = "Unchecked"
   End If
End Sub

Note If the user attempts to double-click the check box control, each click will be processed separately; that is, the check box control does not support the double-click event.

Responding to the Mouse and Keyboard

The Click event of the check box control is also triggered when the focus is shifted to the control with the keyboard by using the TAB key and then by pressing the SPACEBAR.

You can toggle selection of the check box control by adding an ampersand character before a letter in the Caption property to create a keyboard shortcut. For example:

In this example, pressing the ALT+C key combination toggles between the checked and unchecked states.

Visually Enhancing the Check Box Control

The check box control, like the command button and option button controls, may be visually enhanced by altering the setting of the Style property and then using the Picture, DownPicture, and DisabledPicture properties. For example, you may want to add an icon or bitmap to a check box or display a different image when the control is clicked or disabled.


Using the Combo Box Control

A combo box control combines the features of a text box and a list box. This control allows the user to select an item either by typing text into the combo box, or by selecting it from the list.

Figure 7.6 The combo box control

Combo boxes present a list of choices to the user. If the number of items exceeds what can be displayed in the combo box, scroll bars will automatically appear on the control. The user can then scroll up and down or left to right through the list.

When to Use a Combo Box Instead of a List Box

Generally, a combo box is appropriate when there is a list of suggested choices, and a list box is appropriate when you want to limit input to what is on the list. A combo box contains an edit field, so choices not on the list can be typed in this field.

In addition, combo boxes save space on a form. Because the full list is not displayed until the user clicks the down arrow (except for Style 1, which is always dropped down), a combo box can easily fit in a small space where a list box would not fit.

For More Information See "Using List Boxes and Combo Boxes" in "Forms, Controls, and Menus" for a simple demonstration of these controls. Also see "Using the List Box Control" later in this chapter for more information about the list box control.

Data-Bound Features

Visual Basic includes both standard and data-bound versions of the combo box control. While both versions allow you to display, edit, and update information from most standard types of databases, the data-bound combo box provides more advanced data access features. The Data-Bound combo box control also supports a different set of properties and methods than the standard combo box control.

For More Information See "Using the DataCombo and DataList Controls" in "Using Visual Basic's Standard Controls" for more information.

Combo Box Styles

There are three combo box styles. Each style can be set at design time and uses values, or equivalent Visual Basic constants, to set the style of the combo box.

Style | Value | Constant
Drop-down combo box | 0 | vbComboDropDown
Simple combo box | 1 | vbComboSimple
Drop-down list box | 2 | vbComboDropDownList

Figure 7.7 Combo box styles

Drop-down Combo Box

With the default setting (Style = 0 – Dropdown Combo), a combo box is a drop-down combo box. The user can either enter text directly (as in a text box) or click the detached arrow at the right of the combo box to open a list of choices. Selecting one of the choices inserts it into the text portion at the top of the combo box. The user also can open the list by pressing ALT+ DOWN ARROW when the control has the focus.

Simple Combo Box

Setting the Style property of a combo box to 1 – Simple Combo specifies a simple combo box in which the list is displayed at all times. To display all entries in the list, you must draw the list box large enough to display the entries. A vertical scroll bar is automatically inserted when there are more entries than can be displayed. The user can still enter text directly or select from the list. As with a drop-down combo box, a simple combo box also allows users to enter choices not on the list.

Drop-down List Box

A drop-down list box (Style = 2 – Dropdown List) is like a regular list box — it displays a list of items from which a user must choose. Unlike list boxes, however, the list is not displayed until you click the arrow to the right of the box. The key difference between this and a drop-down combo box is that the user can't type into the box, he can only select an item from the list. Use this type of list box when space is at a premium.

Adding Items

To add items to a combo box, use the AddItem method, which has the following syntax:

box.AddItem item[, index]

Argument | Description
box | Name of the list or combo box.
item | String expression to add to the list. If item is a literal constant, enclose it in quotation marks.
index | Specifies where the new item is to be inserted in the list. An index of 0 represents the first position. If index is omitted, the item is inserted at the end (or in the proper sorted order).

While list items are commonly added in the Form_Load event procedure, you can use the AddItem method at any time. This gives you the ability to add items to the list dynamically (in response to user actions).

The following code places "Chardonnay," "Fumé Blanc," "Gewürztraminer," and "Zinfandel" into a combo box named Combo1 with its Style property set to 0 (vbComboDropDown):

Private Sub Form_Load ()
   Combo1.AddItem "Chardonnay"
   Combo1.AddItem "Fumé Blanc"
   Combo1.AddItem "Gewürztraminer"
   Combo1.AddItem "Zinfandel"
End Sub

Whenever the form is loaded at run time and the user clicks the down arrow, the list appears as shown in Figure 7.8.

Figure 7.8 "Wine list" combo box

Adding Items at Design Time

You can also enter items into the list at design time by setting the List property in the Properties window of the combo box control. When you select the List property option and then click the down arrow, you can type list items and then press the CTRL+ENTER key combination to start a new line.

You can only add items to the end of the list. So, if you want to alphabetize the list, set the Sorted property to True. See "Sorting a List" below for more information.

Adding an Item at a Specified Position

To add an item to a list at a specific position, specify an index value after the new item. For example, the next line of code inserts "Pinot Noir" into the first position, adjusting the position of the other items downward:

Combo1.AddItem "Pinot Noir", 0

Notice that it is 0, not 1, that specifies the first item in a list (see Figure 7.9).

Figure 7.9 Adding an item to a list

Sorting a List

You can specify that items be added to a list in alphabetical order by setting the Sorted property to True and omitting the index. The sort is not case-sensitive; thus, the words "chardonnay" and "Chardonnay" are treated the same.

When the Sorted property is set to True, using the AddItem method with the index argument can lead to unpredictable, unsorted results.

Removing Items

You can use the RemoveItem method to delete items from a combo box. RemoveItem has one argument, index, which specifies the item to remove:

box.RemoveItem index

The box and index arguments are the same as for AddItem.

For example, to remove the first entry in a list, you would add the following line of code:

Combo1.RemoveItem 0

To remove all list entries in a combo box, use the Clear method:

Combo1.Clear

Getting List Contents with the Text Property

Usually, the easiest way to get the value of the currently selected item is to use the Text property. The Text property corresponds to whatever is entered in the text box portion of the control at run time. This can be either a selected list item or a string that a user types in the text box.

For example, the following code displays information about Chardonnay if a user selects "Chardonnay" from a list box:

Private Sub Combo1_Click ()
   If Combo1.Text = "Chardonnay" Then
      Text1.Text = "Chardonnay is a medium-bodied _
      white wine."
   End If
End Sub

The Text property contains the currently selected item in the Combo1 list box. The code checks to see if "Chardonnay" has been selected and, if so, displays the information in the text box.

Accessing List Items with the List Property

The List property provides access to all items in the list. This property contains an array in which each item in the list is an element of the array. Each item is represented in string form. To refer to an item in the list, use this syntax:

box.List(index)

The box argument is a reference to a combo box, and index is the position of the item. The top item has an index of 0, the next has an index of 1, and so on. For example, the following statement displays the third item (index = 2) in a list in a text box:

Text1.Text = Combo1.List(2)

Determining Position with the ListIndex Property

If you want to know the position of the selected item in a list in a combo box, use the ListIndex property. This property sets or returns the index of the currently selected item in the control and is available only at run time. Setting the ListIndex property for a combo box also generates a Click event for the control.

The value of this property is 0 if the first (top) item is selected, 1 if the next item down is selected, and so on. ListIndex is – 1 if no item is selected or if a user enters a choice in a combo box (Style 0 or 1) instead of selecting an existing item in the list.

Note The NewIndex property allows you to keep track of the index of the last item added to the list. This can be useful when inserting an item into a sorted list.

Returning the Number of Items with the ListCount Property

To return the number of items in a combo box, use the ListCount property. For example, the following statement uses the ListCount property to determine the number of entries in a combo box:

Text1.Text = "You have " & Combo1.ListCount & " _
entries listed"


Using the Command Button Control

The command button control is used to begin, interrupt, or end a process. When clicked, it invokes a command that has been written into its Click event procedure.

Figure 7.10 The command button control

Most Visual Basic applications have command buttons that allow the user to simply click them to perform actions. When the user chooses the button, it not only carries out the appropriate action, it also looks as if it's being pushed in and released and is therefore sometimes referred to as a push button.

For More Information See "Clicking Buttons to Perform Actions" in "Forms, Controls, and Menus" for a simple demonstration of the Command Button control.

Adding a Command Button to a Form

You will likely use one or more command buttons in your application. To add command buttons to a form, draw them on as you would any other control. Command buttons can be sized with the mouse or by setting their Height and Width properties.

Setting the Caption

To change the text displayed on the command button, use the Caption property. At design time, you can set this property by selecting it from the control's Properties window. When you set the Caption property at design time, the button text will be updated dynamically.

You can set the Caption property up to 255 total characters. If your caption exceeds the width of the command button, it will wrap to the next line. However, it will be clipped if the control cannot accommodate its overall height.

You can change the font displayed on the command button by setting its Font property.

Creating Keyboard Shortcuts

You can use the Caption property to create access key shortcuts for your command buttons by adding an ampersand (&) before the letter you want to use as the access key. For example, to create an access key for the caption "Print" you add an ampersand before the letter "P": "&Print". At run time, the letter "P" will be underlined and the user can select the command button by simultaneously pressing ALT+P.

Note To include an ampersand in a caption without creating an access key, include two ampersands (&&). A single ampersand is displayed in the caption and no characters are underlined.

Specifying the Default and Cancel Properties

On each form, you can select a command button to be the default command button — that is, whenever the user presses the ENTER key the command button is clicked regardless of which other control on the form has the focus. To specify a command button as default set the Default property to True.

You can also specify a default cancel button. When the Cancel property of a command button is set to True, it will be clicked whenever the user presses the ESC key, regardless of which other control on the form has the focus.

Selecting the Command Button

A command button can be selected at run time by using the mouse or keyboard in the following ways:

  • Use a mouse to click the button.

  • Move the focus to the button by pressing the TAB key, and then choose the button by pressing the SPACEBAR or ENTER.

  • Press an access key (ALT+ the underlined letter) for a command button.

  • If the command button is the default command button for the form, pressing ENTER chooses the button, even if you change the focus to a different control.

  • If the command button is the default Cancel button for the form, then pressing ESC chooses the button, even if you change the focus to another control.

The Value Property

Whenever the command button is selected, its Value property is set to True and the Click event is triggered. False (default) indicates the button isn't chosen. You can use the Value property in code to trigger the command button's Click event. For example:

cmdClose.Value = True

The Click Event

When clicked, the command button's Click event is triggered and the code you've written in the Click event procedure is invoked.

Clicking a command button control also generates the MouseDown and MouseUp events. If you intend to attach event procedures for these related events, be sure that their actions don't conflict. The order in which these three events occur varies from control to control. In the command button control, these events occur in this order: MouseDown, Click, MouseUp.

Note If the user attempts to double-click the command button control, each click will be processed separately; that is, the command button control does not support the double-click event.

For More Information See "Responding to Mouse and Keyboard Events" for more information on the MouseDown and MouseUp events.

Visually Enhancing the Command Button

The command button control, like the check box and option button controls, may be visually enhanced by altering the setting of the Style property and then using the Picture, DownPicture and DisabledPicture properties. For example, you may want to add an icon or bitmap to a command button or display a different image when the button is clicked or disabled.


Using the Common Dialog Control

The common dialog control provides a standard set of dialog boxes for operations such as opening and saving files, setting print options, and selecting colors and fonts. The control also has the ability to display Help by running the Windows Help engine.

Figure 7.11 The common dialog control

The common dialog control provides an interface between Visual Basic and the routines in the Microsoft Windows dynamic-link library Commdlg.dll. To create a dialog box using this control, Commdlg.dll must be in your Microsoft Windows \System directory.

You use the common dialog control in your application by adding it to a form and setting its properties. The dialog displayed by the control is determined by the methods of the control. At run time, a dialog box is displayed or the Help engine is executed when the appropriate method is invoked; at design time, the common dialog control is displayed as an icon on a form. This icon can't be sized.

The common dialog control allows you to display these commonly used dialog boxes:

  • Open

  • Save As

  • Color

  • Font

  • Print

To use the common dialog control

  1. If you haven't already done so, add the common dialog control to the toolbox by selecting Components from the Project menu. Locate and select the control in the Controls tabbed dialog, then click the OK button.

  2. On the toolbox, click the CommonDialog control and draw it on a form.

    When you draw a common dialog control on a form, it automatically resizes itself. Like the timer control, the common dialog control is invisible at run time.

  3. At run time, use the appropriate method, as listed in the following table, to display the desired dialog.
    Method | Dialog displayed
    ShowOpen | Open
    ShowSave | Save As
    ShowColor | Color
    ShowFont | Font
    ShowPrinter | Print
    ShowHelp | Invokes Windows Help


Displaying Open and Save As Dialog Boxes

The Open dialog box allows the user to specify a drive, a directory, a file name extension, and a file name.

The Save As dialog box is identical to the Open dialog in appearance, except for the dialog's caption, and file names appearing dimmed out. At run time, when the user chooses a file and closes the dialog box, the FileName property is used to get the selected file name.

Figure 7.12 An Open dialog box

To display the Open dialog box

  1. Specify the list of file filters that are displayed in the Files of type list box.

    You can do this by setting the Filter property using the following format:

    description1 | filter1 | description2 | filter2...

    Description is the string displayed in the list box — for example, "Text Files (*.txt)." Filter is the actual file filter — for example, "*.txt." Each
    description | filter set must be separated by a pipe symbol (|).

  2. Use the ShowOpen method to display the dialog box.

After the user chooses a file, use the FileName property to get the name of the selected file.

With all the common dialog boxes, when the CancelError property is True, an error is generated when the user clicks the dialog box's Cancel button. You detect that the Cancel button was pressed by trapping the error when the dialog box is displayed.

The following code displays an Open dialog box and uses the selected file name as an argument to a procedure that opens a file:

Private Sub mnuFileOpen_Click ()
   ' CancelError is True.
   On Error GoTo ErrHandler
   ' Set filters.
   CommonDialog1.Filter = "All Files (*.*)|*.*|Text _
   Files (*.txt)|*.txt|Batch Files (*.bat)|*.bat"
   ' Specify default filter.
   CommonDialog1.FilterIndex = 2

   ' Display the Open dialog box.
   CommonDialog1.ShowOpen
   ' Call the open file procedure.
   OpenFile (CommonDialog1.FileName)
   Exit Sub

ErrHandler:
' User pressed Cancel button.
   Exit Sub
End Sub


Using the Color Dialog Box

The Color dialog box allows the user to select a color from a palette or to create and select a custom color. At run time, when the user chooses a color and closes the dialog box, you use the Color property to get the selected color.

Figure 7.13 The Color dialog box

To display the Color dialog box

  1. Set the Flags property for the common dialog control to the Visual Basic constant cdlCCRGBInit.

  2. Use the ShowColor method to display the dialog box.

Use the Color property to get the RGB value of the color the user selects. The following code displays the Color dialog box when a user clicks the Command1 command button:

Private Sub Command1_Click ()
   ' Set Cancel to True.
   CommonDialog1.CancelError = True
   On Error GoTo ErrHandler
   ' Set the Flags property.
   CommonDialog1.Flags = cdlCCRGBInit
   ' Display the Color dialog box.
   CommonDialog1.ShowColor
   ' Set the form's background color to the selected
   ' color.
   Form1.BackColor = CommonDialog1.Color
   Exit Sub

ErrHandler:
   ' User pressed Cancel button.
   Exit Sub
End Sub

For More Information See "CommonDialog Control Constants" for a complete list of Flag property constants.


Using the Font Dialog Box

The Font dialog box allows the user to select a font by its size, color, and style. Once the user makes selections in the Font dialog box, the following properties contain information about the user's selection.

Property | Determines
Color | The selected color. To use this property, you must first set the Flags property to cdlCFEffects.
FontBold | Whether bold was selected.
FontItalic | Whether italic was selected.
FontStrikethru | Whether strikethrough was selected.
FontUnderline | Whether underline was selected.
FontName | The selected font name.
FontSize | The selected font size.

Figure 7.14 The Font dialog box

To display the Font dialog box

  1. Set the Flags property to one of the following Visual Basic constant values:
    • cdlCFScreenFonts (screen fonts)

    • cdlCFPrinterFonts (printer fonts)

    • cdlCFBoth (for both screen and printer fonts)

      Caution You must set the Flags property to one of these values before displaying the Font dialog box. Otherwise, the error No fonts exist occurs.

  2. Use the ShowFont method to display the dialog box.

The following code sets the font properties for a text box based on a user's selections in the Font dialog box:

Private Sub Command1_Click ()
   ' Set Cancel to True.
   CommonDialog1.CancelError = True
   On Error GoTo ErrHandler
   ' Set the Flags property.
   CommonDialog1.Flags = cdlCFBoth Or cdlCFEffects
   ' Display the Font dialog box.
   CommonDialog1.ShowFont
   ' Set text properties according to user's
   ' selections.
   Text1.Font.Name = CommonDialog1.FontName
   Text1.Font.Size = CommonDialog1.FontSize
   Text1.Font.Bold = CommonDialog1.FontBold
   Text1.Font.Italic = CommonDialog1.FontItalic
   Text1.Font.Underline = CommonDialog1.FontUnderline
   Text1.FontStrikethru = CommonDialog1.FontStrikethru
   Text1.ForeColor = CommonDialog1.Color
   Exit Sub
ErrHandler:
   ' User pressed Cancel button.
   Exit Sub
End Sub

For More Information See "CommonDialog Control Constants" for a complete list of Flag property constants.


Using the Print Dialog Box

The Print dialog box allows the user to specify how output should be printed. The user can specify a range of pages to be printed, a print quality, a number of copies, and so on. This dialog box also displays information about the currently installed printer and allows the user to configure or reinstall a new default printer.

Note This dialog box does not actually send data to a printer. It allows users to specify how they want data printed. You must write code to print the data in the format they select.

For More Information See "Working with Text and Graphics" for information on printing data.

At run time, when the user makes selections in the Print dialog box, the following properties contain information about the user's selection.

Property | Determines
Copies | The number of copies to print.
FromPage | The page to start printing.
ToPage | The page to stop printing.
hDC | The device context for the selected printer.
Orientation | The page's orientation (portrait or landscape).

Figure 7.15 The Print dialog box

To display the Print dialog box

  1. Set any desired default settings for the dialog by setting the appropriate Print dialog properties.

    For example, to display 2 in the Copies box when the dialog is displayed, set the Copies property to 2:

    CommonDialog1.Copies = 2
  2. Use the ShowPrinter method to display the Print dialog box.

The following code displays the Print dialog box when the user clicks the Command1 command button:

Private Sub Command1_Click ()
   Dim BeginPage, EndPage, NumCopies, Orientation, i
   ' Set Cancel to True.
   CommonDialog1.CancelError = True
   On Error GoTo ErrHandler
   ' Display the Print dialog box.
   CommonDialog1.ShowPrinter
   ' Get user-selected values from the dialog box.
   BeginPage   = CommonDialog1.FromPage
   EndPage       = CommonDialog1.ToPage
   NumCopies   = CommonDialog1.Copies
   Orientation = CommonDialog1.Orientation
   For i = 1 to NumCopies
   ' Put code here to send data to your printer.
   Next
   Exit Sub
ErrHandler:
   ' User pressed Cancel button.
   Exit Sub
End Sub

Note If the PrinterDefault property is set to True, you can print to the Visual Basic Printer object. In addition, when the PrinterDefault property is True, any changes the user makes in the Setup portion of the Print dialog box are used to change the printer settings in the user's Printer setup.


Using the ShowHelp Method to Display a Help File

The ShowHelp method of the common dialog control allows you to display a Help file.

To display a Help file using the ShowHelp method

  1. Set the HelpCommand and HelpFile properties.

  2. Use the ShowHelp method to display the specified Help file.

The following code displays the specified Help file when the user clicks the Command1 command button:

Private Sub Command1_Click()
   ' Set Cancel to True.
   CommonDialog1.CancelError = True
   On Error GoTo ErrHandler
   ' Set the HelpCommand Property
   CommonDialog1.HelpCommand = cdlHelpForceFile
   ' Specify the Help file.
   CommonDialog1.HelpFile = "c:\Windows\Cardfile.hlp"
   ' Display the Windows Help engine.
   CommonDialog1.ShowHelp
   Exit Sub

ErrHandler:
   ' User pressed Cancel button.
   Exit Sub
End Sub

For More Information See "HelpCommand Property," "HelpFile Property," and "ShowHelp Method" for more information on displaying Help files with the common dialog control.


Using the Data Control

The intrinsic Data control implements data access by using the Microsoft Jet Database engine –the same database engine that powers Microsoft Access. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The intrinsic Data control is best suited to smaller (desktop) databases, such as Access and ISAM databases.

You can use the intrinsic Data control to create applications that display, edit, and update information from many types of existing databases, including Microsoft Access, Btrieve, dBASE, Microsoft FoxPro®, and Paradox. You can also use it to access Microsoft Excel, Lotus 1-2-3, and standard ASCII text files as if they were true databases. In addition, the data control allows you to access and manipulate remote Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.

Note Both the Data control and Remote Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control."

The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, and scroll backwards and forwards through the recordset.

Creating a Simple Database Application with the Data Control

To create a simple database application with the Data control

  1. Draw a Data control on the form. The Data control is an intrinsic control and is always available.

  2. Click the Data control to select it, and press F4 the display the Properties window.

  3. In the Properties window, set the Connect property to the type of database you want to use.

  4. In the Properties window, set the DatabaseName property to the file or directory name of the database you want to connect to.

  5. On the Properties window, set the RecordSource property to the name of the database table you want to access.

  6. Draw a TextBox control on the form.

  7. Click the TextBox control to select it, and on the Properties window set the DataSource property to the Data control.

  8. On the Properties window, set the DataField property to the name of the field in the database you want to view or modify.

  9. Repeat steps 6, 7, and 8 for each additional field you want to access.

  10. Press F5 to run the application.

Setting Data-Related Properties of the Data Control

The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:

Note Database technology is a complex science, and the suggestions below are not meant to be taken as rules.

  1. RecordsetType — The RecordsetType property determines if the recordset is a table, dynaset, or snapshot. The choice affects what recordset properties are available. For example, snapshot-type recordsets are more limited than dynaset recordsets.

  2. DefaultType — The DefaultType property specifies whether JET or ODBCDirect workspaces are used.

  3. DefaultCursorType — The DefaultCursorType property determines the location of the cursor. You can allow the ODBC driver to determine the cursor location, or specify server or ODBC cursors. The DefaultCursorType property is valid only when using ODBCDirect workspaces.

  4. Exclusive — Determines if the data is for a single- or multi-user environment.

  5. Options — The property determines the characteristics of the recordset. For example, in a multi-user environment, you can set the Options property to deny changes made by others.

  6. BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).


Using the DataCombo and DataList Controls

The DataCombo and DataList controls strongly resemble the standard list box and combo box controls, but there are some important differences that give them great flexibility and usefulness in database applications. Both controls can be automatically filled from a database field from the data control to which they are bound. In addition, they can optionally pass a selected field to a second data control, making them ideal for "lookup table" applications.

Possible Uses

  • In a relational database, use the data from one table to supply values to be input into a second (related) table. For example, in an inventory database, the names of suppliers are stored in one table with each supplier having a unique ID. Another table showing products uses the unique ID to designate which company supplies the product. Use the DataList control to show the name of the supplier while (invisibly) supplying the ID of the supplier to the products table.

  • Allow the user to narrow a search by selecting a criteria from a drop-down list. For example, a sales-reporting database application might use a DataList control to let the user pick a State or Sales Region. Once that selection is made, the choice is automatically passed to a second data control that positions itself on sales records from the selected region.

Like their built-in counterparts, the main difference between the DataList and the DataCombo controls is that the DataCombo box control provides a text box into whose contents may be edited.

For More Information For an explanation of the DataList and DataCombo controls' ability to link database tables, see "Linking Two Tables using the DataCombo and DataList Controls." To build a simple application using linked tables, see "Creating a Simple DataCombo Application."

Noteworthy Properties of the Controls

Some important properties of the DataList and DataCombo controls include:

Property | Description
BoundText | Contains the value of the field named in the BoundColumn property.
SelectedItem | Returns a bookmark for the row of a selected item.
MatchEntry | Enables extended search mode for locating items in a list generated by the DataCombo control.
IntegralHeight | Sizes the control to display an exact number of complete rows.
VisibleCount | Specifies the number of visible items in a list.

Note The DataFormat property of the DataCombo control is an Extender property. Therefore it's always visible on the property sheet and can be set in code. However the DataCombo control only formats the top item in its list. This can be disconcerting to the end user who sees a formatted top item, but is only given a list of unformatted items to select from. The formatted item may also mislead end users who assume the item will be entered in the database as formatted. For these reasons, it's advisable to not set the DataFormat property when using the DataCombo control.

For More Information To try a step-by-step tutorial that demonstrates the use of the BoundText property, see "Creating a DataGrid Linked to a DataList Control." For a complete list of properties and methods of these controls, see "DataList Control" and "DataCombo Control."


Linking Two Tables Using the DataCombo and DataList Controls

The distinguishing characteristic of the DataCombo and DataList controls is the ability to access two different tables and link data from the first table to a field in the second. This is accomplished by using two data sources (such as the ADO Data control or the Data Environment).

Relational Tables and "Unfriendly" Values

In a relational database, information that is used repeatedly is not stored in its entirety in multiple places. Instead the bulk of the information is stored in a recordset comprised of many fields; among these fields is an ID field that uniquely identifies the recordset. For example, the Biblio database supplied with Visual Basic stores the names of several publishing companies in a table named "Publishers." The table contains many fields, such as address, city, zip code, and phone number. But for the sake of simplicity, consider the Name and PubID fields as the two essential fields in the table. The Name field stores the name of a publisher, while the PubID field stores a comparatively "unfriendly" value such as a number or code. But this unfriendly value is more important because it uniquely identifies the publisher, and serves as a link to the entire recordset. And it's that value that is stored in multiple recordsets in a second table.

The second table is named "Titles," and each recordset contains information such as Title, year of publication, and ISBN. Included among the fields is one named "PubID." This field is named exactly the same as the corresponding field in the Publishers table because it stores the value that links the title to a specific publisher.

This efficient scheme presents a small problem: Given a database application that allows users to insert new titles, the user must somehow input integers that identify the publisher. This is fine if the user has memorized each publisher's unique ID, but it would be easier for people to see the name of the publisher instead, and have the application store the associated value in the database. The DataList and DataCombo controls solve this problem easily.

Two Data Sources, Three Fields, No Coding

The DataList and DataCombo controls use two sources to surmount the problem. While displaying only the name of the publisher (from the Publishers table), the DataList or DataCombo control writes only the value of the PubID field to the Titles table. Through the Properties window, set the RowSource to the data source that will supply the data to be written (the Publishers table). Then set the DataSource property to the data source that will be written to (the Titles table). Finally, set the DataField, ListField, and BoundColumn properties. The figure below shows how the two data sources (in the form of two Data controls) and the three fields are assigned to a DataCombo control:

In brief, the ListField property determines which field is actually displayed by the control. In this case, it is the name of the publisher. The BoundColumn property, on the other hand, determines which field in the Publishers table supplies the actual value to the Titles table. Note that the PubID field in the Publishers table can't (and shouldn't) be edited. Instead, the value found in the PubID field is written to the field specified by the DataField property. In this case, it is the PubID of the Titles table.

The table below summarizes the properties and how to use them.

Property | Description
DataSource | The name of the data control to which the DataList or DataCombo control is bound.
DataField | The name of a field in the recordset specified by the DataSource property. This field will be used to determine which element in the list will be highlighted. If a new selection is made, it is this field that will be updated when you move to a new record.
RowSource | The name of the data control that will be used to fill the list.
BoundColumn | The name of a field in the recordset specified by the RowSource property. This field must be of the same type as the DataField that will be used to update the list.
ListField | The name of a field in the recordset specified by RowSource that will be used to fill the list.

Note You can also use the DataList and DataCombo controls with a single data control. To do this, set both the DataSource and RowSource properties to the same data control, and set the DataField and BoundColumn properties to the same field in the data control’s recordset. In this case, the list will be filled with ListField values from the same recordset that is updated. If a ListField property is specified, but no BoundColumn property is set, BoundColumn will automatically be set to the ListField field.

For More Information To try a step-by-step procedure that builds a simple database application with the DataCombo control, see "Creating a Simple DataCombo Application."


Creating a Simple DataCombo Application

The following example uses the DataCombo box control to create a data entry screen for the Titles table of the Northwind.mdb sample database. It lets the user enter new products and assign them to existing suppliers by providing a lookup table of all the suppliers' names. When users get to the Supplier field in the entry form, they can choose a supplier from a list box. When they select a supplier, that supplier's SupplierID field is copied into the SupplierID field of the Products table.

To create a lookup table with the DataCombo control

  1. Create an OLEDB Data Source for the Northwind database.

    If a Data Source has not been created, follow the steps in "Creating the Northwind OLEDB Data Source."

  2. Create a new Standard EXE project in Visual Basic.

    If the DataGrid, DataCombo, or ADO Data Control is not present in the Toolbox, right-click the Toolbox, and use the Components dialog box to add it.

  3. Add a DataCombo, two ADO Data controls, and a DataGrid control to your form.

  4. In the Properties window, set the properties of the first data control (Adodc1) as shown in the table below.
    Property | Setting
    Name | adoDataSource
    ConnectionString | Northwind.udl
    RecordSource | Select * From Products;
    Caption | Products

  5. In the Properties window, set the properties of the second data control (Adodc2) as shown in the table below.
    Property | Setting
    Name | adoRowSource
    ConnectionString | Northwind.udl
    RecordSource | Select CompanyName, SupplierID From Suppliers;
    Caption | Suppliers
    Visible | False

  6. In the Properties window, set the properties of the DataGrid control as shown in the table below.
    Property | Setting
    Name | grdProducts
    DataSource | AdoDataSource
    Caption | Products

  7. In the Properties window, set the properties of the DataCombo control as shown in the table below.
    Property | Setting
    Name | dcbSuppliers
    DataSource | adoDataSource
    DataField | SupplierID
    RowSource | adoRowSource
    ListField | CompanyName
    BoundColumn | SupplierID

  8. Finally, add the following code to the form's code module:
    Private Sub Form_Load()
   ' Hide the SupplierID field in the DataGrid control, so the user is
   ' not confused on which value to change.
grdProducts.Columns("SupplierID").Visible = False
End Sub
  9. Run the project.

You can navigate through the recordset by clicking the arrows on the visible ADO Data control. As you do so, the DataCombo control will update and display the name of the supplier for each product. To edit the SupplierID field, click the DataCombo control's arrow to display a drop-down list, then click again on a different supplier to change the value written to the SupplierID field.


Using the DataGrid Control

The DataGrid control is a spreadsheet-like bound control that displays a series of rows and columns representing records and fields from a Recordset object. You can use the DataGrid to create an application that allows the end user to read and write to most databases. The DataGrid control can be quickly configured at design time with little or no code. When you set the DataGrid control's DataSource property at design time, the control is automatically filled and its column headers are automatically set from the data source's recordset. You can then edit the grid's columns; delete, rearrange, add column headers to, or adjust any column's width.

At run time, the DataSource can be programmatically switched to view a different table, or you can modify the query of the current database to return a different set of records.

Note The DataGrid control is code-compatible with the DBGrid control that shipped in Visual Basic 5.0 with one exception: the DataGrid control doesn't support the DBGrid notion of "unbound mode." The DBGrid control is included with Visual Basic in the Tools directory.

Possible Uses

  • View and edit data on a remote or local database.

  • Used in conjunction with another data-bound control, such as the DataList control, use the DataGrid control to display records from one table that are linked through a common field to another table displayed by the second data-bound control.

Using the Design-Time Features of the DataGrid Control

You can create a database application with the DataGrid control without writing a line of code by taking advantage of its design-time features. The following instructions outline the general steps needed to implement the DataGrid control in a typical use. For complete step-by-step instructions, see the topic "DataGrid Scenario 1: Create a Simple Database Application with the DataGrid Control."

To implement a DataGrid control at design-time

  1. Create a Microsoft Data Link (.UDL) file for the database you wish to access. See the topic "Creating the Northwind OLE DB Data Link" for an example.

  2. Place an ADO Data control on a form, and set the ConnectionString property to the OLE DB data source created in step 1.

  3. In the RecordSource field of the Ado Data control, type a SQL statement that returns a recordset. For example,
    Select * From MyTableName Where CustID = 12
  4. Place a DataGrid control on a form, and set the DataSource property to the ADO Data control.

  5. Right-click the DataGrid control and then click Retrieve Fields.

  6. Right-click the DataGrid control and then click Edit.

  7. Resize, delete, or add columns to the grid.

  8. Right-click the DataGrid control and then click Properties.

  9. Using the Property Pages dialog box, set the appropriate properties of the control to configure the grid as you wish it to appear and behave.

Changing Displayed Data at Run Time

Once you have created a grid using the design-time features, you may also wish to dynamically change the data source of the grid at run time. The general methods for accomplishing this are discussed below.

Changing the RecordSource of the DataSource

The most common method of changing displayed data is to alter the query of the DataSource. For example, if the DataGrid control uses an ADO Data control as its DataSource, rewriting the RecordSource and refreshing the ADO Data control will change the data displayed.

' The ADO Data control is connected to the Northwind database's
' Products table. The new query asks for all records which have
' the SupplierID = 12.
Dim strQuery As String
strQuery = "SELECT * FROM Suppliers WHERE SupplierID = 12"
Adodc1.RecordSource = strQuery
Adodc1.Refresh

Changing the DataSource

At run-time you can reset the DataSource property to a different data source. For example, you may have several ADO Data controls, each connected to different databases, or set to different RecordSource properties. Simply reset the DataSource from one ADO Data control to another:

' Reset the DataSource to an ADO Data control that is connected to
' the Pubs database, using the Authors table.
Set DataGrid1.DataSource = adoPubsAuthors

Rebind the DataSource

When using the DataGrid control with a remote database such as SQLServer, it's possible that the structure of the table may become altered. For example, a field may be added to the table. In that case, you can invoke the Rebind method to recreate the grid from the new structure. Note that if you have altered the columns' layout of the grid at design-time, the DataGrid control will attempt to recreate the current layout, including any empty columns. You can, however, force the grid to reset all columns by first invoking the ClearFields method.

Returning Values from the DataGrid

Once the DataGrid is connected to a database, you may want to monitor which cell the user has clicked. Use the RowColChange event — not the Click event — as shown below:

Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
   ' Print the Text, row, and column of the cell the user clicked.
   Debug.Print DataGrid1.Text; DataGrid1.Row; DataGrid1.Col
End Sub

Using the CellText and CellValue Methods

The CellText and CellValue properties are useful when a column has been formatted using the NumberFormat property. The NumberFormat property changes the format of any column that contains a number without changing the format of the actual data. For example, given a grid with a column named ProductID that contains integers, the code below will cause the DataGrid to display the values in the format "P-0000." In other words, although the actual value held in the ProductID field is "3," the value displayed by the grid will be "P-0003."

Private Sub Form_Load()
   DataGrid1.Columns("ProductID").NumberFormat = "P-0000"
End Sub

To return the actual value contained in the database, use the CellValue method, as shown below:

Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
   Debug.Print _
   DataGrid1.Columns("ProductID").CellValue(DataGrid1.Bookmark)
End Sub

Note Both the CellValue used above, and the CellText value used below, require the bookmark property as an argument to function correctly.

Conversely, if you want to return the formatted value of the field, use the CellText method:

Private Sub DataGrid1_RowColChange(LastRow As Variant, ByVal LastCol As Integer)
   Debug.Print _
   DataGrid1.Columns("ProductID").CellText(DataGrid1.Bookmark)
End Sub

Note The CellText method above is equivalent to using the Text property of the DataGrid control.

Where to Go From Here

To read a step-by-step procedure for building a simple application with the control, see "Creating a Simple Database Application with the DataGrid Control," or "Creating a DataGrid Linked to a DataList Control."

To learn more about Split objects and how to program them, see "Manipulating DataGrid Views."


Creating the Northwind OLE DB Data Link

An essential step in accessing data is creating an OLE DB data source for each database you want to access. The steps below create such an object for the Nwind.mdb (Northwind), supplied with Visual Basic. This data source is used in some of the example procedures supplied with the Visual Basic documentation. You need to create the OLE DB data source only once on a computer.

To create the Northwind OLE DB data source

  1. Open Windows Explorer, or Windows NT Explorer.

  2. Open a directory where you want to create the OLE DB data source. In this example, open Program Files, Microsoft Visual Studio, and VB98.

  3. Right-click the right pane of Explorer, and then click New on the context menu. From the list of file types, click Microsoft Data Link.

  4. Rename the new file Northwind.UDL.

  5. Right-click the file and click Properties on the context menu to display the Northwind.UDL Properties dialog box.

  6. Click the Connection tab.

  7. Click the Provider box and select Microsoft Jet 3.51 OLE DB Provider.

  8. Click the Next button to go to the Connection tab.

  9. Click the ellipsis button (…) next to the first text box.

  10. Use the Select Access Database dialog box to navigate to the nwind.mdb file, which is installed in the Program Files\Microsoft Visual Studio\Vb98 directory.

  11. Click Test Connection to check the connection.

  12. If the connection passes, click OK.

    Note You can also create an OLE DB data source by going to the Control Panel and clicking the Data Links icon. On the Organize Data Link Files dialog box, click New to create a new data source.


Creating a Simple Database Application with the DataGrid and ADO Data Control

Using only a DataGrid and an ADO Data control you can create a database application that allows the end user to read and write to a recordset.

Create a simple database application using the ADO Data Control

  1. Create an OLE DB data source for the Northwind database.

    If a Data Source has not been created, follow the steps in "Creating the Northwind OLE DB Data Link."

  2. Create a new Standard EXE project in Visual Basic.

    If the DataGrid control is not present in the Toolbox, right-click the Toolbox, and use the Components dialog box to load it. Also load the ADO control.

  3. Place an instance of each control on the blank form.

  4. Set the ADO Control's ConnectionString property to the Northwind data source.

    Click the ADO Data control to select it, and press F4 to make the Properties window appear. Click ConnectionString and then click OLE DB File. Click the Northwind Data Source.

  5. Set the ADO Control's RecordSource property.

    On the Properties window, click RecordSource and type a SQL statement to populate the DataGrid control. In this case, type in Select * From Products.

  6. Set the DataGrid Control's DataSource property to the ADO Data control.

    Click the DataGrid control to select it. On the Properties window, click DataSource and a drop-down list of all data controls will be presented — in this case only the ADO Data control. Click the control.

  7. Press F5 to run the project.


Creating a DataGrid Linked to a DataList Control

A common use of the DataGrid is to show "details" supplied by one table in a database. For example, the Northwind (Nwind.mdb) database includes two tables, one named "Suppliers," and the other named "Products." In this example, we'll use the DataList control to show the company names of suppliers from the "Suppliers" table. When the user clicks on any company name, the DataList control will furnish the SupplierID for the company. Using that ID, a query can be constructed to retrieve all records in the "Products" table which have a matching SupplierID. In other words, when the user clicks on a company (in the DataList control), all of the products produced by that company will appear in the DataGrid control.

To fill a DataGrid Control with products from a particular supplier

  1. Ensure that an OLE DB data source for the Northwind database is present on the machine; if such a datas source has not been created, follow the steps in "Creating the Northwind OLE DB Data Link."

  2. Create a new standard EXE project in Visual Basic.

    If the DataGrid and DataList and ADO Data controls are not present in the Toolbox, right-click the Toolbox, and click Components. In the Components dialog box double-click Microsoft DataGrid Control, Microsoft DataList Controls and Microsoft ADO Control.

  3. Place an instance of the DataGrid and DataList controls on the blank form.

    Place the DataList control in the top left corner of the form, and place the DataGrid control somewhat below it.

  4. Place two instances of the ADO Data control on the form.

    Select the first ADO Data control and press F4 to display its Properties page. Set the Name property of the control to adoSuppliers. Select the second ADO Data control and set its Name property to adoProducts. Place the first control directly underneath the DataList Control, and the second directly below the DataGrid control.

  5. Set the ConnectionString property of the two ADO Data controls to the Northwind OLE DB data source.

    Select the control named adoSuppliers and set the ConnectionString property to the Northwind OLE DB data source (Northwind.udl). Select the control named adoProducts and repeat the operation.

  6. Set the RecordSource property of the two ADO Data controls.

    Select adoSuppliers and click RecordSource on the Properties page. Type Select * From Suppliers. This query instructs the ADO Data control to return all records in the Suppliers table. Select adoProducts, click RecordSource, and type Select * From Products. This query returns all the records from the Products table.

  7. Set the RowSource property of the DataList Control to adoSuppliers.

    The RowSource property determines which data source supplies the data for the ListField property.

  8. Set the ListField property of the DataList control to CompanyName.

    The ListField property is set to the name of a field in the table named Suppliers. At run-time, the DataList control displays the value of the field specified in this property. In this example, the property will display the name of a company found in the Suppliers table.

  9. Set the BoundColumn property of the DataList control to SupplierID.

    The BoundColumn property is set to a second field in the Suppliers table. In this case, the property is set to the SupplierID field. When the DataList control is clicked, the BoundText property returns the value of the SupplierID field associated with the company displayed in the DataList control. This value will be used in a query of the Products table, which provides data for the DataGrid control.

  10. Set the DataSource property of the DataGrid control to adoProducts.

    The DataSource property specifies the data source for the control. In this case, the property is set to the ADO Data control named adoProducts, which returns all of the records in the Products table.

  11. In the code module for the form, add the following:
    Private Sub Datalist1_Click()
   ' Declare a string variable that will contain a new query. The
   ' new query uses the BoundText property of the DataList control
   ' to supply a SupplierID value. The new query simply asks for
   ' all products with the same SupplierID. This query is assigned
   ' to the RecordSource property of the ADO Data control named
   ' adoProducts. After refreshing the control, the DataGrid is
   ' updated with the new recordset of all products that are
   ' supplied by the same company.

   Dim strQuery As String
   strQuery = "Select * FROM Products WHERE SupplierID = " & _
   Datalist1.BoundText

   With adoProducts
      .RecordSource = strQuery
      .Refresh
   End With

   With DataGrid1
      .ClearFields
      .ReBind
   End With
End Sub
  12. Run the project.

    Click any company name in the DataList control, and the DataGrid control is automatically updated with all products supplied by the company.


Working with Columns

You can dynamically change the data displayed in the DataGrid control by changing the DataSource property. For example, you can display a different table from the same database. If you do so, the DataGrid control will display the data with only default properties.

Adding, Deleting, or Hiding Columns

You can programmatically add, delete, or hide Columns by using the properties and methods of the Columns collection and the Column object.

Adding and Deleting a Column

To add a column at run time, use the Add method. If you first declare a variable and assign the new object to the variable, you can set various properties with concise code.

Private Sub AddColumn()
   ' Add a column in the rightmost position. Then set its Visible, Width,
   ' Caption, and Alignment properties. The DataField property specifies
   ' which field the column will be bound to.
   Dim c As Column
   Set c = DataGrid1.Columns.Add(DataGrid1.Columns.Count)
   With c
      .Visible = True
      .Width = 1000
      .Caption = "My New Column"
      .DataField = Adodc1.Recordset.Fields("ProductName").Name
      .Alignment = dbgRight
   End With
End Sub

You can delete any column by using the Remove method. Be sure to specify which column using the ColIndex argument. The following code will remove the clicked column.

Private Sub DataGrid1_HeadClick(ByVal ColIndex As Integer)
   DataGrid1.Columns.Remove ColIndex
End Sub

Hiding a Column

You can hide any column by setting its Visible property to False. This is especially useful when you want to limit columns that the user can view or edit. The example below simply iterates through the Columns collection, hiding all but a few columns.

Private Sub HideColumns()
   ' Use the DataField property to determine which column is being
   ' tested. Show only three columns: ProductName, UnitPrice, and
   ' UnitsInStock.

   Dim c As Column
   For Each c In DataGrid1.Columns
      Select Case c.DataField
      Case "ProductName"
         c.Visible = True
      Case "UnitPrice"
         c.Visible = True
      Case "UnitsInStock"
         c.Visible = True
         c.Caption = "In Stock" ' Change the column header.
      Case Else ' Hide all other columns.
         c.Visible = False
      End Select
   Next c
End Sub


Manipulating DataGrid Views

A grid that is "split" allows the end user to have more than one view on the same data. For example, imagine that you have a large table that consists of ten fields. In this case, the recordset viewed in the DataGrid control will be ten columns wide, and unless your form is very wide, the user will not be able to see more than a few columns. Further, imagine that the user is only interested in the first and last columns (for example, a name in the first column, and phone number in the last). In order to view both columns side-by-side (without having to rearrange the order of the columns), the grid can be split.

Creating a Split Object

At design time, you can create a split by right-clicking the grid, clicking Edit, right-clicking again, and then clicking Split. You can then edit the Split by right-clicking the control and clicking Properties to display the Property Pages dialog box. Use the Splits tab to customize the split. To remove a split, right-click the split and click Remove.

At run time, the end user can also split the grid manually (unless such an operation is disallowed) by clicking the tab located to the right of the grid control's lower left edge, as shown in the figure below:

By default, the DataGrid control contains one Split object. The code to prevent the end user from adding splits is:

DataGrid1.Splits(0).AllowSizing = False

Programmatically Adding and Deleting Splits

The DataGrid control contains a collection of Split objects. In order to add splits programmatically, use the Add method, as shown below:

DataGrid1.Splits.Add 1

Note The Add method requires the index of the new split as an argument. To append a split, set the index argument to the Count property of the Splits collection.

Using the Split collection's Add method, you can programmatically add splits as you require them. Since adding more than two splits can make the grid hard to use, you may wish to limit the number of splits using the Count property of the collection.

If DataGrid1.Splits.Count < 3 Then ' Append a split.
   DataGrid1.Splits.Add DataGrid1.Splits.Count
End If

Synchronizing Splits

When you have more than one split, you may wish to control how the splits scroll. For example, in a grid with three splits, you may decide to synchronize only the first and third splits, while allowing the middle split to scroll independently. To synchronize any two (or more) splits, set the ScrollGroup property of each Split object to the same value.

' Synchronize the first and third Split objects.
With DataGrid1
   .Splits(0).ScrollGroup = 1
.Splits(1).ScrollGroup = 2
.Splits(2).ScrollGroup = 1
End With

You can further customize the appearance of the split by setting the Scrollbars property to display only one scrollbar for the synchronized group.

Controlling Tab and Arrow Key Behavior

Using the WrapCellPointer, TabAcrossSplits, and TabAction properties you can determine how the grid will behave when the end user presses the tab or arrow keys.

Of these three properties, the TabAction property is at the highest level as it governs whether or not the WrapCellPointer and the TabAcrossSplits properties have any effect at all. The TabAction has three settings: Control Navigation, Column Navigation, and Grid Navigation. When the property is set to Control Navigation, pressing the tab key switches the focus to the next control in the TabIndex. This setting preempts WrapCellPointer and TabAcrossSplits.

The WrapCellPointer property determines how the tab and arrow keys behave within any single split. If the property is set to True, and the current cell is in the last column, when the end user presses the tab key the next row of the first column will become the current cell. However, if the current cell is in the last column of the last row, there will be nowhere to "wrap" to.

The TabAcrossSplits property determines how the tab and arrow keys behave when there are two or more splits on the grid. If the property is set to True, and the current cell is on the last column of any split but the last, pressing the tab or arrow key will cause the current cell to "jump" to the first column in next split. The current cell will remain in the same row.

Note If both the WrapCellPointer and TabAcrossSplits properties are set to True, the current cell will not wrap until it is on the last column of the last split. It will then wrap to the next row in the first column of the first split.

Customizing the Columns Collection

Every Split object has a Columns property that allows you to manipulate a collection of Column objects. By doing so, you can change the appearance of every Split object. For example, one split may contain two columns showing first and last name fields, while a second split could show telephone and address fields. To accomplish this, hide the other columns by setting the Visible property of each to False, as shown below.

' Enumerate through the Columns collection to test each Column
' object's DataField property. If the test fails, hide the column.
Dim i As Integer

' Hide all but the ProductName column.
For i = 0 To DataGrid1.Splits(0).Columns.Count - 1
   If DataGrid1.Splits(0).Columns(i).DataField <> "ProductName" Then
      DataGrid1.Splits(0).Columns(i).Visible = False
   End If
Next i

' Hide all but the UnitPrice column.
For i = 0 To DataGrid1.Splits(0).Columns.Count - 1
   If DataGrid1.Splits(1).Columns(i).DataField <> "UnitPrice" Then
      DataGrid1.Splits(1).Columns(i).Visible = False
   End If
Next I


Tracking Records with Bookmarks and SelBookmarks

Bookmarks and SelBookmarks provide a means to track records. This is necessary when you program special functionality into an application, such as allowing the end user to manually select several discontiguous records, and performing a bulk update of the selected records. In that case, you will need to track which records have been selected, and you would therefore use the SelBookmarks collection and its properties.

Two functions, the CellText and CellValue methods, require bookmarks to perform properly.

Tracking User Selections

The SelBookmarks collection contains the bookmarks of all selected records. When the end user manually selects records (by holding down the CTRL key while clicking) the bookmark for each selected record is added to the collection. Using standard iteration methods, you can determine what has been selected, store the bookmarks (in case you need to restore a value), and perform the operation:

Dim i as Integer ' Counter
Dim intCount As Integer
intCount = DataGrid1.SelBookmarks.Count - 1
ReDim arrSelBK(intCount) ' Declare array to hold bookmarks.
For i = 0 To intCount
   ArrSelBK(i) = DataGrid1.SelBookmarks(i)
   ' Perform operations here. If the operations must be
   ' cancelled, exit the loop and use the array to
   ' rollback the changes.
Next i

Programmatically Select Records by Adding to the SelBookmarks Collection

You can also programmatically select records by adding them to the collection. For example, you may have a grid that shows all of the orders by a certain customer. To highlight all of the records where the customer spent more than $100, filter the records, and add the resulting bookmarks to the SelBookmarks collection.

Dim rs As Recordset
Set rs = Adodc1.Recordset

While Not rs.EOF
   If rs!SupplierID = 12 Then
      DataGrid1.SelBookmarks.Add rs.Bookmark
   End If
   rs.MoveNext
Wend


Displaying Calculated Fields

Suppose you have a field in a table named "Price," and you want to calculate the tax on every item in the table using the local tax rate. That is a calculated field, which you can create by modifying the query of the DataSource to calculate the value, and returning that value to the DataGrid control.

To create a calculated field in the DataGrid control

  1. Ensure that an OLE DB data source for the Northwind database is present on the machine; if such a data source has not been created, follow the steps in "Creating the Northwind OLE DB Data Link."

  2. Place an ADO Data control and a DataGrid control on the form.

  3. Set the ConnectionString property of the ADO Data control to the Northwind Data Source.

  4. Set the RecordSource property of the ADO Data control.

    On the Properties window, click RecordSource and type Select ProductName, UnitPrice, (UnitPrice * .082) As Tax From Products.

  5. Set the DataSource property of the DataGrid control to the ADO Data control.

  6. Run the project.


Using the DataGrid Control with a Class Module

If the data you want to access exists in a custom format, or in a form not directly supported by an ODBC driver, you can create a class to encapsulate the data. You can then program the class with customized functions to retrieve the data. The class then becomes a data source that can be used by any data consumer, such as the DataGrid control.

In the class module's Initialize event, you first create an ADODB recordset object by declaring a variable as New ADODB.Recordset. After creating the recordset object, append fields to it, one for each field in your data source. Then fill the recordset with the appropriate data.

Note You can also create a data source using an OLEDB Simple Provider. See "Creating a Component with a Data Provider" for more information about OLEDB Simple Providers.

The class module features a GetDataMember event that occurs whenever a data consumer (such as the DataGrid control) requests data. In the event, the Data argument is set to the recordset object created in the Initialize event.

To use the class module, create a form with a DataGrid control on it. In the form's Load event, place code that sets the control's DataSource property to the class.

Note The class module won't be available at design-time. For example, with the DataGrid control, all available data sources appear in a drop-down list when the user clicks DataSource on the Properties window. The class module will not appear among them, and can only be set through code.

Create a Data Source Using the Class Module

The example below uses a class module to create a simple data source. The DataGrid control is then bound to the module through the DataSource property.

To create a class for use with the DataGrid

  1. Create a new Standard Exe project.

  2. Add a DataGrid control to the form.

    If the DataGrid control is not available in the Toolbox, on the Project menu, click Components. Click Microsoft DataGrid Control, then click OK.

  3. On the Project menu, click References. On the References dialog box, click Microsoft ActiveX Data Objects 2.0 Library.

  4. On the Project menu, click Add Class Module to add a class module to the project.

  5. In the Project Explorer window, click the Class icon to select it, and press F4 to display the Properties window.

  6. On the Properties window, change the name of the class to NamesData.

  7. On the Properties window, click DataSourceBehavior and change the property to vbDataSource.

  8. In the Declarations section of the class module, create an ADODB Recordset variable, as shown below:
    Option Explicit
Private WithEvents rsNames As ADODB.RecordSet

    Declaring the variables using the WithEvents keyword allows you to program the RecordSet object events.

  9. In the class Initialize event, add the following code:
    Private Sub Class_Initialize()
   ' Add the names of the new datamember to the DataMember collection
   ' This allows other objects to see the available DataMembers
   DataMembers.Add "Names"

   Set rsNames = New ADODB.RecordSet ' Set the object variable.

   ' Create a recordset with two fields and open the recordset. The
   ' first record has an integer data type and the second is a string,
   ' with a maximum of 256 characters. The CursorType is set to
   ' OpenStatic--an updatable snapshot of a set of records. The
   ' LockType is set to LockOptimistic to allow updates to the
   ' recordset
   With rsNames
      .Fields.Append "ID", adInteger
      .Fields.Append "Name", adBSTR, 255
      .CursorType = adOpenStatic
      .LockType = adLockOptimistic
      .Open
   End With

   Dim i As Integer
   For i = 1 to 10 ' Add ten records.
      rsNames.AddNew
      rsNames!ID = i
      rsNames!Name = "Name " & i
      rsNames.Update
   Next i
   rsNames.MoveFirst ' Move to the beginning of the recordset.
End Sub

    The code first creates the recordset object, then appends two fields to the recordset. The code then adds ten records to the recordset.

  10. In the class GetDataMember event, add the following code:
    Private Sub Class_GetDataMember(ByVal DataMember As String, _
Data As Object)
   Set Data = rsNames
End Sub

    The code returns the recordset object whenever the event occurs — whenever the class object is bound to a data consumer, such as the DataGrid control.

  11. In the Form object's code module, declare an object variable for the class:
    Option Explicit
Private datNames As NamesData ' Class variable
  12. In the Form object's Load event, add the code to set the DataGrid control's DataSource to the class object.
    Private Sub Form_Load()
   ' Create a new NamesData Object
   Set datNames = New NamesData

   ' Bind the DataGrid to the new DataSource datNames
    Set DataGrid1.DataSource = datNames
End Sub
  13. Press F5 to run the project.

Programming the RecordSet Events

You can also program the events of the Recordset object. In the class module, click the Object Box (in the upper left corner), and then click rsNames. In the Procedures/Events box (upper right corner), the drop-down list will display all of the events of the Recordset object.

Add a Property to the Class

The class module can also be modified to respond to events or function calls. The code below shows how you can first add a property to the class. When invoked from another object, the property returns the RecordCount of the class.

Public Property Get RecordCount() As Long
   RecordCount = rsNames.RecordCount
End Sub

Using the DataMember Property

The GetDataMember event also includes the DataMember argument. Using this argument, you can include more than one recordset in the class module, and return the appropriate recordset by using a Select Case statement with the DataMember argument:

Private Sub Class_GetDataMember(ByVal DataMember As String, Data As _
Object)
   Select Case DataMember
   Case "Names"
      Set Data = rsNames
   Case "Dates"
      Set Data = rsDates
   Case Else
      ' Set a default data member.
      Set Data = rsYears
   End Select
End Sub

To specify which DataMember you want, set the DataMember property of the data consumer to the appropriate string, then set the DataSource as usual. For the DataGrid control, this would be:

Private Sub Form_Load()
   ' Create a new NamesData Object
   Set datNames = New NamesData

   ' Specify which DataMember you want, then set DataSource.
   DataGrid1.DataMember = "Names"
   Set DataGrid1.DataSource = datNames

End Sub


Examining the File System

Each of the file-system controls has been carefully designed to combine flexible and sophisticated file-system inspection capabilities with easy programming. Each control performs its file-data retrieval tasks automatically, but you can write code both to customize their appearance and to specify which information they display.

Figure 7.16 The file-system controls

You can use file-system controls singly or in combination. With combinations, you can write code in each control's event procedures to determine how they interact. Or you can let them act independently. Figure 7.17 shows the three controls used together.

Figure 7.17 The file-system controls used together

File-system controls obtain all their information from the operating system automatically; you can access this information or determine what is displayed by each control through its properties. For example, the contents of the current working directory is displayed by default (that is, the directory from which the application was launched, or what became the current directory as the result of a ChDir statement).

Your application can also display a list of the files with names matching a pattern, such as *.frm. Simply draw a file list box on the form and set its Pattern property to *.frm. You can specify the Pattern property at run time with the following code:

File1.Pattern = "*.FRM"

The file-system controls give you the flexibility that is not available with the common dialog control. You can mix and match them in a variety of ways, and you control their appearance and how they interact.

If your goal is simply to allow users to open and save files, a common dialog control provides a ready-to-run set of dialog boxes for these and other operations. These are the same dialog boxes used by many other Microsoft Windows – based applications, so they provide a standardized look-and-feel. They also recognize network drives when they're available.

For More Information See "Using the Common Dialog Control" earlier in this chapter for more information.


The Drive List Box

The drive list box is a drop-down list box. By default, the current drive is displayed on the user's system. When this control has the focus, the user can type in any valid drive designation or click the arrow at the right of the drive list box. When the user clicks the arrow, the list box drops down to list all valid drives. If the user selects a new drive from the list, that drive appears at the top of the list box.

You can use code to examine the Drive property of the drive list box to determine which drive is currently selected. Your application can also specify which drive appears at the top of the list box with this simple assignment:

Drive1.Drive = "c:\"

The drive list box displays valid available drives. Choosing a drive from the list box doesn't automatically change the current working drive; however, you can use the Drive property to change drives at the operating system level by specifying it as an argument to the ChDrive statement:

ChDrive Drive1.Drive


The Directory List Box

The directory list box displays the directory structure of the current drive on the user's system, beginning with the top-level directory. Initially, the name of the current directory appears highlighted and indented from directories above it in the hierarchy, back to the root. Subdirectories are indented beneath the current directory in the directory list box. As a user moves up or down the list, each of the items is highlighted in turn.

Identifying Individual Directories

Each directory in the box has an integer identifier associated with it that allows you to identify individual directories. This capability is not provided by the common dialog control. The directory specified by the Path property (Dir1.Path) always has the ListIndex value of – 1. The directory immediately above it has the ListIndex value of – 2, the one above that of – 3, and so on up to the root. The first subdirectory of Dir1.Path has the ListIndex 0. If there are multiple directories at the first subdirectory level, the next has the ListIndex value of 1, then 2, and so on, as shown in Figure 7.18.

Figure 7.18 A directory structure displayed in the directory list box

Setting the Current Directory

Use the Path property of the directory list box to set or return the current directory in the box (ListIndex = – 1). For example, if you assign "c:\payroll" to Drive1.Path in Figure 7.18, the \Payroll directory becomes selected as the current working directory.

Similarly, you can assign the Drive property of the drive list box to the Path property of the directory list box:

Dir1.Path = Drive1.Drive

When this assignment is executed, the directory list box displays all the available directories and subdirectories on that drive. By default, the directory list box also displays all directories above, and any subdirectories immediately below, the current directory of a drive assigned to the Dir1.Path property. The directory list box doesn't set the current directory at the operating system level; it merely highlights the directory and gives it the ListIndex value of – 1.

To set the current working directory, use the ChDir statement. For example, the following statement changes the current directory to the one displayed in the directory list box:

ChDir Dir1.Path

In an application that uses file controls, you can set the current directory to the directory where the application's executable (.exe) file is located with the Application object:

ChDrive App.Path   ' Set the drive.
ChDir App.Path      ' Set the directory.

Note The Path property is available only at run time, not at design time.

For More Information See "App Object" for more information on the Application object.

Clicking a Directory Item

When a user clicks an item in a directory list box, that item is highlighted. When an item is double-clicked, it is assigned to the Path property, its ListIndex property gets the value –1, and the directory list box is redrawn to show its immediate subdirectories.

Finding a Directory's Relative Position

The ListCount property returns the number of directories below the currently expanded directory, not the total number of items in the directory list box. Because the ListIndex value of the currently expanded directory is always –1, you can write code to determine how far down from the root the currently expanded directory is in the hierarchy. For example:

' Initialize for currently expanded directory.

GoHigher = 0
' Dir1.List(x) returns empty string if the directory
' doesn't exist.
Do Until Dir1.List(GoHigher) = ""
   GoHigher = GoHigher - 1
Loop
' Convert to positive number, if desired.
LevelsAbove = Abs(GoHigher)


The File List Box

The file list box displays files contained in the directory specified by the Path property at run time. You can display all the files in the current directory on the current drive using the following statement:

File1.Path = Dir1.Path

You can then display a subset of these files by setting the Pattern property — for example, *.frm displays only files with that extension. The Pattern property can also accept a list delimited by semicolons. For example, a line with the following code displays all files with the extensions .frm and .bas:

File1.Pattern = "*.frm; *.bas"

Visual Basic supports the ? wildcard character. For instance, ???.txt displays files that have base names of only three characters with the extension .txt.

Working with File Attributes

The attributes of the currently selected file (Archive, Normal, System, Hidden, and ReadOnly) are also available through file list box properties. You use these properties to specify which kinds of files to display in a file list box. The default value for the System and Hidden attributes is False. The default value for the Normal, Archive, and ReadOnly attributes is True.

To display only read-only files in the list box, for example, simply set the ReadOnly property to True and the other attribute properties to False:

File1.ReadOnly = True
File1.Archive = False
File1.Normal = False
File1.System = False
File1.Hidden = False

When Normal = True, those files without the System or Hidden attribute are displayed. When Normal = False, you can still display files with ReadOnly and/or Archive attributes by setting these attributes to True.

Note You cannot use the attribute properties to set file attributes. To set file attributes, use the SetAttr statement.

By default, you can highlight only a single selection in a file list box. To select multiple files, use the MultiSelect property.

For More Information For more information on SetAttr, see "SetAttr Statement." Also see "MultiSelect Property."


Using File-System Controls Together

If you use a combination of file-system controls, you can synchronize the information they display. For example, if you have a drive list box, a directory list box, and a file list box with the default names Drive1, Dir1, and File1, the sequence of events might work like this:

  1. The user selects a drive in the Drive1 list box.

  2. A Drive1_Change event is generated, and the display in Drive1 is updated to reflect the new drive.

  3. Code in the Drive1_Change event procedure assigns the new selection (the Drive1.Drive property) to the Path property of the Dir1 list box with the following statements:
    Private Sub Drive1_Change ()
   Dir1.Path = Drive1.Drive
End Sub
  4. The assignment to the Path property generates a Dir1_Change event and updates the display in Dir1 to reflect the current directory of the new drive.

  5. Code in the Dir1_Change event procedure assigns the new path (the Dir1.Path property) to the File1.Path property of the File1 list box:
    Private Sub Dir1_Change ()
   File1.Path = Dir1.Path
End Sub
  6. The assignment to the File1.Path property causes the display in the File1 list box to reflect the Dir1 path specification.

The event procedures you use and the properties you change depend on the way your application uses the combination of file-system controls. The code in "File-System Controls Scenario: A File Seeker Application" illustrates the synchronization of controls described here.


File-System Controls Scenario: A File Seeker Application

Because users often want to find a file or group of files available to an application quickly, many applications provide capabilities for investigating the file system. The Winseek.vbp sample application helps the user browse drives and directories, and displays any category of files.

Figure 7.19 File-system controls in the WinSeek application

The following table summarizes the controls in Seek.frm from the WinSeek application.

Control | Property | Setting
Drive list box | Name | drvList
Directory list box | Name | dirList
File list box | Name Pattern | filList *.*
First command button | Name Caption Default | cmdSearch &Search True
Second command button | Name Caption | cmdExit E&xit
List box | Name | lstFoundFiles

Note The file-system controls do not have caption properties, although you can label them and give them access keys. For more information on using labels this way, see "Using the Label Control" later in this chapter.

Writing Code for the WinSeek Application

In the drive list box, a Change event is triggered by a single mouse click on an item. A Change event also occurs when the user selects an item and then changes the focus on the form. In the directory list box, a DblClick event is necessary to generate a Change event.

When users want to change directories without using a mouse, they typically use the arrow keys to select the desired directory and then press the ENTER key.

Because ENTER is commonly associated with the default command button control, WinSeek must recognize when the user simply wants to change directories rather than conduct a search for files.

The WinSeek application resolves this ambiguity by determining if the path of the dirList box differs from the currently highlighted directory. This situation can occur when the user single-clicks an item in the directory list box or navigates the directory list box using the arrow keys. The following code determines whether the dirList.Path is different from the path of the highlighted directory. If the paths are different, the dirList.Path is updated. If the paths are the same, the search is performed.

Private Sub cmdSearch_Click()
   .
   .
   .
' If the dirList.Path is different from the currently
' selected directory, update it; otherwise perform the
' search.
If dirList.Path <> dirList.List(dirList.ListIndex) Then
   dirList.Path = dirList.List(dirList.ListIndex)
   Exit Sub
End If
' Continue with search.
   .
   .
   .
End Sub

The WinSeek application uses the following procedures to handle significant events:

  • The drvList_Change procedure

  • The dirList_Change procedure

  • The cmdSearch_Click procedure

The Drive List Box's Change Event

When the user clicks an item in the drive list box, its Change event is generated. The drvList_Change event procedure is invoked, and the following code is run:

Private Sub drvList_Change ()
   On Error GoTo DriveHandler
   ' If new drive was selected, the Dir1 box
   ' updates its display.
   dirList.Path = drvList.Drive
   Exit Sub
' If there is an error, reset drvList.Drive with the
' drive from dirList.Path.
DriveHandler:
   drvList.Drive = dirList.Path
   Exit Sub
End Sub

Notice that the Change event in a drive list box occurs when a new drive is selected, either with a single mouse click or when the user moves the selection (for example, with an arrow key). The error handler is triggered by actions such as attempting to access a floppy disk drive while the drive door is open or selecting a network drive that has been inadvertently disconnected. Because the error prevents the original assignment, dirList.Path still contains the previous valid drive. Reassigning dirList.Path to drvList.Drive corrects this error.

For More Information See "Debugging Your Code and Handling Errors" for more information.

The Directory List Box's Change Event

If the user double-clicks an item in the directory list box, or if the Path property of dirList is changed in code (as in the drvList_Change procedure), the dirList_Change event is initiated. The following code responds to that event:

Private Sub dirList_Change ()
   ' Update file list box to synchronize with the
   ' directory list box.
   filList.Path = dirList.Path
End Sub

This event procedure assigns the Path property of the dirList box to the Path property of the filList box. This causes a PathChange event in the filList list box, which is redrawn; you don't need to add code to the filList_PathChange procedure, because in this application, the event chain ends in the filList list box.

The Command Button's Click Event

This event procedure determines whether the highlighted item in the dirList list box is the same as the dirList.Path. If the items are different, then dirList.Path is updated. If the items are the same, then the search is performed.

Private Sub cmdSearch_Click ()
   .
   .
   .
   ' If the dirList.Path is different from the
   ' currently selected directory, update it;
   ' otherwise perform the search.
   If dirList.Path <> dirList.List _
   (dirList.ListIndex) Then
      dirList.Path = dirList.List(dirList.ListIndex)
      Exit Sub
   End If
   ' Continue with search.
   .
   .
   .
End Sub

Note You can enhance the WinSeek application with additional features. For example, you might want to use a file control's attribute properties. You could use check boxes to allow the user to set different combinations of file attributes so that the file list box displays files that are Hidden, System, and so on. This would restrict a search to conforming files.


Using the Frame Control

Frame controls are used to provide an identifiable grouping for other controls. For example, you can use frame controls to subdivide a form functionally — to separate groups of option button controls.

Figure 7.21 The frame control

In most cases, you will use the frame control passively — to group other controls — and will have no need to respond to its events. You will, however, most likely change its Name, Caption, or Font properties.

For More Information See "Grouping Options with Option Buttons" in "Forms, Controls, and Menus" for a simple demonstration of using the frame control to group option buttons.

Adding a Frame Control to a Form

When using the frame control to group other controls, first draw the frame control, and then draw the controls inside of it. This enables you to move the frame and the controls it contains together.

Drawing Controls Inside the Frame

To add other controls to the frame, draw them inside the frame. If you draw a control outside the frame, or use the double-click method to add a control to a form, and then try to move it inside the frame control, the control will be on top of the frame and you'll have to move the frame and controls separately.

Figure 7.22 Controls inside a frame

Note If you have existing controls that you want to group in a frame, you can select all the controls, cut them to the clipboard, select the frame control, and then paste them into the frame control.

Selecting Multiple Controls in a Frame

To select multiple controls in a frame , hold down the CTRL key while using the mouse to draw a box around the controls. When you release the mouse, the controls inside the frame will be selected, as in Figure 7.23.

Figure 7.23 Selecting controls inside a frame


Using the Microsoft Hierarchical FlexGrid Control

The Microsoft Hierarchical FlexGrid (MSHFlexGrid) and Microsoft FlexGrid (MSFlexGrid) controls present Recordset data, from one or more tables, in a grid format.

The Hierarchical FlexGrid control provides you with advanced features for displaying data in a grid. It is similar to the Microsoft Data Bound grid (DataGrid) control, but with the distinct difference that the Hierarchical FlexGrid control does not allow the user to edit data bound to, or contained within, it. This control, therefore, allows you to display data to the user while ensuring that the original data remains secure and unchanged. It is also possible, however, to add cell-editing features to your Hierarchical FlexGrid control by combining it with a text box.

While the Hierarchical FlexGrid control is based on the FlexGrid control used in Visual Basic 5.0, the Hierarchical FlexGrid control is the more flexible of the two. The Hierarchical FlexGrid control also provides more display options with which you can define a custom format that best suits your needs.

These topics primarily focus on using the Hierarchical FlexGrid. For information on the former FlexGrid control, see your Visual Basic 5.0 documentation.

Visual Basic's FlexGrid controls

Icon | Abbreviation | Control Name
 | MSHFlexGrid control | Microsoft Hierarchical FlexGrid control
 | MSFlexGrid control | Microsoft FlexGrid control

The Hierarchical FlexGrid control supports the following features:

  • Read-only data binding.

  • Dynamic rearrangement of columns and rows.

  • Automatic regrouping of data during column adjustment.

  • Adaptation to existing Visual Basic code for the DBGrid control.

  • Each cell may contain text, a picture, or both.

  • Changing current cell text in code or at run time.

  • Reading data automatically when the Hierarchical FlexGrid is assigned to a data control.

  • Wordwrap for text within cells.

  • ActiveX® Data Binding when the DataSource and DataMember properties of the control are bound to a specific data provider. *

  • Binding via the Data Binding Manager in Visual Basic. *

  • Binding directly to grouped and related ADO Recordsets from a Command hierarchy. *

  • Additional display options when the Hierarchical FlexGrid is bound to a hierarchy of Recordsets. These additional options enable a diverse display for grouped and related Recordsets, including bands. *

Note To take advantage of the features marked with an asterisk (*) you must use the Hierarchical FlexGrid control. These features are not available in the FlexGrid control.

Due to limitations of the previous FlexGrid control, some features, such as bands, are not accessible using this control. To have access to all features, we recommend that you use the Hierarchical FlexGrid control when creating a new data-bound grid control. The FlexGrid control does not automatically update to the Hierarchical FlexGrid control.

Accessing the Hierarchical FlexGrid Control

Use the following procedure to install and access the Hierarchical FlexGrid control in Visual Basic.

To install and access the Hierarchical FlexGrid control

  1. On the Project menu, select Components. The Components dialog box appears.

  2. On the Controls tab, select Microsoft Hierarchical FlexGrid Control 6.0, and then click OK. The MSHFlexGrid control is added to the Visual Basic toolbox.

  3. On the Visual Basic toolbox, click the MSHFlexGrid control, and then drop it on a Visual Basic form.

    -or-

    On the Visual Basic toolbox, double-click the MSHFlexGrid control to add it to a form.


Binding Data to the Hierarchical FlexGrid

Before you can use its features, you must bind data to the Hierarchical FlexGrid. You can bind data to this control either using Visual Basic's new Data Binding Manager or programmatically.

Once your Hierarchical FlexGrid is bound to a data source, the design-time display within the Hierarchical FlexGrid is a single blank column and single blank row. The field and band information is not automatically retrieved (to obtain this information, see Retrieving Structure). If the Hierarchical FlexGrid is run without field and band information, the data displays using the default property settings. That is, if the Hierarchical FlexGrid is bound to a hierarchical Command, the bands of data display horizontally with each band containing a column for each field in the Recordset.

Hierarchical FlexGrid bound to the Data Source

Using the Visual Basic Data Binding Manager to Bind Data to the Hierarchical FlexGrid

This section describes how to use the Visual Basic Data Binding Manager to bind data to your Hierarchical FlexGrid. This Data Binding Manager provides a user interface that promotes ease-of-use when binding data.

To set the DataSource using the Visual Basic Data Binding Manager

  1. Create a source of data for your Hierarchical FlexGrid.

    The data source can be a DataEnvironment object or an ActiveX® Data Object (ADO) Data Control, a new Visual Basic feature. For this procedure, create the data source as a DataEnvironment object.

  2. On the Visual Basic toolbox, click the MSHFlexGrid control, and then drop it on a Visual Basic form.

    -or-

    On the Visual Basic toolbox, double-click the MSHFlexGrid control to drop it on a Visual Basic form.

  3. On the Visual Basic Properties window, set the DataSource property to the DataEnvironment object that contains the Command object that you want to bind to the Hierarchical FlexGrid.

    Caution If the DataSource is reset, any user-defined modified data in the Hierarchical FlexGrid cells is lost.

  4. On the Visual Basic Properties window, set the DataMember property to a Command object in the Data Environment. If you want to view hierarchical data in your Hierarchical FlexGrid, you must specify a top-most, parent Command object in a Command hierarchy as DataMember.

  5. To view the data in the Hierarchical FlexGrid, on the Run menu, select Start.

    -or-

    Press f5.

Programmatically Binding Data to the Hierarchical FlexGrid

This section describes how to bind data to the Hierarchical FlexGrid programmatically.

To set the DataSource programmatically

  1. On the Visual Basic toolbox, double-click the MSHFlexGrid control to place it on a Visual Basic form.

  2. Right-click your Hierarchical FlexGrid and select View Code from the shortcut menu. The Code Editor window appears.

  3. In the Form_Load event, add code to create an ADO Recordset and assign it to the Hierarchical FlexGrid. This code is provided in the following sub-steps.

    Note To set the data source programmatically, the project must have a reference to Microsoft ActiveX Data Objects: On the Project menu, choose Select References, and then select Microsoft ActiveX Data Objects 2.0 Library.

    • Create an ADO Connection and Recordset by inserting the following code, replacing the comments as appropriate (for example, replace <myDataSource> with the actual name of your data source):
      DIM Cn As New Connection, Rs As New Recordset

' You need to replace <myDataSource> with a valid
' DSN on your system.
Cn.ConnectionString = "DSN=<myDataSource>"

' Use the following code for SHAPE Commands
Cn.Provider = "MSDataShape"
Cn.CursorLocation = adUseNone

' Alternatively, for SQL Commands, use the following code
Cn.CursorLocation = adUseNone

Cn.Open

' You need to specify a valid data source for
' your Recordset for the Connection created above
Rs.Source = "<valid SQL SELECT command>"

' Now associate the Command with the Connection
' and execute them.
Set Rs.ActiveConnection = Cn
Rs.Open
    • Assign the open Recordset in Rs to the Hierarchical FlexGrid by inserting the following code:
      Set MSHFlexGrid1.DataSource = Rs
  4. To view the data in the Hierarchical FlexGrid, on the Run menu, select Start.

    -or-

    Press f5.


Using the Hierarchical FlexGrid with a Hierarchical Recordset

You can use the Hierarchical FlexGrid with a hierarchical Recordset to view relational information. You can display this information to the user while ensuring that the original data remains secure and unchanged, or you can add cell-editing features to your Hierarchical FlexGrid by adding a text box to the form. When the Hierarchical FlexGrid is bound to a hierarchy of Recordsets, you can display grouped and related Recordsets using bands.

Note Data must be bound to your Hierarchical FlexGrid before performing this procedure. To do this, see Binding Data to the Hierarchical FlexGrid.

To use bands when displaying hierarchical Recordsets in a Hierarchical FlexGrid

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the General tab, set the BandDisplay. See Formatting a Band for a description of each band display.

  3. On the Bands tab, select a band from the Band box. This list of available bands is based on the Recordsets in the Command hierarchy. For all bands, the name of the Command that generates the Recordset is shown in parentheses.

  4. Modify the properties of each band as necessary. See Formatting a Band for more information.

  5. Click OK to apply the band properties to your Hierarchical FlexGrid and close the Property Pages dialog box.


Formatting a Band

In your Hierarchical FlexGrid you can use band formatting to specify the display options for a particular Recordset. A band is created for each Recordset in an ADO hierarchical Recordset. For example, when binding a Hierarchical FlexGrid to an ADO hierarchical Recordset that contains Customers and Orders, the Hierarchical FlexGrid initially contains two bands.

You can customize the appearance of your Hierarchical FlexGrid by formatting a band. This enables you to highlight important information among multiple bands. Band elements you can format include column headers, gridlines, colors, and indentation.

When binding to a non-hierarchical Recordset there is only one band, and this band is referred to as Band 0. No other bands are available, because bands are based on the Recordsets in the Command hierarchy.

Changing Band Layout

You can change the way that fields display within a band by changing the band's layout. By default, the fields display horizontally within bands, as in a standard grid.

Horizontal Band Display

Vertical display may cause the band to expand its height to accommodate all fields in the band. Any other bands that display in the Hierarchical FlexGrid also expand, ensuring that all bands are of equal height.

Vertical Band Display

To set band layout

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the General tab, set the BandDisplay.

  3. Click OK to apply the band's display properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Displaying Column Headers

When the band display is horizontal, you can set headers to display in your Hierarchical FlexGrid. The headers display directly above the band, repeating for each band in the Hierarchical FlexGrid. To display only one set of headers for each band, at the top or left of the Hierarchical FlexGrid, use fixed cells instead of headers. See "Customizing Fixed Appearance" (in the topic Customizing Areas of the Hierarchical FlexGrid) for information on fixed cells.

Horizontal Column Headers

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Bands tab, select ColumnHeaders, and then select a column header style from the TextStyleHeader list box.

  3. Click OK to apply the column header properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Changing Column Order

You can change the order of columns within a band of your Hierarchical FlexGrid.

To change column order within a band

  1. Select the column that you want to move.

  2. Use the up and down arrows to move the column to a new position within the band.

Modifying Color and Gridlines

Band color and gridline information can be set globally or individually. You can modify color and gridlines to highlight important information within your Hierarchical FlexGrid and increase readability. In addition, you can specify whether to display gridlines between cells within a band. Use the following procedure to modify color and gridlines for all bands within your Hierarchical FlexGrid.

Note To change color information for an individual band, you must programmatically perform the change using the BackColorBand property.

To globally modify color and gridlines

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Bands tab, select a style from the Gridlines box, and then click Apply. This style determines the type of line drawn between the standard, text-filled, areas of your Hierarchical FlexGrid for the selected band.

  3. On the Style tab, select a style from GridLinesFixed. Next, select a style from GridLinesUnpopulated, and then click Apply. These styles determine the type of lines drawn between fixed and unpopulated areas of your Hierarchical FlexGrid.

  4. On the Color tab, assign each gridline property a color. To do this, first select a Color Set. Next, select the property to change (for example, GridColor), select a color from the Color Palette, and then click Apply. Repeat this for each of the Hierarchical FlexGrid gridlines that you wish to change.

    Note When using Windows Default colors, the Hierarchical FlexGrid appears in the colors specified in the your Display Control Panel. In addition to changing the Hierarchical FlexGrid property's colors to a standard or Windows default color, you can create your own color definitions by clicking Edit Custom Color and using the resulting Color dialog box.

  5. Click OK to apply the gridline and color properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Indenting a Band

When displaying bands vertically, you can indent a band a specific number of columns. This enables you to clearly present bands of information to the user. The indented column before each band contains empty, non-working cells. Therefore, the user cannot place focus into these areas. The format characteristics of these cells are defined by the indent formatting properties, such as GridLinesIndent.

See the Vertical Band Display figure in Changing Band Layout, above.

To indent a band

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Bands tab, select BandIndent and specify the number of columns by which to indent the band.

  3. Click OK to apply the band properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Using the Band Functionality with a Non-Hierarchical Recordset

You can format the band of a non-hierarchical Recordset using any of the properties in the Hierarchical FlexGrid's Property Pages dialog box.

A non-hierarchical Recordset contains only one band, Band 0. There are no other bands available because bands are based on the Recordsets in the Command hierarchy.


Using Band Expand and Collapse Functionality

The expand and collapse functionality makes it easier to view the Recordset organization and to scroll through the Hierarchical FlexGrid. This functionality makes it possible for users to either view large amounts of data or condense the information. When a band is expandable, a default expand (+) or collapse (-) bitmap appears in the upper-left corner of the band. The expand and collapse functionality is available for both vertical and horizontal bands.

When bands are expanded the collapse (-) bitmap appears. In their expanded state, bands show their maximum amount of data. When collapsed, the expand (+) bitmap appears and bands shows a minimal amount of data.

When collapsed, bands may show unpopulated areas. These areas are formatted according to the unpopulated format specifications.

Collapsed Band

When a band is collapsed, so none of its records display, the columns from that band are not shown. As the user expands the band the columns display. As bands are expanded they maintain a uniform height. If necessary, smaller bands will enlarge to retain a height consistent with larger bands.

Expanded Band

Expanding and Collapsing Bands

This section describes how to add expand and collapse functionality to a band in your Hierarchical FlexGrid. It also describes how to use the expand and collapse functionality.

To add expand and collapse functionality to a band

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Bands tab, select BandExpandable. Next, select the band to expand from the Band list.

    Note For a band to be expandable, it must have at least one sub-band. Therefore, the BandExpandable check box is disabled if only Band 0 exists.

  3. Click OK to apply the band properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

To use the expand and collapse functionality

  1. Once band functionality has been added to your Hierarchical FlexGrid through its Property Pages dialog box, on the Run menu, select Start.

    -or-

    Press f5.

  2. Click the expand (+) graphic in the upper-left corner of the band to view maximum (expanded) Recordset information.

  3. Click the collapse (-) graphic in the upper-left corner of the band to view minimal (collapsed) Recordset information.

    Note If you expand a band that contains a collapsed sub-band, the sub-band remains collapsed until you expand it.


Retrieving Structure

Your Hierarchical FlexGrid's structure information includes detailed band and column setup information. By default, the column order within each band is in the same order as the underlying ADO Recordset. Once retrieved, you can use the structure information to control how data appears in your Hierarchical FlexGrid.

To retrieve band and field information

  • Right-click your Hierarchical FlexGrid, and then select Retrieve Structure from the shortcut menu.

    -or-

    Access the Hierarchical FlexGrid's Property Pages dialog box and select the Bands tab. This tab contains the structural information of your Hierarchical FlexGrid.

Following retrieval of the structure of the data, the information is stored with the Hierarchical FlexGrid. Once the structure information is retrieved, the design-time Hierarchical FlexGrid displays updated information for each band and field.

Note If the structure of the DataSource changes, you must re-retrieve the structure before the changes will appear in your Hierarchical FlexGrid.

Clearing Band and Column Information

When you clear band and column information, they are restored to their default settings.

To clear band and field information

  • Right-click your Hierarchical FlexGrid, and select Clear Structure from the shortcut menu.

Note If you have made any changes to the band or column properties of your Hierarchical FlexGrid, a warning message appears. At this point, you can click OK to reset the Hierarchical FlexGrid to its default state and discard all user-defined settings.


Binding Your Hierarchical FlexGrid to a Data Environment

Before you can use bands in your Hierarchical FlexGrid, you bind the FlexGrid to a Data Environment, which contains a top-level, parent Command object.

To bind your Hierarchical FlexGrid control to a Data Environment

  1. Drag a MSHFlexGrid control onto a Visual Basic form.

  2. From the Visual Basic Properties window, set the DataSource property to the DataEnvironment object that contains the Command object that you want to bind to the Hierarchical FlexGrid control. For example, DataEnvironment1.

  3. From the Visual Basic Properties window, set the DataMember property to a top-level, parent Command object in your Data Environment. Your data is now bound to a Hierarchical FlexGrid control.

  4. To view the data in the Hierarchical FlexGrid, select Start from the Run menu or press f5. The structure is retrieved and appears in the Hierarchical FlexGrid.


Sorting and Merging Data in the Hierarchical FlexGrid

In this scenario, you can sort and merge data in the Hierarchical FlexGrid. In most cases, you access data by downloading from a database into your Hierarchical FlexGrid. However, here you enter sample data using the Code Editor window to populate the columns and rows of your Hierarchical FlexGrid.

To create this data display

  1. Set the properties of the Hierarchical FlexGrid.

  2. Create the data.

  3. Define the routines to calculate an index and do a sort.

  4. Define the routine to enter the data (from step 2) into the Hierarchical FlexGrid.

  5. Allow the control to switch data organization views.

To complete the scenario, follow the procedures in this section in the order shown.

Setting the Properties of the Control

Use the settings in the following table for the number of columns and rows, the font information, and to create the column headings of your Hierarchical FlexGrid.

MSHFlexGrid control

Property | Setting
Name | Fg1
Cols | 4
Rows | 20
MergeCells | 2 – Restrict Rows
FormatString | <Region |<Product |<Employees |>Sales
FontName | Arial

Sorting and Merging the Data

Use the following procedures to complete the scenario of sorting and merging data in your Hierarchical FlexGrid.

To sort and merge the data

  1. Create an array to store the sample data. To do this, insert the following routine in the Form_Load event of your Code Editor window:
    Sub Form_Load ()
   Dim I As Integer
   ' Create array.
   For i = Fg1.FixedRows To Fg1.Rows - 1
      ' Region.
      Fg1.TextArray(fgi(i, 0)) = RandomString(0)
      ' Product.
      Fg1.TextArray(fgi(i, 1)) = RandomString(1)
      ' Employee.
      Fg1.TextArray(fgi(i, 2)) = RandomString(2)
      Fg1.TextArray(fgi(i, 3)) = _
      Format(Rnd * 10000, "#.00")
   Next

   ' Set up merging.
   Fg1.MergeCol(0) = True
   Fg1.MergeCol(1) = True
   Fg1.MergeCol(2) = True

   ' Sort to see the effects.
   DoSort

   ' Format Grid
   Fg1.ColWidth(0) = 1000
   Fg1.ColWidth(1) = 1000
   Fg1.ColWidth(2) = 1000
   Fg1.ColWidth(3) = 1000

End Sub
  2. Calculate an index and complete a sort. To do this, define a routine to calculate an index and do a sort. The index is used with the TextArray property to sort the data. Insert the following routine to calculate an index:
    Function Fgi (r As Integer, c As Integer) As Integer
   Fgi = c + Fg1.Cols * r
End Function

Sub DoSort ()
   Fg1.Col = 0
   Fg1.ColSel = Fg1.Cols - 1
   Fg1.Sort = 1   ' Generic ascending.
End Sub
  3. Enter data into your Hierarchical FlexGrid. To do this, define a routine that populates the Hierarchical FlexGrid with the sample data:
    Function RandomString (kind As Integer)
   Dim s As String
   Select Case kind

      Case 0   ' Region.
         Select Case (Rnd * 1000) Mod 5
            Case 0: s = "1. Northwest"
            Case 1: s = "2. Southwest"
            Case 2: s = "3. Midwest"
            Case 3: s = "4. East"
            Case Else: s = "5. Overseas"
         End Select

      Case 1   ' Product.
         Select Case (Rnd * 1000) Mod 5
            Case 0: s = "1. Chai"
            Case 1: s = "2. Peppermint"
            Case 2: s = "3. Chamomile"
            Case Else: s = "4. Oolong"
         End Select

      Case 2   ' Employee.
         Select Case (Rnd * 1000) Mod 4
            Case 0: s = "Clare"
            Case 1: s = "Tiffany"
            Case 2: s = "Sally"
            Case Else: s = "Lori"
         End Select
   End Select
   RandomString = s
End Function

    If you run the project at this point, it should look something like this:

    Next, you need to allow the user to reorganize the data. That is, you must allow the Hierarchical FlexGrid to switch data organization views.

  4. Reorganize the data by adding the following routine, which will drag columns to new positions. This routine uses the Tag property to save the column number when the user presses the mouse button, triggering the MouseDown event.
    Sub Fg1_MouseDown (Button As Integer, _
Shift As Integer, X As Single, Y As Single)
   Fg1.Tag = ""
   If Fg1.MouseRow <> 0 Then Exit Sub
   Fg1.Tag = Str(Fg1.MouseCol)
   MousePointer = vbSizeWE
End Sub
  5. Add the following routine to readjust the columns and sort the data when the user releases the mouse button, triggering the MouseUp event.
    Sub Fg1_MouseUp (Button As Integer, Shift As _
Integer, X As Single, Y As Single)
   MousePointer = vbDefault
   If Fg1.Tag = "" Then Exit Sub
   Fg1.Redraw = False
   Fg1.ColPosition(Val(Fg1.Tag)) = Fg1.MouseCol
   DoSort
   Fg1.Redraw = True
End Sub

    Once the procedures in this scenario are complete, the data automatically reorganizes whenever you drag a column to a new position at run time. For example, if you drag the Employee column to the left, it would appear as follows:


Editing Cells in a Hierarchical FlexGrid Spreadsheet

In this scenario, you can edit cells in a Hierarchical FlexGrid spreadsheet. This scenario illustrates some of the capabilities of the Hierarchical FlexGrid's control events and containers, and shows how this control can be used to create a spreadsheet with in-cell editing using standard Visual Basic controls.

Note The sample application (Flex.vbp) demonstrates the functionality necessary for navigating around and selecting ranges of cells.

To create this data display

  1. Create Hierarchical FlexGrid and TextBox controls.

  2. Set the properties of these controls.

  3. Add row and column headings to the Hierarchical FlexGrid.

  4. Add in-cell editing to the Hierarchical FlexGrid.

  5. Add functionality to the TextBox control (the "edit" box) for updating the data.

  6. Copy the data from the text box to the Hierarchical FlexGrid.

To complete the scenario, follow the procedures in this section in the order shown.

Creating the Controls

Add a Hierarchical FlexGrid to your project, and then add a TextBox control to it to create a parent-child relationship, as shown:

Setting the Properties of the Controls

Set the properties of the Hierarchical FlexGrid and TextBox controls as follows:

MSHFlexGrid control

Property | Setting
Name | Fg2
Cols | 6
Rows | 20
FillStyle | 1 – Repeat
FocusRect | 2 – Heavy
FontName | Arial
FontSize | 9

TextBox control

Property | Setting
Name | TxtEdit
FontName | Arial
FontSize | 9
BorderSize | 0 – None
Visible | False

Editing Cells in a Spreadsheet

Use the following procedures to edit cells in your Hierarchical FlexGrid.

To edit cells in a spreadsheet

  1. Modify your Hierarchical FlexGrid, so it resembles a spreadsheet, by adding the following code to the Form_Load procedure in the Code Editor window:
    Sub Form_Load ()
   Dim i As Integer

   ' Make first column narrow.
   Fg2.ColWidth(0) = Fg2.ColWidth(0) / 2
   Fg2.ColAlignment(0) = 1   ' Center center.

   ' Label rows and columns.
   For i = Fg2.FixedRows To Fg2.Rows - 1
      Fg2.TextArray(fgi(i, 0)) = i
   Next
   For i = Fg2.FixedCols To Fg2.Cols - 1
      Fg2.TextArray(fgi(0, i)) = i
   Next

   ' Initialize edit box (so it loads now).
   txtEdit = ""
End Sub
  2. Create a function to calculate an index for the TextArray property as follows:
    Function Fgi (r As Integer, c As Integer) As Integer
   Fgi = c + Fg2.Cols * r
End Function
  3. Add the following code to the Hierarchical FlexGrid's KeyPress and DblClick events:
    Sub Fg2_KeyPress (KeyAscii As Integer)
   MSHFlexGridEdit Fg2, txtEdit, KeyAscii
End Sub

Sub Fg2_DblClick ()
   MSHFlexGridEdit Fg2, txtEdit, 32 ' Simulate a space.
End Sub
  4. Add the following routine to initialize the text box and pass the focus from the Hierarchical FlexGrid to the TextBox control:
    Sub MSHFlexGridEdit (MSHFlexGrid As Control, _
Edt As Control, KeyAscii As Integer)

   ' Use the character that was typed.
   Select Case keyascii

   ' A space means edit the current text.
   Case 0 To 32
      Edt = MSHFlexGrid
      Edt.SelStart = 1000

   ' Anything else means replace the current text.
   Case Else
      Edt = Chr(keyascii)
      Edt.SelStart = 1
   End Select

   ' Show Edt at the right place.
   Edt.Move MSHFlexGrid.Left + MSHFlexGrid.CellLeft, _
      MSHFlexGrid.Top + MSHFlexGrid.CellTop, _
      MSHFlexGrid.CellWidth - 8, _
      MSHFlexGrid.CellHeight - 8
   Edt.Visible = True

   ' And make it work.
   Edt.SetFocus
End Sub
  5. Add updating data functionality to the text box by adding the following routines to its KeyPress and DblClick events:
    Sub txtEdit_KeyPress (KeyAscii As Integer)
   ' Delete returns to get rid of beep.
   If KeyAscii = Asc(vbCr) Then KeyAscii = 0
End Sub

Sub txtEdit_KeyDown (KeyCode As Integer, _
Shift As Integer)
   EditKeyCode Fg2, txtEdit, KeyCode, Shift
End Sub

Sub EditKeyCode (MSHFlexGrid As Control, Edt As _
Control, KeyCode As Integer, Shift As Integer)

   ' Standard edit control processing.
   Select Case KeyCode

   Case 27   ' ESC: hide, return focus to MSHFlexGrid.
      Edt.Visible = False
      MSHFlexGrid.SetFocus

   Case 13   ' ENTER return focus to MSHFlexGrid.
      MSHFlexGrid.SetFocus

   Case 38      ' Up.
      MSHFlexGrid.SetFocus
      DoEvents
      If MSHFlexGrid.Row > MSHFlexGrid.FixedRows Then
         MSHFlexGrid.Row = MSHFlexGrid.Row - 1
      End If

   Case 40      ' Down.
      MSHFlexGrid.SetFocus
      DoEvents
      If MSHFlexGrid.Row < MSHFlexGrid.Rows - 1 Then
         MSHFlexGrid.Row = MSHFlexGrid.Row + 1
      End If
   End Select
End Sub

    Next, you need to instruct the Hierarchical FlexGrid what to do with the data when it is entered into the text box. The focus returns to the control after the user enters the data and either presses enter or clicks a different cell in the Hierarchical FlexGrid. Therefore, you must copy the data from the text box into the active cell; to do this, continue with the following steps.

  6. Copy the data from the text box to the Hierarchical FlexGrid by adding the following code to the GotFocus and LeaveCell event procedures:
    Sub Fg2_GotFocus ()
   If txtEdit.Visible = False Then Exit Sub
   Fg2 = txtEdit
   txtEdit.Visible = False
End Sub

Sub Fg2_LeaveCell ()
   If txtEdit.Visible = False Then Exit Sub
   Fg2 = txtEdit
   txtEdit.Visible = False
End Sub

    Once the procedures in this scenario are complete, data can be entered into individual cells at run time, as shown in the following figure.


Displaying an Outline with Headings using the Hierarchical FlexGrid

In this scenario you can use the Hierarchical FlexGrid to create an outline-style display with heading items that you can collapse or expand.

To create this data display

  1. Set the properties of the Hierarchical FlexGrid.

  2. Create the data.

  3. Add collapse and expand functionality.

To complete the scenario, follow the procedures in this section in the order shown.

Setting the Properties of the Control

Set four column headings using the FormatString property. Set the first column as narrow and empty (like a spreadsheet) and the other three to contain data (include the spacing between each heading). In addition, set the following:

MSHFlexGrid control

Property | Setting
Name | Fg3
Cols | 4
Rows | 2
Selection Mode | 1 – By Row
FillStyle | 1 – Repeat
FocusRect | 0 – None
GridLines | 0 – None
GridLinesFixed | 2 – Inset
FormatString | ^ |Description |>Date |>Amount
FontName | Arial

Creating an Outline Display with Headings

Use the following procedure to complete the scenario of creating an outline display with headings using the Hierarchical FlexGrid.

To create an outline display with headings

  1. Create the sample data, and set the headings, column, and row properties. To do this, insert the following code into the Form_Load event of the Code Editor window. This creates sample data, sets up and calculates the headings at the top of the control, and sets the Col and Row properties to select the first cell at run time.
    Sub Form_Load ()
   Dim i As Integer, tot As Integer
   Dim t As String, s As String

   ' Create sample data.
   t = Chr(9)
   Fg3.Rows = 1

   Fg3.AddItem "*" + t + "Airfare"
   s = "" +t+ "SFO-JFK" +t+ "9-Apr-95" +t+ "750.00"
   For i = 0 to 5
      Fg3.AddItem s
   Next

   Fg3.AddItem "*" + t + "Meals"
   s = "" +t+ "Flint's BBQ" +t+ "25-Apr-95" _
   +t+ "35.00"
   For i = 0 to 5
      Fg3.AddItem s
   Next

   Fg3.AddItem "*" +t+ "Hotel"
   s = "" +t+ "Center Plaza" +t+ "25-Apr-95" _
   +t+ "817.00"
   For i = 0 to 5
      Fg3.AddItem s
   Next

   ' Add up totals and format heading entries.
   For i = Fg3.Rows - 1 To 0 Step -1
      If Fg3.TextArray(i * Fg3.Cols) = "" Then
         tot = tot + Val(Fg3.TextArray _
         (i * Fg3.Cols + 3))
      Else
         Fg3.Row = i
         Fg3.Col = 0
         Fg3.ColSel = Fg3.Cols - 1
         Fg3.CellBackColor = &HC0C0C0
         Fg3.CellFontBold = True
         Fg3.CellFontWidth = 8
         Fg3.TextArray(i * Fg3.Cols + 3) = _
         Format(tot, "0")
         tot = 0
      End If
   Next
   Fg3.ColSel = Fg3.Cols - 1

   ' Format Grid
   Fg3.ColWidth(0) = 300
   Fg3.ColWidth(1) = 1500
   Fg3.ColWidth(2) = 1000
   Fg3.ColWidth(3) = 1000

End Sub

    At run time, the rows are sorted into three divisions, grouped by their respective headings: Air Fare, Meals, and Hotels, as shown in the following figure:

  2. Add collapse and expand functionality to the row headings by inserting the following code in the DblClick event procedure of the Hierarchical FlexGrid:
    Sub Fg3_DblClick ()
   Dim i As Integer, r As Integer

   ' Ignore top row.
   r = Fg3.MouseRow
   If r < 1 Then Exit Sub

   ' Find field to collapse or expand.
   While r > 0 And Fg3.TextArray(r * Fg3.Cols) = ""
      r = r - 1
   Wend

   ' Show collapsed/expanded symbol on first column.
   If Fg3.TextArray(r * Fg3.Cols) = "*" Then
      Fg3.TextArray(r * Fg3.Cols) = "+"
   Else
      Fg3.TextArray(r * Fg3.Cols) = "*"
   End If

   ' Expand items under current heading.
   r = r + 1
   If Fg3.RowHeight(r) = 0 Then
      Do While Fg3.TextArray(r * Fg3.Cols) = ""
         Fg3.RowHeight(r) = -1 ' Default row height.
         r = r + 1
         If r >= Fg3.Rows Then Exit Do
      Loop

   ' Collapse items under current heading.
   Else
      Do While Fg3.TextArray(r * Fg3.Cols) = ""
         Fg3.RowHeight(r) = 0   ' Hide row.
         r = r + 1
         If r >= Fg3.Rows Then Exit Do
      Loop
   End If
End Sub

    Once all the steps in this scenario are complete, you can expand and collapse the row headings by double clicking on the "+" or "*" symbols in the first column at run time, as shown in the following figure:

    Note You can modify this example to show images instead of the "+" and "*" characters, or to add additional levels to the outline.


Using the Horizontal and Vertical Scroll Bar Controls

Scroll bars provide easy navigation through a long list of items or a large amount of information by scrolling either horizontally or vertically within an application or control. Scroll bars are a common element of the Windows 95 and Windows NT interface.

Figure 7.24 The horizontal and vertical scroll bar controls

The horizontal and vertical scroll bar controls are not the same as the built-in scroll bars found in Windows or those that are attached to text boxes, list boxes, combo boxes, or MDI forms within Visual Basic. Those scroll bars appear automatically whenever the given application or control contains more information than can be displayed in the current window size (or, in the case of text boxes and MDI forms, when the ScrollBars property is also set to True).

In previous versions of Visual Basic, scroll bars were most often used as input devices. Windows interface guidelines now suggest, however, that slider controls be used as input devices instead of scroll bar controls. A Windows 95 slider control is included in the Professional and Enterprise versions of Visual Basic.

Scroll bar controls are still of value in Visual Basic because they provide scrolling to applications or controls that do not provide them automatically. See "Scroll Bar Controls Scenario: Creating a Scrollable Graphics Viewport" for information on using scroll bars in this manner.

How the Scroll Bar Controls Work

The scroll bar controls use the Scroll and Change events to monitor the movement of the scroll box (sometimes referred to as the thumb) along the scroll bar.

Event | Description
Change | Occurs after the scroll box is moved.
Scroll | Occurs as the scroll box is moved. Does not occur if the scroll arrows or scroll bar is clicked.

Using the Scroll event provides access to the scroll bar value as it is being dragged. The Change event occurs after the scroll box is released or when the scroll bar or scroll arrows are clicked.

The Value Property

The Value property (which, by default, is 0) is an integer value corresponding to the position of the scroll box in the scroll bar. When the scroll box position is at the minimum value, it moves to the leftmost position (for horizontal scroll bars) or the top position (for vertical scroll bars). When the scroll box is at the maximum value, the scroll box moves to the rightmost or bottom position. Similarly, a value halfway between the bottom and top of the range places the scroll box in the middle of the scroll bar.

In addition to using mouse clicks to change the scroll bar value, a user can also drag the scroll box to any point along the bar. The resulting value depends on the position of the scroll box, but it is always within the range of the Min to Max properties set by the user.

Note Min can be larger than Max if you want your scroll bar to display information changing from a larger to a smaller value.

The LargeChange and SmallChange Properties

To specify the amount of change to report in a scroll bar, use the LargeChange property for clicking in the scroll bar, and the SmallChange property for clicking the arrows at the ends of the scroll bar. The scroll bar's Value property increases or decreases by the values set for the LargeChange and SmallChange properties. You can position the scroll box at run time by setting Value between 0 and 32,767, inclusive.


Scroll Bar Controls Scenario: Creating a Scrollable Graphics Viewport

The horizontal and vertical scroll bar controls can be used, in addition to the picture box control, to create a scrollable graphics viewport application. The picture box control alone will not allow you to scroll a graphic if it exceeds its boundaries — the picture box control does not automatically add scroll bars.

This application uses two picture boxes. The first is referred to as the stationary parent picture box control. The second, which is contained within the parent, is referred to as the child picture box control. The child picture box contains the graphic image and is moved around within the parent picture box control when the scroll bar controls are used.

Figure 7.25 Adding scroll bar controls at design time

Start by creating a new project and then draw two picture boxes, a horizontal scroll bar, and a vertical scroll bar on the form, as shown in Figure 7.25.

The form's Form_Load event is used to set the scale mode, to size the child picture box within the parent picture box, to position and size the horizontal and vertical scroll bars, and then to load a bitmap graphic. Add the following code to the form's Form_Load event procedure:

Private Sub Form_Load()

   ' Set ScaleMode to pixels.
   Form1.ScaleMode = vbPixels
   Picture1.ScaleMode = vbPixels

   ' Autosize is set to True so that the boundaries of
   ' Picture2 are expanded to the size of the actual
   ' bitmap.
   Picture2.AutoSize = True

   ' Set the BorderStyle of each picture box to None.
   Picture1.BorderStyle = 0
   Picture2.BorderStyle = 0

   ' Load the bitmap.
   Picture2.Picture = _
   LoadPicture("c:\Windows\Winlogo.bmp")

   ' Initialize location of both pictures.
   Picture1.Move 0, 0, ScaleWidth - VScroll1.Width, _
   ScaleHeight - HScroll1.Height
   Picture2.Move 0, 0

   ' Position the horizontal scroll bar.
   HScroll1.Top = Picture1.Height
   HScroll1.Left = 0
   HScroll1.Width = Picture1.Width

   ' Position the vertical scroll bar.
   VScroll1.Top = 0
   VScroll1.Left = Picture1.Width
   VScroll1.Height = Picture1.Height

   ' Set the Max property for the scroll bars.
   HScroll1.Max = Picture2.Width - Picture1.Width
   VScroll1.Max = Picture2.Height - Picture1.Height

   ' Determine if the child picture will fill up the
   ' screen.
   ' If so, there is no need to use scroll bars.
   VScroll1.Visible = (Picture1.Height < _
   Picture2.Height)
   HScroll1.Visible = (Picture1.Width < _
   Picture2.Width)

End Sub

The horizontal and vertical scroll bars' Change event is used to move the child picture box up and down or left and right within the parent picture box. Add the following code to the Change event of both scroll bar controls:

Private Sub HScroll1_Change()
   Picture2.Left = -HScroll1.Value
End Sub

Private Sub VScroll1_Change()
   Picture2.Top = -VScroll1.Value
End Sub

The Left and Top properties of the child picture box are set to the negative value of the horizontal and vertical scroll bars so that as you scroll up or down or right or left, the display moves appropriately.

At run time, the graphic will be displayed as shown in Figure 7.26.

Figure 7.26 Scrolling the bitmap at run time

Resizing the Form at Run Time

In the example described above, the viewable size of the graphic is limited by the original size of the form. To resize the graphic viewport application when the user adjusts the size of the form at run time, add the following code to the form's Form_Resize event procedure:

Private Sub Form_Resize()
   ' When the form is resized, change the Picture1
   ' dimensions.
   Picture1.Height = Form1.Height
   Picture1.Width = Form1.Width

   ' Reinitialize the picture and scroll bar
   ' positions.
   Picture1.Move 0, 0, ScaleWidth - VScroll1.Width, _
   ScaleHeight - HScroll1.Height
   Picture2.Move 0, 0
   HScroll1.Top = Picture1.Height
   HScroll1.Left = 0
   HScroll1.Width = Picture1.Width
   VScroll1.Top = 0
   VScroll1.Left = Picture1.Width
   VScroll1.Height = Picture1.Height
   HScroll1.Max = Picture2.Width - Picture1.Width
   VScroll1.Max = Picture2.Height - Picture1.Width

   ' Check to see if scroll bars are needed.
   VScroll1.Visible = (Picture1.Height < _
   Picture2.Height)
   HScroll1.Visible = (Picture1.Width < _
   Picture2.Width)

End Sub


Using the Image Control

The image control is used to display graphics. Image controls can display graphics in the following formats: bitmap, icon, metafile, enhanced metafile, or as JPEG or GIF files.

Figure 7.27 The image control

In addition, image controls respond to the Click event and can be used as a substitute for command buttons, as items in a toolbar, or to create simple animations.

For More Information See "Lightweight Graphical Controls" in "Forms, Controls, and Menus" for a simple demonstration of using the image control like a command button. See "Creating a Toolbar" in "Creating a User Interface" for information on using image controls to create a toolbar. See "Creating Simple Animation" in "Working with Text and Graphics" for more information on using the image control to create simple animations.

When to Use an Image Control Instead of a Picture Box Control

The image control uses fewer system resources and repaints faster than a picture box control, but it supports only a subset of the picture box control's properties, events, and methods. Both controls support the same picture formats. However, you can stretch pictures in an image control to fit the control's size. You cannot do this with the picture box control.

For More Information See "Cutting Back on Graphics" in "Designing for Performance and Compatibility" for information on using image controls to conserve system resources.

Supported Graphic Formats

The image control can display picture files in any of the following standard formats.

Picture format | Description
Bitmap | A bitmap defines an image as a pattern of dots (pixels). A bitmap has the file name extensions .bmp or .dib. Bitmaps are also called "paint-type" graphics. You can use bitmaps of various color depths, including 2, 4, 8, 16, 24, and 32-bits, but a bitmap only displays correctly if the display device supports the color depth used by the bitmap. For example, an 8-bit-per-pixel (256 color) bitmap only displays in 16 colors when shown on a 4-bit-per-pixel (16 color) device.
Icon | An icon is a special kind of bitmap. Icons have a maximum size of 32 pixels by 32 pixels, but under Microsoft Windows 95, icons are also found in 16 by 16 pixel size. An icon has the file name extension .ico.
Cursor | Cursors, like icons, are essentially bitmaps. Cursors, however, also contain a hot spot, a pixel that tracks the location of the cursor by its x and y coordinates. Cursors have the file name extension .cur.
Metafile | A metafile defines an image as coded lines and shapes. Conventional metafiles have the file name extension .wmf. Enhanced metafiles have the file name extension .emf. Only files that are compatible with Microsoft Windows can be loaded. Metafiles are also called "draw-type" graphics.
JPEG | JPEG (Joint Photographic Experts Group) is a compressed bitmap format which supports 8- and 24-bit color. It is a popular file format on the Internet.
GIF | GIF (Graphic Interchange Format) is a compressed bitmap format originally developed by CompuServe. It supports up to 256 colors and is a popular file format on the Internet.

Loading a Graphic Into the Image Control

Pictures can be loaded into the image control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.

Set Image1.Picture = LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)

When a picture is loaded into the image control, the control automatically resizes to fit the picture — regardless of how small or large the image control was drawn on the form.

You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.

To clear the graphic from the image control, use the LoadPicture function without specifying a file name. For example:

Set Image1.Picture = LoadPicture

This will clear the image control even if a graphic was loaded into the Picture property at design time.

Using the Clipboard

You can also add a graphic to an image control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the Clipboard, select the image control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu.

The Stretch Property

The Stretch property determines whether the picture is stretched when the image control is resized at design time. If set to True, the picture loaded into the image control via the Picture property is stretched. Stretching a picture (especially a bitmap format) can produce a loss in image quality, as shown in Figure 7.28. Metafiles, which are "draw-type" graphics, are better suited for stretching.

Figure 7.28 Stretching a bitmap image


Using the Label Control

Label controls are used to display text and cannot be edited by the user. They are used to identify objects on a form — provide a description of what a certain control will do if clicked, for example — or at run time, they can display information in response to an event or process in your application.

Figure 7.29 The label control

Labels are used in many instances, for many different purposes. Most commonly, they are used to label controls that don't have their own Caption properties. For example, you can use the label control to add descriptive labels to text boxes, list boxes, combo boxes and so on. They can also be used to add descriptive text to a form, for example, to provide the user with Help information.

You can also write code that changes the text displayed by a label control in response to events at run time. For example, if your application takes a few minutes to process a change, you can display a processing-status message in a label.

Because the label control cannot receive the focus, it can also be used to create access keys for other controls.

Setting the Label's Caption

To change the text displayed in the label control, use the Caption property. At design time, you can set this property by selecting it from the control's Properties window.

You can set the length of the Caption property up to a maximum of 1024 bytes.

Aligning Text

The Alignment property allows you to set the alignment of the text within the label control to either Left Justify (0, the default) , Center (1), or Right Justify (2).

The AutoSize and WordWrap Properties

By default, when text entered into the Caption property exceeds the width of the control, the text wraps to the next line and is clipped if it exceeds the control's height.

To allow the control to automatically adjust to the size of its contents, set the AutoSize property to True. The control will expand horizontally to fit the entire contents of the Caption property. To allow the contents to wrap down and expand vertically, set the WordWrap property to True.

For More Information See "Using Labels to Display Text" in "Forms, Controls, and Menus" for a simple demonstration of the AutoSize and WordWrap properties.

Using Labels to Create Access Keys

Set the UseMnemonic property to True if you want to define a character in the Caption property of the label as an access key. When you define an access key in a label control, the user can press and hold down ALT+ the character you designate to move the focus to the next control in the tab order.

You can also create access keys for any other controls that have a Caption property by adding an ampersand (&) before the letter you want to use as the access key. To assign an access key to controls that don't have captions, use a label with the control. Because labels can't receive focus, focus automatically moves to the next control in the tab order. Use this technique to assign access keys to text boxes, picture boxes, combo boxes, list boxes, drive list boxes, directory list boxes, grids, and images.

To assign an access key to a control with a label

  1. Draw the label first, and then draw the control.

    -or-

    Draw the controls in any order and set the TabIndex property of the label to one less than the control.

  2. Use an ampersand in the label's Caption property to assign the access key for the label.

    Note You may want to display ampersands in a label control, rather than using them to create access keys. This may occur if you bind a label control to a field in a recordset where the data includes ampersands. To display ampersands in a label control, set the UseMnemonic property to False.


Using the Line Control

The line control is used to create simple line segments on a form, a frame, or in a picture box.

Figure 7.30 The line control

You can control the position, length, color, and style of line controls to customize the look of applications. Figure 7.31 shows a line control used to graphically separate the label containing the text "Company Name" from the rest of the form.

Figure 7.31 A line control on a form

The line control has limited functionality and is intended for simple uses — display and printing. Line segments cannot be joined to form other shapes, for instance. For more advanced uses you need to use the line method.

For More Information See "Using Graphics Methods" in "Working with Text and Graphics" for more information on drawing lines, rectangles, and filled-in boxes at run time using the line method or for more information on drawing circles, ellipses, and arcs at run time using the Circle method.

Setting Border Style and Color

You set the color and style of a line segment by using the BorderStyle and BorderColor properties.

The BorderStyle property provides you with six line styles:

  • Transparent

  • Solid

  • Dash

  • Dot

  • Dash-Dot

  • Dash-Dot-Dot

  • Inside Solid

You can specify a line style at design time by choosing the BorderStyle property from the Properties window of the line control or, at run time, by specifying the style using its equivalent Visual Basic constant in code.

The BackColor property is used to specify the color of the line.

At design time, you can set the line color by choosing the BorderColor property from the Properties window of the line control and then selecting from the available palette or system colors.

To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify border colors.

Note When BorderStyle is set to 0 (Transparent), the BorderColor property is ignored.

For More Information See "Color Constants" for a list of Visual Basic color constants. See "RGB Function" for information on specifying RGB colors. Also, refer to "Working with Text and Graphics" for detailed information on creating graphics in Visual Basic.

Moving and Sizing a Line Segment

You can move or resize the line control at run time by altering its X1, X2, Y1, and Y2 properties. The X1 and Y1 properties set the horizontal and vertical positions of the left end of the line segment. The X2 and Y2 properties set the horizontal and vertical positions of the right end of the line segment. You can't move a line segment using the Move method.

Drawing Lines on a Form

You can use the line control to draw simple lines on forms.

To draw a line on a form

  1. In the toolbox, select the line control.

    When the pointer moves onto the form, it changes to a cross hair.

  2. Click the form where you want the line to begin and hold down the mouse button.

  3. Drag the cross hair to where you want the line to end and release the mouse button.

  4. From the Properties window, select the BorderStyle property if you want to change the appearance of the line.

  5. In the Settings box, select the style you want.


Using the List Box Control

A list box control displays a list of items from which the user can select one or more.

Figure 7.32 The list box control

List boxes present a list of choices to the user. By default, the choices are displayed vertically in a single column, although you can set up multiple columns as well. If the number of items exceeds what can be displayed in the list box, scroll bars automatically appear on the control. The user can then scroll up and down, or left to right through the list. Figure 7.33 shows a single-column list box.

Figure 7.33 Single-column list box

Data-Bound Features

Visual Basic includes both standard and data-bound versions of the list box control. While both versions of the list box control allow you to display, edit, and update information from most standard types of databases, the DataList provides more advanced data access features. The DataList control also supports a different set of properties and methods than the standard list box control.

For More Information See "Using the DataCombo and DataList Controls" for more information on the data-bound version of the list box control.

The Click and Double-Click Events

A recommended practice for list box events, especially when the list box appears as part of a dialog box, is to add a command button to use with the list box. The Click event procedure for this button should make use of the list-box selection, carrying out whatever action is appropriate for your application.

Double-clicking an item in the list should have the same effect as selecting the item and then clicking the command button. To do this, have the DblClick procedure for the list box call the Click procedure for the command button:

Private Sub List1_DblClick ()
   Command1_Click
End Sub

Or, set the value of the command button's Value property to True, which will automatically invoke the event procedure:

Private Sub List1_DblClick ()
   Command1.Value = True
End Sub

This provides mouse users with a shortcut, yet does not prevent keyboard users from performing the same action. Note that there is no keyboard equivalent for the DblClick event.

Adding Items to a List

To add items to a list box, use the AddItem method, which has the following syntax:

box.AddItem item[, index]

Argument | Description
box | Name of the list box.
item | String expression to add to the list. If item is a literal constant, enclose it in quotation marks.
index | Specifies where the new item is to be inserted in the list. An index of 0 represents the first position. If index is omitted, the item is inserted at the end (or in the proper sorted order).

While list items are commonly added in the Form_Load event procedure, you can use the AddItem method at any time. This gives you the ability to add items to the list dynamically (in response to user actions).

The following code places "Germany," "India," "France," and "USA" into a list box named List1:

Private Sub Form_Load ()
   List1.AddItem "Germany"
   List1.AddItem "India"
   List1.AddItem "France"
   List1.AddItem "USA"
End Sub

Whenever the form is loaded at run time, the list appears as shown in Figure 7.34.

Figure 7.34 "Countries" list box

Adding an Item at a Specified Position

To add an item to a list at a specific position, specify an index value for the new item. For example, the next line of code inserts "Japan" into the first position, adjusting the position of the other items downward:

List1.AddItem "Japan", 0

Notice that it is 0, not 1, that specifies the first item in a list (see Figure 7.35).

Figure 7.35 Adding an item to a list

Adding Items at Design Time

You can also enter items into the list at design time by setting the List property in the Properties window of the list box control. When you select the List property option and then click the down arrow, you can type list items and then press the CTRL+ENTER key combination to start a new line.

You can only add items to the end of the list. So, if you want to alphabetize the list, set the Sorted property to True. See "Sorting a List" below for more information.

Sorting a List

You can specify that items be added to a list in alphabetical order by setting the Sorted property to True and omitting the index. The sort is not case-sensitive; thus, the words "japan" and "Japan" are treated the same.

When the Sorted property is set to True, using the AddItem method with the index argument can lead to unpredictable, unsorted results.

Removing Items from a List

You can use the RemoveItem method to delete items from a list box. RemoveItem has one argument, index, which specifies the item to remove:

box.RemoveItem index

The box and index arguments are the same as for AddItem.

For example, to remove the first entry in a list, you would add the following line of code:

List1.RemoveItem 0

To remove all list entries in bound or standard versions of the list and combo boxes, use the Clear method:

List1.Clear

Getting List Contents with the Text Property

Usually, the easiest way to get the value of the currently selected item is to use the Text property. The Text property always corresponds to a list item a user selects at run time.

For example, the following code displays information about the population of Canada if a user selects "Canada" from a list box:

Private Sub List1_Click ()
   If List1.Text = "Canada" Then
      Text1.Text = "Canada has 24 million people."
   End If
End Sub

The Text property contains the currently selected item in the List1 list box. The code checks to see if "Canada" has been selected and, if so, displays the information in the Text box.

Accessing List Items with the List Property

The List property provides access to all items in the list. This property contains an array in which each item in the list is an element of the array. Each item is represented in string form. To refer to an item in the list, use this syntax:

box.List(index)

The box argument is a reference to a list box, and index is the position of the item. The top item has an index of 0, the next has an index of 1, and so on. For example, the following statement displays the third item (index = 2) in a list in a text box:

Text1.Text = List1.List(2)

Determining Position with the ListIndex Property

If you want to know the position of the selected item in a list, use the ListIndex property. This property sets or returns the index of the currently selected item in the control and is available only at run time. Setting the ListIndex property for a list box also generates a Click event for the control.

The value of this property is 0 if the first (top) item is selected, 1 if the next item down is selected, and so on. ListIndex is – 1 if no item is selected.

Note The NewIndex property allows you to keep track of the index of the last item added to the list. This can be useful when inserting an item into a sorted list.

Returning the Number of Items with the ListCount Property

To return the number of items in a list box, use the ListCount property. For example, the following statement uses the ListCount property to determine the number of entries in a list box:

Text1.Text = "You have " & List1.ListCount & " _
entries listed"

Creating Multiple-Column and Multiple-Selection List Boxes

The Columns property allows you to specify the number of columns in a list box. This property can have the following values:

Value | Description
0 | Single-column list box with vertical scrolling.
1 | Single-column list box with horizontal scrolling.
>1 | Multiple-column list box with horizontal scrolling.

Visual Basic takes care of wrapping list items to the next line and adding a horizontal scroll bar to the list if needed; if the list fills a single column, no scroll bar is added. Wrapping to the next column also occurs automatically as needed. Note that if a list box entry is wider than the width of a column, the text is truncated.

You can allow users to select multiple items from a list. Multiple selection in standard list boxes is handled by setting the MultiSelect property, which can have the following values.

Value | Type of selection | Description
0 | None | Standard list box.
1 | Simple multiple selection | A click or the SPACEBAR selects or deselects additional items in the list.
2 | Extended multiple selection | The SHIFT+ click or SHIFT+ an arrow key extends the selection to include all the items between the current and previous selections. CTRL+ click selects or deselects an item in the list.

For More Information See "List Box Control Scenario 2: Creating Multiple-Column List Boxes" later in this chapter for more information on the Columns and MultiSelect properties.


List Box Control Scenario 1: Adding and Deleting Items

This example shows how you can use the AddItem, RemoveItem, and Clear methods with the ListIndex and ListCount properties to add and remove list entries at run time. The example in Figure 7.36 lets a user type a client's name in a text box, which can be added to the list box if the Add button is clicked. A user can remove a current list item by selecting the item and choosing the Remove button, or by choosing Clear to clear all list entries.

Figure 7.36 A list box using the AddItem, RemoveItem, and Clear methods

The number of clients in the list box is displayed in a label that looks like a text box (BorderStyle is set to 1-Fixed Single). This label is updated every time a client name is added or removed. Because the Sorted property for the list box is set to True, items are added to the list box in alphabetical order.

Create a form with a text box, a list box, three labels, and four command buttons. The following table lists the property settings for the objects in the application.

Object | Property | Setting
Top text box | Name Text | txtName (Empty)
Top label | Name Caption | lblName &Name to add
List box | Name Sorted | lstClient True
Bottom label | Name Caption | lblClients # Clients
Number of clients label (looks like a text box) | Name Caption BorderStyle | lblDisplay (Empty) 1-Fixed Single
First command button | Name Caption | cmdAdd &Add
Second command button | Name Caption | cmdRemove &Remove
Third command button | Name Caption | cmdClear &Clear
Fourth command button | Name Caption | cmdClose &Close

Events in the List Box Application

Add this code to the cmdAdd_Click event procedure:

Private Sub cmdAdd_Click ()
   lstClient.AddItem txtName.Text   ' Add to list.
   txtName.Text = ""      ' Clear text box.
   txtName.SetFocus
   ' Display number.
   lblDisplay.Caption = lstClient.ListCount
End Sub

Add this code to the cmdRemove_Click event procedure:

Private Sub cmdRemove_Click ()
   Dim Ind As Integer

   Ind = lstClient.ListIndex   ' Get index.
   ' Make sure list item is selected.
   If Ind >= 0 Then
      ' Remove it from list box.
      lstClient.RemoveItem Ind
      ' Display number.
      lblDisplay.Caption = lstClient.ListCount
   Else
      Beep
   End If
   ' Disable button if no entries in list.
   cmdRemove.Enabled = (lstClient.ListIndex <> -1)
End Sub

Add this code to the cmdClear_Click event procedure:

Private Sub cmdClear_Click ()
   ' Empty list box.
   lstClient.Clear
   ' Disable Remove button.
   cmdRemove.Enabled = False
   ' Display number.
   lblDisplay.Caption = lstClient.ListCount
End Sub

Add this code to the cmdClose_Click event procedure:

Private Sub cmdClose_Click ()
   Unload Me
End Sub

Add this code to the lstClient_Click event procedure:

Private Sub lstClient_Click ()
   cmdRemove.Enabled = lstClient.ListIndex <> -1
End Sub

Add this code to the txtName_Change event procedure:

Private Sub txtName_Change ()
' Enable the Add button if at least one character
' in the name.
cmdAdd.Enabled = (Len(txtName.Text) > 0)
End Sub


List Box Control Scenario 2: Creating Multiple-Column List Boxes

To create a multiple-column, multiple-selection list box, you need to set both the Columns and the MultiSelect properties of a list box. In the following example, these properties are used to create such a list box.

You'll notice that when you run the application, the list box contains two columns, as shown in Figure 7.37.

Figure 7.37 Multiple-column list box

If you draw the list box large enough to hold all the items in one column, the second column will be empty; the other items will wrap, and horizontal scroll bars will appear automatically only if the list box is not long enough. Try resizing the top list box and adding additional list items to see how Visual Basic automatically handles multiple columns.

The example uses the Selected property — a Boolean array containing the selection status of a list box — to determine which items are selected. Each entry in the array corresponds to a list item and is set to True if the item is selected, or False if it is not selected. After the user selects items from the list, each array entry is checked to see if it is set (True). If so, the entry is added to the second list, a normal single-column list box, using the AddItem method.

Set the properties for the example as indicated in the following table.

Object | Property | Setting
Form | Caption | Multiple-Column List Box
Top list box | Name Columns MultiSelect | lstTop 2 2-Extended
Bottom list box | Name | lstBottom
First command button | Name Caption | cmdTransfer &Transfer
Second command button | Name Caption | cmdClear &Clear
Third command button | Name Caption | cmdClose &Close

The MultiSelect property allows you to select a range of values in a list box. If you click the first list item, and then press SHIFT and click the last item in the range (or use the SHIFT+ DOWN ARROW keys), all the items in the range are selected.

Events in the Multiple-Column List Box Application

Add code to the Form_Load procedure to initialize the top list, 1stTop:

Private Sub Form_Load ()
   lstTop.AddItem "Paris"
   lstTop.AddItem "New Orleans"
   lstTop.AddItem "San Francisco"
   lstTop.AddItem "Chicago"
   lstTop.AddItem "Seattle"
   lstTop.AddItem "Toronto"
   lstTop.AddItem "New York"
   lstTop.AddItem "Tbilisi"
   lstTop.AddItem "Moscow"
   lstTop.AddItem "Portland"
   ' Select a couple of items.
   1stTop.Selected(0) = True
   1stTop.Selected(1) = True
End Sub

Note You can add items to list boxes without repeatedly using the AddItem method by typing items in the List property of the Properties window. After entering each item, press CTRL+ENTER to go to the next line. This allows you to type multiple entries in a multiple-column list box.

Add the following code to the 1stTop_DblClick event procedure:

Private Sub 1stTop_DblClick ()
   cmdTransfer.Value = True   ' Press transfer button.
End Sub

Add the following code to the Click event procedure for the Transfer command button:

Private Sub cmdTransfer_Click ()
   For n = 0 To (lstTop.ListCount - 1)
' If selected, add to list.
      If lstTop.Selected(n) = True Then
         lstBottom.AddItem lstTop.List(n)
      End If
   Next
   cmdClear.Enabled = True
End Sub

Notice how the array index values start from 0 and go to ListCount -1.

Add the following code to the Click event procedure for the Clear command button:

Private Sub cmdClear_Click ()
   lstBottom.Clear
   cmdClear.Enabled = False
End Sub

Add the following code to the Click event procedure for the Close command button.

Private Sub cmdClose_Click ()
   Unload Me
End Sub


Using the OLE Container Control

You can link or embed any object that supports Automation (formerly called OLE Automation) into the OLE container control. Using this control, your Visual Basic application can display and manipulate data from other Windows–based applications, such as Microsoft Excel and Microsoft Word for Windows.

Figure 7.38 The OLE container control

The OLE container control is used to create a document-centered application. In such an application, the user combines data from different applications to create a single document. This type of application may be a word processor that allows the user to enter text and then embed a spreadsheet or chart.

The OLE container control allows you to add objects from other applications to your Visual Basic applications. With this control, you can:

  • Create a placeholder in your application for an object. You can create the object that appears within the OLE container control at run time, or you can change an object you have placed in the OLE container control at design time.

  • Create a linked object in your application.

  • Bind the OLE container control to a database.

  • Perform an action if the user moves, sizes, or updates the object in the OLE container control.

  • Create objects from data that was copied onto the Clipboard.

  • Display objects as icons.

  • Provide backward compatibility with an application that includes many OLE container controls (called OLE client controls in previous versions of Visual Basic).

For More Information See "Using a Component's Visual Interface" "Programming with Components" for a detailed discussion and examples of how the OLE container control is used.


Using the Option Button Control

Option button controls are used to display options, usually in option button groups, from which the user can choose one.

Figure 7.39 The option button control

While option button controls and check box controls may appear to function similarly, there is an important difference: when a user selects an option button, the other option button controls in the same group are automatically unavailable. In contrast, any number of check box controls can be selected.

For More Information See "Grouping Options with Option Buttons" in "Forms, Controls, and Menus" for a demonstration of the use of option buttons.

Creating Option Button Groups

You group option button controls by drawing them inside a container such as a frame control, a picture box control, or a form. At run time, the user may select a single option button from each distinct group of option buttons. For example, if you add option buttons to a form and option buttons to a frame control on the form, you have created two distinct groups of option buttons.

Figure 7.40 Creating option button groups

All option buttons that are added directly to the form become one group. To add additional groups, you need to place them inside frame or picture box controls.

To group option button controls in a frame or picture box, draw the frame or picture box first, and then draw the option button controls inside. At design time, option buttons contained within a frame or picture box control may be selected and moved as a single unit.

To select multiple controls contained inside a frame control, a picture box control, or a form, hold down the CTRL key while using the mouse to draw a box around the controls.

For More Information See "Using the Frame Control" earlier in this chapter for more information on drawing controls inside a frame.

Selecting an Option Button at Run Time

An option button may be selected at run time in several ways: clicking it with the mouse, using the TAB key to shift the focus to the control, using the TAB key to select a group of option button controls and then using the arrow keys to select one within the group, creating a shortcut key in the option button's caption, or, by setting its Value property to True in code.

The Click Event

When an option button is selected, its Click event is triggered. Depending upon the functionality of your application, you may or may not need to respond to this event. Responding to this event is useful when you want to update a label control's caption to provide the user with information about the option that has been selected, for example.

The Value Property

The Value property of the option button control indicates whether the option button is selected. When selected, the value is changed to True. You can select an option button in code by setting its Value property. For example:

optPentium.Value = True

To make an option button the default within an option button group, set the Value property at design time using the Properties window or at run time in code, as shown above.

When you present the user with a dialog box containing option buttons, you are asking them to select options that will determine what your application will do next. You can use the Value property of each of the option button controls to determine which option or options were selected and then respond accordingly.

Creating Keyboard Shortcuts

You can use the Caption property to create access key shortcuts for your option buttons by adding an ampersand (&) before the letter you want to use as the access key. For example, to create an access key for the option button caption "Pentium" you add an ampersand before the letter "P": "&Pentium". At run time, the letter "P" will be underlined and the user can select the command button by simultaneously pressing ALT+P.

Note To include an ampersand in a caption without creating an access key, include two ampersands (&&). A single ampersand is displayed in the caption and no characters are underlined.

Disabling an Option Button

To disable an option button, set its Enabled property to False. At run time, the option button will appear dimmed, meaning that it is unavailable.

Visually Enhancing the Option Button Control

The look of the option button control may be enhanced by altering the setting of the Style property and then using the Picture, DownPicture and DisabledPicture properties.


Using the Picture Box Control

The picture box control is used to display graphics, to act as a container for other controls, and to display output from graphics methods or text using the Print method.

Figure 7.41 The picture box control

The picture box control is similar to the image control in that each can be used to display graphics in your application — each supports the same graphic formats. The picture box control, however, contains functionality which the image control does not, for example: the ability to act as a container for other controls and support for graphics methods.

For More Information See "Working with the Picture Box Control" "Forms, Controls, and Menus" for a demonstration of the use of picture boxes.

Supported Graphic Formats

The picture box control can display picture files in any of the following formats: bitmap, cursor, icon, metafile, enhanced metafile, or as JPEG or GIF files.

For More Information See "Using the Image Control" earlier in this chapter for detailed descriptions of these graphic formats.

Loading a Graphic Into the Picture Box Control

Pictures can be loaded into the picture box control at design time by selecting the Picture property from the control's Properties window, or at run time by using the Picture property and the LoadPicture function.

Set Picture1.Picture = _
LoadPicture("c:\Windows\Winlogo.cur", vbLPLarge, vbLPColor)

You may want to use icon (.ico) and cursor (.cur) files containing separate images at different sizes and color depths to support a range of display devices. The LoadPicture function's settings allow you to select images with specific color depths and sizes from an .ico or .cur file. In cases where an exact match to the requested settings isn't available, LoadPicture loads the image with the closest match available.

To clear the graphic from the picture box control, use the LoadPicture function without specifying a file name. For example:

Set Picture1.Picture = LoadPicture

This will clear the picture box control even if a graphic was loaded into the Picture property at design time.

Using the Clipboard

You can also add a graphic to a picture box control at design time by pasting it from another application. For example, you may want to add a bitmap image that was created in Windows Paint. Simply copy the image to the clipboard, select the picture box control, and either use the keyboard shortcut CTRL+V or the Paste command from the Edit menu.

Sizing a Picture

By default, graphics are loaded into a picture box at their original size, meaning that if the graphic is larger than the control, the image will be clipped — the picture box control does not provide scroll bars. To make a picture box control automatically resize to display an entire graphic, set its AutoSize property to True. The control will then size to the graphic — growing or shrinking.

Unlike the image control, the picture box control cannot stretch the image to fit the size of the control.

For More Information See "Scroll Bar Controls Scenario: Creating a Scrollable Graphics Viewport" earlier in this chapter for information on using picture boxes to create a scrollable graphics viewport.

Using the Picture Box Control as a Container

You can use the picture box control as a container for other controls. For example, since the picture box can be placed inside the internal area of a MDI form, it is often used to manually create a toolbar or status bar.

For More Information See "Creating a Toolbar" in "Creating a User Interface" for more information on using the picture box control as a container for other controls.

Graphics Methods

Picture boxes, like forms, can be used to receive the output of graphics methods such as Circle, Line, and Point. For example, you can use the Circle method to draw a circle in a picture box by setting the control's AutoRedraw property to True.

Picture1.AutoRedraw = True
Picture1.Circle (1200, 1000), 750

Setting AutoRedraw to True allows the output from these methods to be drawn to the control and automatically redrawn when the picture box control is resized or redisplayed after being hidden by another object.

For More Information See "Using Graphics Methods" "Working with Text and Graphics" for more information about using the picture box control with the graphics methods.

Using the Print Method

You can use the picture box control to output text by using the Print method and setting the AutoRedraw property to True. For example:

Picture1.Print "A text string"

When using the Print method you can also modify the font style and size or use the CurrentX, CurrentY, Height, and Width properties to align text within a picture box.

For More Information See "Working with Text and Graphics" and "Print Method" for more information.


Using the RemoteData Control

The Remote Data control implements data access by using the Microsoft Remote Data Objects. This technology gives you seamless access to many standard database formats and allows you to create data-aware applications without writing any code. The Remote Data control is suited to larger client-server databases, including Open Database Connectivity (ODBC) databases such as Microsoft SQL Server and Oracle.

It should be noted that the RemoteData control does not work with Image data types.

Note Both the Remote Data control and Data control are included with Visual Basic for backward compatibility. However, because of the flexibility of ActiveX Data Objects (ADO), it's recommended that you create new database applications using the ADO Data control. For more details, see "Using the ADO Data Control."

The Data control, Remote Data control, and the ADO Data control are all conceptually similar: all three are "data controls" that connect a data source to a data-bound control. All three also share the same appearance — a set of four buttons that allow the user to go immediately to the beginning of the recordset, end of the recordset, or scroll backwards and forwards through the recordset.

Creating a Simple Database Application with the Remote Data Control

The following procedures create a simple database application using the Data control.

To create a simple database application with the Remote Data Control

  1. Draw a Remote Data control on the form. (The icon's tooltip control is "MSRDC.")

    If the Remote Data control is not in the Toolbox, press CTRL+T to display the Components dialog box. In the Components dialog box, click Microsoft RemoteData Control. Click OK to add it to the Toolbox.

  2. Click the Remote Data control to select it, and press F4 to display the Properties window.

  3. In the Properties window, set the DataSourceName property to the DSN of the database you want to connect to.

  4. On the Properties window, click the SQL property and type in a SQL statement, for example:
    SELECT * FROM Products WHERE AuthorID = 72

    You should always include a WHERE clause when accessing a table. Failing to do so will lock the entire table, which would be a major hindrance to other users.

  5. Draw a TextBox control on the form.

  6. Click the TextBox control to select it, and on the Properties window set the DataSource property to the Remote Data control.

  7. On the Properties window, set the DataField property to the name of the field in the database you want to view or modify.

  8. Repeat steps 5, 6, and 7 for each additional field you want to access.

  9. Press F5 to run the application.

Setting Data-Related Properties of the RemoteData Control

The following data-related properties can be set at design time. The list suggests a logical order for setting the properties:

  1. Connect — The Connect property is a string that can contain all the settings necessary to make a connection. The parameters passed in the string are driver-dependent. For example, ODBC drivers allow the string to contain driver, database, user name, and password.

  2. UserName — Identifies a user to a protected database. The user must also supply a valid password that the database management system recognizes. The user name can also be contained in the Connect property value, making it redundant here.

  3. Password — Along with UserName, the Password allows the user to access protected data. The password can also be contained in the Connect property value, making it redundant here.

  4. SQL — The SQL property contains the SQL statement used to retrieve a resultset. The size of the resultset may determine whether you want to use a client-side or server-side cursor. For example, a small resultset can be managed on a client-side cursor.

  5. RowSetSize — Sets the number of rows returned in a resultset, if the cursor is a keyset. You can fine-tune this number to the computer's resources (memory) for performance gains.

  6. ReadOnly — Specifies if the data will be written to. If writing data is not needed, setting this property to True can give you a performance gain.

  7. CursorDriver — Determines the location and type of driver. The setting of this property affects how you set other properties. For example, selecting ODBC client-side cursors may give you performance increases as long as the resultset is small.

  8. LockType — The LockType determines how the data is locked when others attempt to change the data. If you do not expect others to change the data (while you are looking at it) you can set the LockType to "optimistic" — leaving the data free to be viewed and changed by others. If you set it to "pessimistic," others cannot access the data while you are accessing it.

  9. BOFAction, EOFAction — These two properties determine what will happen when the control is at the beginning and end of the cursor. Choices include staying at the beginning or end, moving to the first or last record, or adding a new record (at the end only).

  10. ResultSetType — Determines if the cursor is static or a keyset type.

  11. KeySetSize — If the cursor is a keyset, you can optimize the size of the returned resultset with the KeySetSize property.

  12. LoginTimeout — Sets the number of seconds to wait until an error is returned.

  13. MaxRows — Specifies how large the cursor will be. How you determine this depends on the size of the records you are retrieving and the resources available on your computer (memory). A large record (one with many columns and large strings) would take more resources than a smaller record. The MaxRows property should be consequently decreased.

  14. Options — Specifies whether or not the control executes queries asynchronously. Use asynchronous operation when you expect a query to take more than a few minutes to execute.

  15. Prompt — When RDO opens a connection based on the parameters of the RemoteData control, the Connect property is expected to contain sufficient information to establish the connection. If information like the data source name, user name, or password is not provided, the ODBC driver manager exposes one or more dialog boxes to gather this information from the user. If you do not want these dialog boxes to appear, set the Prompt property accordingly to disable this feature.

  16. QueryTimeout — Sets the number of seconds to wait for a query to complete before returning an error.

  17. BatchSize — This property determines how many statements can be sent in a batch--if batch statements are allowed by the driver.


Using the Shape Control

The shape control is used to create the following predefined shapes on forms, frames, or picture boxes: rectangle, square, oval, circle, rounded rectangle, or rounded square.

Figure 7.42 The shape control

You can set the shape style, color, fill style, border color, and border style of any of the shapes you draw on a form.

For simple uses, the shape control allows you to create a variety of shapes without writing any code. For more advanced functionality you need to use the Line and Circle methods.

For More Information See "Using Graphics Methods" in "Working with Text and Graphics" for more information on drawing lines, rectangles, and filled-in boxes at run time using the Line method or for more information on drawing circles, ellipses, and arcs at run time using the Circle method.

Predefined Shapes

The Shape property of the shape control provides you with six predefined shapes. The following table lists all the predefined shapes, their values and equivalent Visual Basic constants:

Shape | Style | Constant
Rectangle | 0 | vbShapeRectangle
Square | 1 | vbShapeSquare
Oval | 2 | vbShapeOval
Circle | 3 | vbShapeCircle
Rounded Rectangle | 4 | vbShapeRoundedRectangle
Rounded Square | 5 | vbShapeRoundedSquare

Figure 7.43 Predefined shapes

Fill and Line Styles

You can use the FillStyle and BorderStyle properties to set the fill style and border style of any of the shapes you draw on a form.

The FillStyle property, like the Style property, provides you with a number of predefined fill style patterns. These include: Solid, Transparent, Horizontal Line, Vertical Line, Upward Diagonal, Downward Diagonal, Cross, and Diagonal Cross.

The BorderStyle property provides you with a number of predefined border styles. These include: Transparent, Solid, Dash, Dot, Dash-Dot, Dash-Dot-Dot, and Inside Solid.

For More Information Both the FillStyle and BorderStyle properties provide you with constants that represent the styles listed above. See "FillStyle Property" and "BorderStyle Property" for more information.

Setting Color Attributes

The BackColor and FillColor properties allow you to add color to the shape and its border.

At design time, you can set the fill or border colors by choosing either property from the Properties window of the shape control and then selecting from the available palette or system colors.

To set colors at run time, use the Visual Basic color constants (vbGreen, for example) or the system color constants (vbWindowBackground, for example) or the RGB function to specify fill colors.

Note When the FillStyle or BackStyle properties are set to 1 (Transparent), the FillColor and BackColor properties are ignored.

For More Information See "Color Constants" for a list of Visual Basic color constants. See "RGB Function" for information on specifying RGB colors. Also, refer to "Working with Text and Graphics" for detailed information on creating graphics in Visual Basic.

Drawing Shapes on a Form

You can use the shape control to draw rectangles (regular or rounded corners), squares (regular or rounded corners), ovals, and circles on a form.

To draw a shape on a form

  1. In the toolbox, select the Shape control.

    When the pointer moves onto the form, it changes to a cross hair.

  2. Click and drag the cross hair to make the item the size you want.

  3. From the Properties window, select the Shape property.

  4. In the Settings box, select the style you want.

Shapes can be sized like any other controls, by selecting and dragging the control to the desired size, or by setting the Height and Width properties.


Using the Text Box Control

The text box control is used to display information entered by the user at run time, or assigned to the Text property of the control at design or run time.

Figure 7.44 The text box control

In general, the text box control should be used for editable text, although you can make it read-only by setting its Locked property to True. Text boxes also allow you to display multiple lines, to wrap text to the size of the control, and to add basic formatting.

The Text Property

Text entered into the text box control is contained in the Text property. By default, you can enter up to 2048 characters in a text box. If you set the MultiLine property of the control to True, you can enter up to 32K of text.

Formatting Text

When text exceeds the boundaries of the control, you can allow the control to automatically wrap text by setting the MultiLine property to True and add scroll bars by setting the ScrollBars property to add either a horizontal or vertical scroll bar, or both. Automatic text wrapping will be unavailable, however, if you add a horizontal scroll bar because the horizontal edit area is increased by the presence of the scroll bar.

When the MultiLine property is set to True, you can also adjust the alignment of the text to either Left Justify, Center, or Right Justify. The text is left-justified by default. If the MultiLine property is False, setting the Alignment property has no effect.

For More Information See "Working with Text Boxes" for a demonstration of the MultiLine, ScrollBar, and Alignment properties.

Selecting Text

You can control the insertion point and selection behavior in a text box with the SelStart, SelLength and SelText properties.

For More Information See "Working with Text Boxes" in "Forms, Controls, and Menus" for a demonstration of the SelStart, SelText, and SelLength properties.

Creating a Password Text Box

A password box is a text box that allows a user to type in his or her password while displaying placeholder characters, such as asterisks. Visual Basic provides two text box properties, PasswordChar and MaxLength, which make it easy to create a password text box.

PasswordChar specifies the character displayed in the text box. For example, if you want asterisks displayed in the password box, you specify * for the PasswordChar property in the Properties window. Regardless of what character a user types in the text box, an asterisk is displayed, as shown in Figure 7.45.

Figure 7.45 Password example

With MaxLength, you determine how many characters can be typed in the text box. After MaxLength is exceeded, the system emits a beep and the text box does not accept any further characters.

Canceling Keystrokes in a Text Box

You can use the KeyPress event to restrict or transform characters as they are typed. The KeyPress event uses one argument, keyascii. This argument is an integer that represents the numeric (ASCII) equivalent of the character typed in the text box.

The next example demonstrates how to cancel keystrokes as they are typed. If the character typed is not within the specified range, the procedure cancels it by setting KeyAscii to 0. The text box for this example is named txtEnterNums, and the procedure prevents the text box from receiving any characters other than digits. Compare KeyAscii directly to the numeric (Asc) values of various characters.

Private Sub txtEnterNums_KeyPress (KeyAscii As Integer)
   If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then
      KeyAscii = 0   ' Cancel the character.
      Beep            ' Sound error signal.
   End If
End Sub

For More Information See "Responding to Keyboard Events" in "Responding to Mouse and Keyboard Events" for more information about the KeyPress event.

Creating a Read-Only Text Box

You can use the Locked property to prevent users from editing text box contents. Set the Locked property to True to allow users to scroll and highlight text in a text box without allowing changes. With the Locked property set to True, a Copy command will work in a text box, but Cut and Paste commands will not. The Locked property only affects user interaction at run time. You can still change text box contents programmatically at run time by changing the Text property of the text box.

Printing Quotation Marks in a String

Sometimes quotation marks (" ") appear in a string of text.

She said, "You deserve a treat!"

Because strings assigned to a variable or property are surrounded by quotation marks (" "), you must insert an additional set of quotation marks for each set to display in a string. Visual Basic interprets two quotation marks in a row as an embedded quotation mark.

For example, to create the preceding string, use the following code:

Text1.Text = "She said, ""You deserve a treat!"" "

To achieve the same effect, you can use the ASCII character (34) for a quotation mark:

Text1.Text = "She said, " & Chr(34) + "You deserve a treat!" & Chr(34)


Using the Timer Control

Timer controls respond to the passage of time. They are independent of the user, and you can program them to take actions at regular intervals. A typical response is checking the system clock to see if it is time to perform some task. Timers also are useful for other kinds of background processing.

Figure 7.46 The timer control

Each timer control has an Interval property that specifies the number of milliseconds that pass between one timer event to the next. Unless it is disabled, a timer continues to receive an event (appropriately named the Timer event) at roughly equal intervals of time.

The Interval property has a few limitations to consider when you're programming a timer control:

  • If your application or another application is making heavy demands on the system — such as long loops, intensive calculations, or drive, network, or port access — your application may not get timer events as often as the Interval property specifies.

  • The interval can be between 0 and 64,767, inclusive, which means that even the longest interval can't be much longer than one minute (about 64.8 seconds).

  • The interval is not guaranteed to elapse exactly on time. To ensure accuracy, the timer should check the system clock when it needs to, rather than try to keep track of accumulated time internally.

  • The system generates 18 clock ticks per second — so even though the Interval property is measured in milliseconds, the true precision of an interval is no more than one-eighteenth of a second.

Every timer control must be associated with a form. Therefore, to create a timer application, you must create at least one form (though you don't have to make the form visible if you don't need it for any other purpose).

Note The word "timer" is used in several ways in Visual Basic, each closely related to the workings of the timer control. In addition to the control name and control type, "timer" is used in the Timer event and the Timer function.

Sample Application: Alarm.vbp

Techniques for working with the timer control are included in the Alarm sample application (Alarm.vbp) which is listed in the Samples directory.

Placing a Timer Control on a Form

Placing a timer control on a form is like drawing any other control: Click the timer button in the toolbox and drag it onto a form.

The timer appears on the form at design time only so you can select it, view its properties, and write an event procedure for it. At run time, a timer is invisible and its position and size are irrelevant.

Initializing a Timer Control

A timer control has two key properties.

Property | Setting
Enabled | If you want the timer to start working as soon as the form loads, set it to True. Otherwise, leave this property set to False. You might choose to have an outside event (such as a click of a command button) start operation of the timer.
Interval | Number of milliseconds between timer events.

Note that the Enabled property for the timer is different from the Enabled property for other objects. With most objects, the Enabled property determines whether the object can respond to an event caused by the user. With the Timer control, setting Enabled to False suspends timer operation.

Remember that the Timer event is periodic. The Interval property doesn't determine "how long" as much as it determines "how often." The length of the interval should depend on how much precision you want. Because there is some built-in potential for error, make the interval one-half the desired amount of precision.

Note The more often a timer event is generated, the more processor time is used in responding to the event. This can slow down overall performance. Don't set a particularly small interval unless you need it.


Timer Control Scenario: Responding to the Timer Event

When a timer control's interval elapses, Visual Basic generates the Timer event. Typically, you respond to this event by checking some general condition, such as the system clock.

A digital clock is a very simple but very useful application involving a timer control. Once you understand how the application works, you can enhance it to work as an alarm clock, stopwatch, or other timing device.

The Digital Clock application includes a timer and a label with a border. At design time, the application looks like Figure 7.47.

Figure 7.47 The Digital Clock application

At run time, the timer is invisible. The following table lists the property settings for the Digital Clock application.

Control | Property | Setting
Label1 | BorderStyle | Fixed Single
Timer1 | Interval | 500 (half a second)
Timer1 | Enabled | True

The only procedure in this application is an event procedure for the timer:

Private Sub Timer1_Timer ()
   If lblTime.Caption <> CStr(Time) Then
      lblTime.Caption = Time
   End If
End Sub

The procedure displays the system time by calling the intrinsic Time function. This function returns a Variant containing the current time as a date/time value (VarType 7). When you assign it to a string variable or property, such as the Caption property in this case, Visual Basic converts it to a string using the time format specified in the Control Panel. If you want to display it using a different format, you can use the Format function.

For More Information See "Format Property."

The Interval property for the timer is set to 500, following the rule of setting the Interval to half of the shortest period you want to distinguish (one second in this case). This may cause the timer code to update the label with the same time twice in one second. This is wasteful and can cause some visible flicker, so the code tests to see if the time is different from what is displayed in the label before it changes the caption.

You can customize the look of the Digital Clock without having to write any additional statements. For example, you might want to select a different font for the label or change the BorderStyle property of the form.


More About Programming

This chapter goes beyond the fundamentals of Visual Basic programming and introduces a variety of features that make it easier for you to create powerful, flexible applications.

For example, you can load multiple projects into a single session of the programming environment, work with Windows registry settings, or selectively compile certain parts of your program.

Beyond the fundamentals of writing code, Visual Basic provides a variety of language elements that enhance your code. The last four topics in this chapter discuss four of these language elements: user-defined types, enumerated constants, arrays, and collections.

Topics

Working with Multiple Projects

An introduction to working with multiple projects in the same session of the programming environment.
Managing Application Settings

An introduction to working with Windows registry settings.
Using Conditional Compilation

An introduction to selectively compiling certain parts of your program to create platform-specific or language-specific versions.
Working with Resource Files

An introduction to using resource files to store version-specific text and bitmaps.
Working with Templates

An introduction to using templates to make it easier to build applications and objects.
Working with Command Line Switches

An introduction to using command line switches to control how Visual Basic executes.
Compiling Your Project to Native Code

A discussion of how you can use the Professional or Enterprise edition of Visual Basic to compile your code in native code format.
Creating Your Own Data Types

An introduction to creating and working with user-defined types.
Using Enumerations to Work with Sets of Constants

An introduction to using enumerated constants to work with groups of related constants.
Using Collections as an Alternative to Arrays

A discussion of when you might use collections instead of arrays to work with sets of items.


Working with Multiple Projects

You can create many applications by working with a single project. However, as your applications become more complex, you may want to work with multiple projects in the same session of the programming environment. For example, you may want to use one project to build an application's executable file, and a second project to serve as a "scratch pad" for testing code before you add it to the application.

You can add a new or existing project to your current editing session by adding it to a project group. You can then save the project group and work with it in subsequent editing sessions. You can open either the project group or an individual project in the project group, or add the project group or its individual projects to another project group.

In a project group, one executable project serves as a startup project. When a project group is open and you choose Start from the Run menu, click the Start button on the toolbar, or press F5, Visual Basic runs the startup project.

In the Professional or Enterprise edition, you can use project groups to create and debug multiple-component applications. For example, you can create and debug project groups containing standard executable projects, ActiveX executable projects, ActiveX dynamic-link library projects, or ActiveX control projects. For more information, see "Creating ActiveX Components" in the Component Tools Guide included with the Professional and Enterprise Editions of Visual Basic.

Adding or Removing a Project

When you create a new project either at startup or by selecting the New Project command on the File menu, Visual Basic automatically creates a project group for it. You can then add additional new or existing projects to the project group.

To add a new project to a project group

  • Click the Add Project button on the Toolbar or choose Add Project from the File menu.

    In Visual Basic, Learning edition, Visual Basic automatically adds a new executable project to the project group. In the Professional and Enterprise editions, you can select the type of project you want to add from the popup menu of the Add Project button, or the icons displayed in the New tab of the Add Project dialog box.

To add an existing project to a project group

  1. From the File menu, choose Add Project.

    Visual Basic displays the Add Project dialog box.

  2. Click the Existing tab.

  3. Select a project file, then choose Open.

    Visual Basic adds the selected project to the project group.

Visual Basic displays multiple projects in the Project window in a hierarchical view. Each project appears at the top level, with the project's forms, modules, controls, property pages, or document objects grouped under it in the hierarchical view.

Figure 8.1 The Project window with multiple projects

To remove a project from a project group

  1. Select the name of the project in the Project window.

  2. From the File menu, choose Remove Project.

    Visual Basic removes the selected project from the project group.

In the Professional and Enterprise editions of Visual Basic, you can change the type of a project by selecting the Project Properties command on the Project menu, then changing the Project Type option on the General tab in the Project Properties dialog box.

Figure 8.2 The General tab in the Project Properties dialog box

Specifying a Startup Project

Because a project group contains multiple projects, Visual Basic needs to know which project to run when you choose Start from the Run menu, click the Start button on the toolbar, or press F5. By default, Visual Basic runs the first executable (.exe) project that is added to a project group. However, you can specify a different startup component.

To specify a startup component

  1. In the Project window, select a project.

  2. Click the right mouse button and select Set as Start Up from the context menu.

Visual Basic displays the startup project's name in bold type in the Project window.

For More Information Debugging multiple projects is discussed in "Debugging, Testing, and Deploying Components," in "Creating ActiveX Components" in the Component Tools Guide included with the Professional and Enterprise editions of Visual Basic.


Managing Application Settings

In Microsoft Windows 3.1 and earlier versions of Windows, program settings like window positions, files used, and other items were commonly stored in .ini files. In Windows NT, Windows 95, and later versions of Windows these program settings are stored in the system registry.

Visual Basic provides a standard registry location for storing program information for applications created in Visual Basic:

HKEY_CURRENT_USER\Software\VB and VBA Program Settings\appname\section\key

Visual Basic also provides four statements and functions to manipulate the settings stored in your application's registry location.

Function or Statement | Description
GetSetting function | Retrieves registry settings.
SaveSetting statement | Saves or creates registry settings.
GetAllSettings function | Returns an array containing multiple registry settings.
DeleteSetting statement | Deletes registry settings.

Note To view registry entries, use the Regedit application, included with Windows 95 and Windows NT.

To read more about using application settings, see the following topics:

  • Creating or Saving Application Settings Explains how to save new values for registry keys stored in your application's registry location.

  • Retrieving Application Settings Explains how to retrieve registry values stored in your application's registry location.

  • Deleting Application Settings Explains how to delete registry keys, sections, or an application's registry location.


Creating or Saving Application Settings

You can use the SaveSetting statement to save a new value for a registry key stored in your application's registry location. For example, you could add code to the Form_Unload event in the application's main form in order to preserve settings at shutdown, or in the Form_Unload event on an Options dialog box to update user preferences.

Use the following syntax for the SaveSetting statement:

SaveSetting appname, section, key, value

The following code saves new values for the Backup and LastEntry keys in the Startup section of the registry for an application named "RegCust." This code assumes that the variables strDate and intLastEntry contain the new values.

Private Sub Form_Unload(Cancel As Integer)
   SaveSetting "RegCust", "Startup", "Backup", strDate
   SaveSetting "RegCust", "Startup", "LastEntry", _
   intLastEntry
End Sub

If an entry for the application "RegCust" or any of these sections or keys don't exist in the Software/Microsoft section in the registry, this code will create it.

For More Information See the "SaveSetting Statement."


Retrieving Application Settings

You can use the GetSetting and GetAllSettings functions to retrieve registry values stored in your application's registry location. For example, your application can retrieve registry settings to recreate its condition at the time it was closed.

One Setting at a Time

To retrieve a single registry setting, use the following syntax for the GetSetting function:

GetSetting(appname, section, key[, default])

The following code retrieves the value of the LastEntry key in the "RegCust" application's Startup section, and displays the value in the Immediate window.

Private Sub Form_Load()
   Dim intLastEntry As Integer
   intLastEntry = GetSetting("RegCust", "Startup", _
   "LastEntry", "0")
   Debug.Print intLastEntry
End Sub

Note that you can use the optional parameter, default, to set the value returned by Visual Basic when there is no value listed in the registry for the specified key.

Multiple Settings

To retrieve a list of registry keys and their values, use the following syntax for the GetAllSettings function:

GetAllSettings(appname, section)

The following code retrieves a two-column list of registry keys and their values in the "RegCust" application's Startup section, and displays the results in the Immediate window.

Private Sub Form_Load()
   Dim avntSettings As Variant
   Dim intX As Integer
   avntSettings = GetAllSettings("RegCust", "Startup")
   For intX = 0 To UBound(avntSettings, 1)
      Debug.Print avntSettings(intX, 0), _
      avntSettings(intX, 1)
   Next intX
End Sub

For More Information See "GetSetting Function" and "GetAllSettings Function."


Deleting Application Settings

You can use the DeleteSetting statement to delete a registry key, section, or an application's registry location. For example, you may want to delete all registry information for an application when the application is uninstalled.

Use the following syntax for the DeleteSetting statement:

DeleteSetting(appname, section, key)

The following code deletes the LastEntry key in the "RegCust" application's Startup section.

Private Sub cmdDelKey_Click()
   DeleteSetting "RegCust", "StartUp", "LastEntry"
End Sub

The following code deletes the "RegCust" application's entire Startup section of the registry.

Private Sub cmdDelSection_Click()
   DeleteSetting "RegCust", "StartUp"
End Sub

The following code deletes the entire registry location for the "RegCust" application.

Private Sub cmdUnInstall_Click()
   DeleteSetting "RegCust"
End Sub

For More Information See "DeleteSetting Statement."


Using Conditional Compilation

Conditional compilation lets you selectively compile certain parts of the program. You can include specific features of your program in different versions, such as designing an application to run on different platforms, or changing the date and currency display filters for an application distributed in several different languages.

Structuring Code for Conditional Compiling

To conditionally compile a part of your code, enclose it between #If...Then and #EndIf statements, using a Boolean constant as the branching test. To include this code segment in compiled code, set the value of the constant to –1 (True).

For example, to create French language and German language versions of the same application from the same source code, embed platform-specific code segments in #If...Then statements using the predefined constants conFrenchVersion and conGermanVersion.

#If conFrenchVersion Then
   ' <code specific to the French language version>.
#ElseIf conGermanVersion then
   ' <code specific to the German language version>.
#Else
   ' <code specific to other versions>.
#End If

If the value of the conFrenchVersion constant is set to True at compile time, the conditional code for the French language version will be compiled. If the value of the conGermanVersion constant is set to True, the compiler uses the German language version.

Declaring Conditional Compilation Constants

There are three ways to set conditional compilation constants: in the Conditional Compilation Arguments field of the Make tab on the Project Properties dialog box, on a command line, or in code.

Conditional compilation constants have a special scope and cannot be accessed from standard code. How you set a conditional compilation constant may depend on the scope you want the constant to have.

How Set | Scope
Project Properties dialog box | Public to all modules in the project
Command line | Public to all modules in the project
#Const statement in code | Private to the module in which they are declared

Setting Constants on the Project Properties Dialog Box

Before creating the executable file, from the Project menu, choose Project Properties, click the Make tab on the Project Properties dialog box, and enter an argument, such as conFrenchVersion = –1, in the Conditional Compilation Arguments field (if you are compiling your application for the French language version). When you compile the program, this argument will satisfy the #If...Then condition, and the code between the #If...Then and #EndIf statements will be included in the compiled program.

If you have a complex #If...Then statement, containing one or more #ElseIf statements, you will need to set additional constants. You can set multiple constants by separating them with colons, as in the following example:

conFrenchVersion=-1:conANSI=0

Setting Constants on the Command Line

If you want to start compilation from a command line, use the /d switch to enter conditional compilation constants, as shown here:

vb6.exe /make MyProj.vbp /d conFrenchVersion=–1:conANSI=0

No space is required between the /d switch and the first constant. Command-line declarations override declarations entered on the Project Properties dialog box, but do not erase them; arguments set on the Project Properties dialog box remain in effect for subsequent compilations.

For More Information See "#If…Then…#Else Directive" and "#Const Statement."


Working with Resource Files

A resource file allows you to collect all of the version-specific text and bitmaps for an application in one place. This can include icons, screen text, and other material that may change between localized versions or between revisions or specific configurations.

Adding Resources to a Project

You can create a resource file using the Resource Editor add-in. The compiled resource file will have a .res file name extension. Each project can contain only one resource file.

The actual file consists of a series of individual strings, bitmaps, or other items, each of which has a unique identifier. The identifier is either a Long or a String, depending on the type of data represented by the resource. Strings, for example, have a Long identifier, while bitmaps have a Long or String identifier. To retrieve resources in your code, learn the identifier for each resource. The function parameters referring to the resources can use the Variant data type.

To add a new resource file to your project

  1. Choose Resource Editor from the Tools menu. An empty resource file will be opened in the Resource Editor window.

    Note The Resource Editor add-in must be installed. For information on installing add-ins, see "Using Wizards and Add-Ins" in "Managing Projects".

  2. Select the Save button on the Resource Editor toolbar to save the resource file. The file will be added to the Project Explorer under the Related Documents section.

To add an existing resource file to your project

  • Choose Add New Resource File from the Project menu. Any existing resource file in your project will be replaced.

    Caution If you make any modifications to an existing resource file it could affect other projects that use that resource file. Make sure that you save the file under a new filename.

    Note The Resource Editor add-in must be installed. For information on installing add-ins, see "Using Wizards and Add-Ins" in "Managing Projects."

For More Information For more information on resource files, see "Using Resource Files for Localization" in "International Issues."

Note Windows resource files are specific to 16-bit or 32-bit applications. Visual Basic will generate an error message if you try to add a 16-bit resource file to a project.

Using Resources in Code

Visual Basic provides three functions for retrieving data from the resource file for use in code.

Function | Description
LoadResString | Returns a text string.
LoadResPicture | Returns a Picture object, such as a bitmap, icon, or cursor.
LoadResData | Returns a Byte array. This is used for .wav files, for example.

For More Information See the appropriate function topic.


Working with Templates

Visual Basic provides a variety of templates for creating common application components. Rather than creating all the pieces of your application from scratch, you can customize an existing template. You can also reuse custom components in multiple applications by creating your own templates.

You can open an existing template by selecting its icon in the Add Object dialog box when you create a new form, module, control, property page, or document. For example, Visual Basic provides built-in form templates for creating an About dialog box, Options dialog box, or splash screen.

Figure 8.3 The Add Form dialog box

When you open a template, Visual Basic displays the object with placeholders that you can customize. For example, to create an About dialog box, open the About Dialog template and replace the Application Title, Version, and App Description placeholders with information specific to your application.

Figure 8.4 The About Dialog form template

To create your own template, save the object that you want to use as a template, then copy it to the appropriate subdirectory of the Visual Basic Template directory. For example, to create a custom MyForm form template, save a form named MyForm, then copy the MyForm.frm file to the \VB\Template\Forms directory. When you select the Add Form command from the Project menu, Visual Basic displays the MyForm template in the Add Form dialog box, as shown in Figure 8.3.

You can disable display of templates in the Add object dialog box by selecting the Options command on the Tools menu and clearing the Show Templates options on the Environment tab of the Options dialog box. For example, to disable the display of form templates, clear the Forms option in the dialog box.

Figure 8.5 The Environment tab of the Options dialog box


Working with Command Line Switches

Command line switches provide a way to control how Visual Basic executes. Using command line switches, you can start an instance of Visual Basic and run a specified project, make an executable file or dynamic-link library, or specify a string to be passed to the Command$ function.

For example, to run the project MyProj.vbp and then automatically exit, start Visual Basic with the following command line:

c:\Program Files\Microsoft Visual Studio\VB\vb6.exe /runexit MyProj.vbp

The following table summarizes the Visual Basic command line switches.

Switch | Description
/cmd cmdstring | Specifies a command string to be passed to the Command$ function. When used, it must be the last switch on the command line.
/d compileconst | Specifies one or more conditional compilation constants to use with the /make or /makedll switch.
/make projectname | Makes the specified project into an executable file.
/makedll projectname | Makes the specified project into a dynamic-link library.
/mdi | Starts Visual Basic using the multiple document interface (MDI) programming environment.
/out filename | Outputs errors to a file when used with the /make or /makedll switch.
/run projectname | Runs the specified project.
/runexit projectname | Runs the specified project and then automatically exits.
/sdi | Starts Visual Basic using the single document interface (SDI) programming environment.
/? | Displays a list of valid command line switches.

For More Information See "Command Line Arguments."


Compiling Your Project to Native Code

If you have the Professional or Enterprise edition of Visual Basic, you can compile your code either in standard Visual Basic p-code format or in native code format. Native code compilation provides several options for optimizing and debugging that aren't available with p-code.

P-code, or pseudo code, is an intermediate step between the high-level instructions in your Basic program and the low-level native code your computer's processor executes. At run time, Visual Basic translates each p-code statement to native code. By compiling directly to native code format, you eliminate the intermediate p-code step.

You can debug compiled native code using standard native code debugging tools, such as the debugging environment provided by Visual C++. You can also use options available in languages such as Visual C++ for optimizing and debugging native code. For example, you can optimize code for speed or for size.

Note All projects created with Visual Basic use the services of the run-time DLL (MSVBVM60.DLL). Among the services provided by this DLL are startup and shutdown code for your application, functionality for forms and intrinsic controls, and run-time functions like Format and CLng.

Compiling a project with the Native Code option means that the code you write will be fully compiled to the native instructions of the processor chip, instead of being compiled to p-code. This will greatly speed up loops and mathematical calculations, and may somewhat speed up calls to the services provided by MSVBVM60.DLL. However, it does not eliminate the need for the DLL.

To compile a project to native code

  1. In the Project window, select the project you want to compile.

  2. From the Project menu, choose Project Properties.

  3. In the Project Properties dialog box, click the Compile tab.

    Figure 8.6 The Compile tab in the Project Properties dialog box

  4. Select Compile to Native Code.

    Visual Basic enables several options for customizing and optimizing the executable file. For example, to create compiled code that will be optimized for size, select the Optimize for Small Code option.

    For additional advanced optimization options, click the Advanced Optimizations button.

  5. Select the options you want, then click OK.

  6. From the File menu, choose Make Exe, or Make Project Group.

The following table describes the native code options for optimization.

Option | Description
Assume No Aliasing (Advanced Optimization) | Tells the compiler that your program does not use aliasing. Checking this option allows the compiler to apply optimization such as storing variables in registers and performing loop optimizations.
Create Symbolic Debug Info | Produces a .pdb file and .exe or .dll file containing information to allow for debugging using Microsoft Visual C++ 5.0 or another compatible debugger.
Favor Pentium Pro(tm) | Optimizes code to favor the Pentium Pro(tm) processor.
No Optimization | Disables all optimizations.
Optimize for Fast Code | Maximizes the speed of .exe and .dll files by telling the compiler to favor speed over size.
Optimize for Small Code | Minimizes the size of .exe and .dll files by telling the compiler to favor size over speed.
Remove Array Bounds Checks (Advanced Optimization) | Disables Visual Basic array bounds checking.
Remove Floating Point Error Checks (Advanced Optimization) | Disables Visual Basic floating-point error checking.
Remove Integer Overflow Checks (Advanced Optimization) | Disables Visual Basic integer overflow checking.
Remove Safe Pentium(tm) FDIV Checks (Advanced Optimization) | Disables checking for safe Pentium(tm) processor floating-point division.

For More Information For more about native code options, see "Native Code Compiler Switches."


Creating Your Own Data Types

You can combine variables of several different types to create user-defined types (known as structs in the C programming language). User-defined types are useful when you want to create a single variable that records several related pieces of information.

You create a user-defined type with the Type statement, which must be placed in the Declarations section of a module. User-defined types can be declared as Private or Public with the appropriate keyword. For example:

Private Type MyDataType

-or-

Public Type MyDataType

For example, you could create a user-defined type that records information about a computer system:

' Declarations (of a standard module).
Private Type SystemInfo
   CPU As Variant
   Memory As Long
   VideoColors As Integer
   Cost As Currency
   PurchaseDate As Variant
End Type

Declaring Variables of a User-Defined Type

You can declare local, private module-level, or public module-level variables of the same user-defined type:

Dim MySystem As SystemInfo, YourSystem As SystemInfo

The following table illustrates where, and with what scope, you can declare user-defined types and their variables.

Procedure/Module | You can create a user-defined type as... | Variables of a user-defined type can be declared...
Procedures | Not applicable | Local only
Standard modules | Private or public | Private or public
Form modules | Private only | Private only
Class modules | Private or public | Private or public

Note If declared using the Dim keyword, user-defined types in Standard or Class modules will default to Public. If you intend a user-defined type to be private, make sure you declare it using the Private keyword.

Assigning and Retrieving Values

Assigning and retrieving values from the elements of this variable is similar to setting and getting properties:

MySystem.CPU = "486"
If MySystem.PurchaseDate > #1/1/92# Then

You can also assign one variable to another if they are both of the same user-defined type. This assigns all the elements of one variable to the same elements in the other variable.

YourSystem = MySystem

User-Defined Types that Contain Arrays

A user-defined type can contain an ordinary (fixed-size) array. For example:

Type SystemInfo
   CPU As Variant
   Memory As Long
   DiskDrives(25) As String   ' Fixed-size array.
   VideoColors As Integer
   Cost As Currency
   PurchaseDate As Variant
End Type

It can also contain a dynamic array.

Type SystemInfo
   CPU As Variant
   Memory As Long
   DiskDrives() As String      ' Dynamic array.
   VideoColors As Integer
   Cost As Currency
   PurchaseDate As Variant
End Type

You can access the values in an array within a user-defined type in the same way that you access the property of an object.

Dim MySystem As SystemInfo
ReDim MySystem.DiskDrives(3)
MySystem.DiskDrives(0) = "1.44 MB"

You can also declare an array of user-defined types:

Dim AllSystems(100) As SystemInfo

Follow the same rules to access the components of this data structure.

AllSystems(5).CPU = "386SX"
AllSystems(5).DiskDrives(2) = "100M SCSI"

Passing User-Defined Types to Procedures

You can pass procedure arguments using a user-defined type.

Sub FillSystem (SomeSystem As SystemInfo)
   SomeSystem.CPU = lstCPU.Text
   SomeSystem.Memory = txtMemory.Text
   SomeSystem.Cost = txtCost.Text
   SomeSystem.PurchaseDate = Now
End Sub

Note If you want to pass a user-defined type in a form module, the procedure must be private.

You can return user-defined types from functions, and you can pass a user-defined type variable to a procedure as one of the arguments. User-defined types are always passed by reference, so the procedure can modify the argument and return it to the calling procedure, as illustrated in the previous example.

Note Because user-defined types are always passed by reference, all of the data contained in the user-defined type will be passed to and returned from the procedure. For user-defined types that contain large arrays, this could result in poor performance, especially in client/server applications where a procedure may be running on a remote machine. In such a situation, it is better to extract and pass only the necessary data from the user-defined type.

For More Information To read more about passing by reference, see "Passing Arguments to Procedures" in "Programming Fundamentals."

User-Defined Types that Contain Objects

User-defined types can also contain objects.

Private Type AccountPack
   frmInput as Form
   dbPayRollAccount as Database
End Type

Tip Because the Variant data type can store many different types of data, a Variant array can be used in many situations where you might expect to use a user-defined type. A Variant array is actually more flexible than a user-defined type, because you can change the type of data you store in each element at any time, and you can make the array dynamic so that you can change its size as necessary. However, a Variant array always uses more memory than an equivalent user-defined type.

Nesting Data Structures

Nesting data structures can get as complex as you like. In fact, user-defined types can contain other user-defined types, as shown in the following example. To make your code more readable and easier to debug, try to keep all the code that defines user-defined data types in one module.

Type DriveInfo
   Type As String
   Size As Long
End Type

Type SystemInfo
   CPU As Variant
   Memory As Long
   DiskDrives(26) As DriveInfo
   Cost As Currency
   PurchaseDate As Variant
End Type

Dim AllSystems(100) As SystemInfo
AllSystems(1).DiskDrives(0).Type = "Floppy"


Using Enumerations to Work with Sets of Constants

Enumerations provide a convenient way to work with sets of related constants and to associate constant values with names. For example, you can declare an enumeration for a set of integer constants associated with the days of the week, then use the names of the days in code rather than their integer values.

You create an enumeration by declaring an enumeration type with the Enum statement in the Declarations section of a standard module or a public class module. Enumeration types can be declared as Private or Public with the appropriate keyword. For example:

Private Enum MyEnum

-or-

Public Enum MyEnum

By default, the first constant in an enumeration is initialized to the value 0, and subsequent constants are initialized to the value of one more that the previous constant. For example the following enumeration, Days, contains a constant named Sunday with the value 0, a constant named Monday with the value 1, a constant named Tuesday with the value of 2, and so on.

Public Enum Days
   Sunday
   Monday
   Tuesday
   Wednesday
   Thursday
   Friday
   Saturday
End Enum

Tip Visual Basic provides a built-in enumeration, vbDayOfWeek, containing constants for the days of the week. To view the enumeration's predefined constants, type vbDayOfWeek in the code window, followed by a period. Visual Basic automatically displays a list of the enumeration's constants.

You can explicitly assign values to constants in an enumeration by using an assignment statement. You can assign any long integer value, including negative numbers. For example you may want constants with values less than 0 to represent error conditions.

In the following enumeration, the constant Invalid is explicitly assigned the value –1, and the constant Sunday is assigned the value 0. Because it is the first constant in the enumeration, Saturday is also initialized to the value 0. Monday's value is 1 (one more than the value of Sunday), Tuesday's value is 2, and so on.

Public Enum WorkDays
   Saturday
   Sunday = 0
   Monday
   Tuesday
   Wednesday
   Thursday
   Friday
   Invalid = -1
End Enum

Note Visual Basic treats constant values in an enumeration as long integers. If you assign a floating-point value to a constant in an enumeration, Visual Basic rounds the value to the nearest long integer.

By organizing sets of related constants in enumerations, you can use the same constant names in different contexts. For example, you can use the same names for the weekday constants in the Days and WorkDays enumerations.

To avoid ambiguous references when you refer to an individual constant, qualify the constant name with its enumeration. The following code refers to the Saturday constants in the Days and WorkDays enumerations, displaying their different values in the Immediate window.

Debug.Print "Days.Saturday = " & Days.Saturday
Debug.Print "WorkDays.Saturday = " & WorkDays.Saturday

You can also use the value of a constant in one enumeration when you assign the value of a constant in a second enumeration. For example, the following declaration for the WorkDays enumeration is equivalent to the previous declaration.

Public Enum WorkDays
   Sunday = 0
   Monday
   Tuesday
   Wednesday
   Thursday
   Friday
   Saturday = Days.Saturday - 6
   Invalid = -1
End Enum

After you declare an enumeration type, you can declare a variable of that type, then use the variable to store the values of enumeration's constants. The following code uses a variable of the WorkDays type to store integer values associated with the constants in the WorkDays enumeration.

Dim MyDay As WorkDays
MyDay = Saturday         ' Saturday evaluates to 0.
If MyDay < Monday Then   ' Monday evaluates to 1,
                        ' so Visual Basic displays
                        ' a message box.
   MsgBox "It's the weekend. Invalid work day!"
End If

Note that when you type the second line of code in the example in the code window, Visual Basic automatically displays the WorkDays enumeration's constants in the Auto List Members list.

Figure 8.7 Visual Basic automatically displays an enumeration's constants

Because the constant Sunday also evaluates to 0, Visual Basic also displays the message box if you replace "Saturday" with "Sunday" in the second line of the example:

MyDay = Sunday         ' Sunday also evaluates to 0.

Note Although you normally assign only enumeration constant values to a variable declared as an enumeration type, you can assign any long integer value to the variable. Visual Basic will not generate an error if you assign a value to the variable that isn't associated with one of the enumeration's constants.

For More Information See "Enum Statement." Also see "Providing Named Constants for Your Component" in "Creating ActiveX Components" in the Component Tools Guide, available in the Professional and Enterprise editions.


Advanced Features of Arrays

Although arrays are most commonly used to store groups of variables, there are several other ways in which arrays are useful. You can assign the contents of one array to another, create functions that return arrays, and create properties that return arrays. In many cases these techniques can improve the performance of your application.

Assigning Arrays

Just as you can assign the contents of one variable to another, for example strA = strB, you can also assign the contents of one array to another. Imagine, for instance, that you wanted to copy an array of bytes from one location to another. You could do it by copying one byte at a time, like this:

Sub ByteCopy(oldCopy() As Byte, newCopy() As Byte)
   Dim i As Integer
   ReDim newCopy (Lbound(oldCopy) To UBound(oldCopy)

   For i – Lbound(oldCopy) To Ubound(oldCopy)
      newCopy(i) = oldCopy(i)
   Next
End Sub

A much more efficient way to do this is to assign one array to another:

Sub ByteCopy(oldCopy() As Byte, newCopy() As Byte)
   newCopy = oldCopy
End Sub

With variable assignment there are certain rules that you need to keep in mind. For example, although you can assign a variable declared as Integer to a variable declared as Long without any problem, assigning a Long to an Integer could easily lead to an overflow error. In addition to data typing rules, array assignments have additional rules involving the number of dimensions, the size of those dimensions, and whether an array is fixed or dynamic.

Attempting to assign arrays with different dimensions and/or data types may or may not succeed, depending on several factors:

  • The type of array used on the left-hand side of the assignment: a fixed array (Dim x(1 to 10) As Integer) or a dynamic array (Dim x() As Integer).

  • Whether or not the number of dimensions on the left-hand side match the number of dimensions of the array on the right-hand side of the assignment.

  • Whether or not the number of elements for each dimension on each side of the assignment match. The dimensions may match even if the declarations are different, such as when one array is zero-based and another is one-based, as long as they have the same number of elements.

  • The data types of all elements for each side of the assignment must be compatible. The rules are the same as for variable assignments.

The following table shows the effects of these factors:

Left-hand Side | Number of Dimensions Match? | Number of Elements Match? | Result of Assignment
Dynamic | No | Yes or No | Succeeds. Left-hand side ReDim’s to match right-hand side if necessary.
Dynamic | Yes | No | Succeeds. Left-hand side ReDim’s to match right-hand side if necessary.
Dynamic | Yes | Yes | Succeeds.
Fixed | Yes or No | Yes or No | Fails with a compilation error.

Errors can occur both at compile time and at run time (for example, if data types can’t be coerced or if an assignment attempts to ReDim a fixed size array.) As the programmer, it’s up to you to add error handling to make sure that the arrays are compatible before attempting an assignment.

Returning an Array from a Function

It’s possible for a function to return an array of values. For example, you might want to return an array of bytes from a function without having to perform conversions to and from a string.

Here’s a simple example of a function that returns an array of bytes:

Private Sub Form_Load()
   Dim b As Byte
   Dim I As Integer
   Dim ReturnArray() As Byte

   i  = Cbyte(54)
   ReturnArray() = ArrayFunction(b)
   For i = 0 To Ubound(ReturnArray)
      Debug.Print ReturnArray(i)
   Next
End Sub

Public Function ArrayFunction(b As Byte) As Byte()
   Dim x(2) As Byte

   x(0) = b
   x(1) = b + CByte(200)
   x(2) = b + b

   ArrayFunction = x
End Function

After running the above example, ReturnArray() would be a three-element array containing the values assigned to the array in the ArrayFunction. Note that the array must be of the same data type as the function (in this case, Byte). Because this is a function call, you can pass the array without the parentheses.

Note Although it’s possible to return the array by assigning another array (ArrayFunction = x()), this isn’t recommended for performance reasons.

You must specify a type for a function that returns an array; that type may be a Variant. Thus Function X() As Variant() would work whereas Function X() As () would fail.

When calling a function that returns an array, the variable to hold the return values must be an array and must be of the same data type as the function, otherwise it will display a "Type Mismatch" error.

For More Information To learn more about using arrays, see "Arrays" in "Programming Fundamentals." For information about returning arrays from properties, see "Putting Property Procedures to Work for You" in "Programming with Objects."


Using Collections as an Alternative to Arrays

Although collections are most often used for working with objects, you can use a collection to work with any data type. In some circumstances, it may be more efficient to store items in a collection rather than an array.

You may want to use a collection if you're working with a small, dynamic set of items. The following code fragment shows how you might use a collection to save and display a list of URL addresses.

' Module-level collection.
Public colURLHistory As New Collection

' Code for adding a specified URL address
' to the collection.
Private Sub SaveURLHistory(URLAddress As String)
   colURLHistory.Add URLAddress
End Sub

' Code for displaying the list of URL addresses
' in the Immediate window.
Private Sub PrintURLHistory()
   Dim URLAddress As Variant
   For Each URLAddress in colURLHistory
      Debug.Print URLAddress
   Next URLAddress
End Sub

For More Information For more information on using collections, see "Programming With Your Own Objects" in "Programming with Objects." To learn more about using arrays, see "Arrays" in "Programming Fundamentals."


Programming with Objects

Objects are central to Visual Basic programming. Forms and controls are objects. Databases are objects. There are objects everywhere you look.

If you've used Visual Basic for a while, or if you've worked through the examples in the first five chapters of this book, then you've already programmed with objects — but there's a lot more to objects than what you've seen so far.

In this chapter, user-defined types will take on personalities of their own, and become classes. You'll see how easy it is to create your own objects from the classes you define, and to use objects to simplify your coding and increase code reuse.

Contents

The following topics introduce the possibilities opened by programming with objects.

  • What You Need to Know About Objects in Visual Basic

  • Finding Out About Objects

  • Creating Your Own Classes

  • Adding Properties and Methods to a Class

  • Adding Events to a Class

  • Naming Properties, Methods, and Events

  • Creating Data-Aware Classes

  • Polymorphism

  • Programming with Your Own Objects

  • Object Models

  • Creating Your Own Collection Classes

  • ActiveX Designers

And it doesn't stop here. Chapter 10, "Programming with Components," takes the next step, showing how you can use Visual Basic to control objects provided by other applications.

Sample Applications: ProgWOb.vbp, Dataware.vbp

Some of the code examples in this chapter are taken from the Programming with Objects (ProgWOb.vbp) and Data-aware Classes (Dataware.vbp) samples. The sample applications are listed in the Samples directory.

Topics

What You Need to Know About Objects in Visual Basic

Reviews the correct terms to use for creating, working with, and talking about objects. Introduces the Collection object, the With statement, and other techniques for working with objects.
Finding Out About Objects

Explains how to use the Object Browser to discover an object's properties, methods, and events; to find things in projects and libraries; and to add descriptions of your own objects.
Creating Your Own Classes

Creating classes is easy — you may be surprised to learn that you've already done it. A step by step procedure introduces object creation and lifetime issues.
Adding Properties and Methods to a Class

Explains how to use property procedures to create properties that can validate the values that are assigned to them, and shows how to create many different kinds of properties.
Adding Events to a Class

Your objects can raise events that you handle in forms or in other objects.
Naming Properties, Methods, and Events

Following a few simple rules can make your objects much easier to use, reuse, and share.
Creating Data-Aware Classes

It’s easy to bind your object to a database or other source of data. Classes can act as either a source or a consumer of data.
Polymorphism

It's useful to be able to treat both a Tyrannosaur and a Flea as Animals. Polymorphism lets you do this through multiple interfaces — which also provide new ways to let your programs evolve.
Programming with Your Own Objects

Once you've created objects, how do you incorporate them into programs, keep track of them, and clean up after them?
Object Models

How do the objects you create from your classes relate to each other? Object models express the way objects contain other objects; this can be a powerful organizing principle for your programs.
Creating Your Own Collection Classes

Exposes some limitations with the Visual Basic Collection object, and shows how you can create your own collection classes.
ActiveX Designers

Designers are what put the "visual" in Visual Basic. You've used Form designers, but there's a whole new world of possibilities being opened up by ActiveX Designers.

And it doesn't stop here. "Programming with Components" takes the next step, showing how you can use Visual Basic to control objects provided by other applications.

Sample application

ProgWOb.vbp, Dataware.vbp

Some of the code examples in this chapter are taken from the Programming with Objects (ProgWOb.vbp) and Data-aware Classes (Dataware.vbp) samples. You'll find these applications listed in the Samples directory. .


What You Need to Know About Objects in Visual Basic

Visual Basic makes using objects easy, but more importantly it makes possible a gradual transition between procedural coding and programming with objects.

Of course, it helps that you've been using objects for as long as you've been using Visual Basic. The following topics will extend your understanding of Visual Basic objects and introduce some new capabilities you can take advantage of.

  • The One-Minute Terminologist Visual Basic object terminology in a nutshell. A road map to keep handy as you explore objects.

  • Discovering the Class an Object Belongs To You can use the TypeOf keyword or the TypeName function to find out the class of an object.

  • Calling a Property or Method Using a String Name The CallByName function allows you to call the properties and methods of an object at run time.

  • Performing Multiple Actions on an Object Save typing by using the With statement to set multiple properties of an object.

  • Using Default Properties Default properties allow you to access the most important property of an object without coding the property name, but you have to be careful when using Variants.

  • Creating Arrays of Objects You can store object references in arrays, just as you can store other data types.

  • Creating Collections of Objects Collections often provide a more convenient way to keep a changing number of object references.

  • The Visual Basic Collection Object Explains the features of the generic Collection object and its properties and methods.

  • Collections in Visual Basic Visual Basic provides many built-in collections, in addition to the Collection object. They all have their idiosyncrasies.


The One-Minute Terminologist

The following is a whirlwind tour of terms you'll meet in discussions of Visual Basic objects and their capabilities. If you're coming to Visual Basic from another programming language, or from having worked with ActiveX (formerly OLE) terminology, this topic will help you make the transition.

If you're new to objects, you may find it all a little bewildering. That's okay — by taking a quick tour of the terms you're going to meet, you'll start forming a picture of how they fit together. As you discover more about objects in the rest of this chapter, you can return to this topic to integrate each piece of information into the whole.

Here Goes

Objects are encapsulated — that is, they contain both their code and their data, making them more easier to maintain than traditional ways of writing code.

Visual Basic objects have properties, methods, and events. Properties are data that describe an object. Methods are things you can tell the object to do. Events are things the object does; you can write code to be executed when events occur.

Objects in Visual Basic are created from classes; thus an object is said to be an instance of a class. The class defines an object's interfaces, whether the object is public, and under what circumstances it can be created. Descriptions of classes are stored in type libraries, and can be viewed with object browsers.

To use an object, you must keep a reference to it in an object variable. The type of binding determines the speed with which an object's methods are accessed using the object variable. An object variable can be late bound (slowest), or early bound. Early-bound variables can be DispID bound or vtable bound (fastest).

A set of properties and methods is called an interface. The default interface of a Visual Basic object is a dual interface which supports all three forms of binding. If an object variable is strongly typed (that is, Dim … As classname), it will use the fastest form of binding.

In addition to their default interface, Visual Basic objects can implement extra interfaces to provide polymorphism. Polymorphism lets you manipulate many different kinds of objects without worrying about what kind each one is. Multiple interfaces are a feature of the Component Object Model (COM); they allow you to evolve your programs over time, adding new functionality without breaking old code.

Visual Basic classes can also be data-aware. A class can act as a consumer of data by binding directly to an external source of data, or it can act as a source of data for other objects by providing data from an external source.

On to Symphony Hall

Whew! If all of that seemed like old hat to you, you'll cruise through the rest of this chapter. If not, don't worry — there are strategically located explanations of all these terms sprinkled through the text (and presented at a much less frenetic pace).


Discovering the Class an Object Belongs To

Generic object variables (that is, variables you declare As Object) can hold objects of many different classes. Similarly, variables declared with Visual Basic's built-in Form and Control types can contain forms and controls of different classes.

When using variables of these types, you may need to take different actions based on the class of an object — for example, some objects may not support a particular property or method. Visual Basic provides two ways to do this: the TypeOf keyword and the TypeName function.

The TypeOf keyword can only be used in If ... Then ... Else statements. You must include the class name directly in your code. For example, If TypeOf MyControl Is CheckBox Then.

The TypeName function is more flexible. You can use it anywhere in your code, and because it returns the class name as a string, you can compare it to the value in a string variable.


Calling a Property or Method Using a String Name

Most of the time you can discover the properties and methods of an object at design time and write code to handle them. In a few cases, however, you may not know about an object’s properties and methods in advance, or you may simply want the flexibility of allowing an end user to specify properties or execute methods at run time.

Consider, for example, a client application that evaluates expressions entered by the user by passing an operator to a server application. Now suppose that you are constantly adding new functions to the server that require new operators. Unfortunately, you would need to recompile and redistribute the client application before it would be able to use the new operators. In order to avoid this, you can use the CallByName function to pass the new operators as strings, without changing the application.

The CallByName function allows you to use a string to specify a property or method at run time. The signature for the CallByName function looks like this:

Result = CallByName(Object, ProcedureName, CallType, Arguments())

The first argument to CallByName takes the name of the object that you want to act upon. The second argument, ProcedureName, takes a string containing the name of the method or property procedure to be invoked. The CallType argument takes a constant representing the type of procedure to invoke: a method (vbMethod), a property let (vbLet), a property get (vbGet), or a property set (vbSet). The final argument is optional, it takes a variant array containing any arguments to the procedure.

Suppose you had a server application, MathServer, with a new SquareRoot function. Your application has two TextBox controls: Text1 contains the expression to be evaluated upon; Text2 is used to enter the name of the function. You could use the following code in the Click event of a command button to invoke the SquareRoot function on the expression in Text1:

Private Sub Command1_Click()
   Text1.Text = CallByName(MathServer, Text2.Text, vbMethod, Text1.Text)
End Sub

If the user enters "64 / 4" in Text1 and "SquareRoot" in Text 2, the above code would invoke the SquareRoot function (which takes a string containing the expression to be evaluated as a required argument) and return "4" in Text1 (the square root of 16, or 64 divided by 4). Of course, if the user entered an invalid string in Text2, or if the string contained the name of a property instead of a method, or if the method had an additional required argument, a run-time error would occur. As you might guess, you’ll need to add robust error handling code when you use CallByName to anticipate these or any other errors.

While the CallByName function may be useful in some situations, you need to weigh its usefulness against the performance implications — using CallByName to invoke a procedure is slightly slower than late-bound calls. If you’re invoking a function that will be called repeatedly, such as inside a loop, CallByName could have a severe effect on performance.


Performing Multiple Actions on an Object

You often need to perform several different actions on the same object. For example, you might need to set several properties for the same object. One way to do this is to use several statements.

Private Sub Form_Load()
   Command1.Caption = "OK"
   Command1.Visible = True
   Command1.Top = 200
   Command1.Left = 5000
   Command1.Enabled = True
End Sub

Notice that all these statements use the same object variable, Command1. You can make this code easier to write, easier to read, and more efficient to run by using the With...End With statement.

Private Sub Form_Load()
   With Command1
      .Caption = "OK"
      .Visible = True
      .Top = 200
      .Left = 5000
      .Enabled = True
   End With
End Sub

You can also nest With statements by placing one With...End With statement inside another With...End With statement.


Using Default Properties

Many objects have default properties. You can use default properties to simplify your code, because you don't have to refer explicitly to the property when setting its value. For an object where Value is the default property, these two statements are equivalent:

object = 20

and

object.Value = 20

To see how this works, draw a command button and a text box on a form. Add the following statement to the command button's Click event:

Text1 = "hello"

Run the application and click the command button. Because Text is the default property of the text box, the text box will display the text, "hello."

Using Default Properties with Object Variables

When a reference to an object is stored in an object variable, you can still use the default property. The following code fragment demonstrates this.

Private Sub Command1_Click()
   Dim obj As Object
   ' Place a reference to Text1 in the object
   '   variable.
   Set obj = Text1
   ' Set the value of the default property (Text).
   obj = "hello"
End Sub

In the code above, obj = "hello" is exactly the same as typing obj.Text = "hello".

Using Default Properties with Variants

Accessing default properties is different when an object reference is stored in a variable of type Variant, instead of in an object variable. This is because a Variant can contain data of many different types.

For example, you can read the default property of Text1 using a reference in a Variant, but trying to assign the string "goodbye" to the default property doesn't work. Instead, it replaces the object reference with the string, and changes the Variant type.

To see how this works, enter the following code in the Click event of the command button from the previous example:

Private Sub Command1_Click()
   Dim vnt As Variant
   ' Set the default property (Text) to "hello".
   Text1 = "hello"
   ' Place a reference to Text1 in the Variant.
   Set vnt = Text1
   ' Display the default property of Text1, and show
   '   that the Variant contains an object reference.
   MsgBox vnt, , "IsObject? " & IsObject(vnt)
   ' Attempt to set the default property of Text1.
   vnt = "goodbye"
   MsgBox vnt, , "IsObject? " & IsObject(vnt)
End Sub

When you run the application and click the command button, you first get a message box displaying the current value of the default property of Text1, "hello," which you can verify by looking at Text1. The caption of the message box confirms that the Variant contains an object reference — that is, a reference to Text1.

When you click the OK button on the message box, "goodbye" is assigned to the Variant, destroying the reference to Text1. Another message box is then displayed, showing the contents of the Variant — which as you can see doesn't match the current value of Text1.Text.

The caption of the message box confirms that the Variant no longer contains an object reference — it now contains the string "goodbye."

For More Information For details on Variants and other data types, see "Introduction to Variables, Constants, and Data Types" in "Programming Fundamentals."

Other aspects of using objects with Variants are discussed in "The Visual Basic Collection Object."


Creating Arrays of Objects

You can declare and use arrays of an object type just as you declare and use an array of any data type. These arrays can be fixed-size or dynamic.

Arrays of Form Variables

You can declare an array of forms with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. If you declare the array with the New keyword, Visual Basic automatically creates a new instance of the form for each element in the array as you use the elements in the array.

Private Sub Command1_Click ()
   Dim intX As Integer
   Dim frmNew(1 To 5) As New Form1
   For intX = 1 To 5
      frmNew(intX).Show
      frmNew(intX).WindowState = vbMinimized
      ' To create minimized forms without having them
      ' first appear briefly at normal size, reverse
      ' the order of the two lines above.
   Next
End Sub

Pressing the command button to execute the code above will create five minimized instances of Form1.

Note If you look at the Task Bar, you'll see Form1 six times. The extra instance of Form1 isn't minimized — it's the one you started with.

Arrays of Control Variables

You can declare an array of controls with Private, Dim, ReDim, Static, or Public in the same way you declare an array of any other type. Unlike form arrays, however, control arrays cannot be declared with the New keyword. For example, you can declare an array to be a specific control type:

ReDim ActiveImages(10) As Image

When you declare an array to be a particular control type, you can assign only controls of that type to the array. In the case of the preceding declaration, for example, you can only assign image controls to the array — but those image controls can come from different forms.

Contrast this with the built-in Controls collection, which can contain many different types of controls — all which must be on the same form.

Alternatively, you can declare an array of generic control variables. For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a dynamic array of control variables that contains references to each control that has been dropped:

Private Sub List1_DragDrop(Source As VB.Control, _
      X As Single, Y As Single)
   Dim intX As Integer
   Static intSize As Integer
   Static ctlDropped() As Control
   For intX = 1 To intSize
      ' If the dropped control is in the array, it's
      ' already been dropped here once.
      If ctlDropped(intX) Is Source Then
         Beep
         Exit Sub
      End If
   Next
   ' Enlarge the array.
   intSize = intSize + 1
   ReDim Preserve ctlDropped(intSize)
   ' Save a reference to the control that was dropped.
   Set ctlDropped(intSize) = Source
   ' Add the name of the control to the list box.
   List1.AddItem Source.Name
End Sub

This example uses the Is operator to compare the variables in the control array with the control argument. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.

The example also uses the Set statement to assign the object reference in the Source argument to an element in the array.

For More Information See "Is Operator" in the Language Reference.

Arrays are introduced in "Arrays" and "Dynamic Arrays" in "Programming Fundamentals."

For an easier way to keep track of objects, see "Creating Collections of Objects" later in this chapter.


Creating Collections of Objects

Collections provide a useful way to keep track of objects. Unlike arrays, Collection objects don't have to be re-dimensioned as you add and remove members.

For example, you might want to keep track of every control that was dropped onto a particular control, and not allow any control to be dropped more than once. You can do this by maintaining a Collection that contains references to each control that has been dropped:

Private Sub List1_DragDrop(Source As VB.Control, _
X As Single, Y As Single)
   Dim vnt As Variant
   Static colDroppedControls As New Collection
   For Each vnt In colDroppedControls
      ' If the dropped control is in the collection,
      ' it's already been dropped here once.
      If vnt Is Source Then
         Beep
         Exit Sub
      End If
   Next
   ' Save a reference to the control that was dropped.
   colDroppedControls.Add Source
   ' Add the name of the control to the list box.
   List1.AddItem Source.Name
End Sub

This example uses the Is operator to compare the object references in the colDroppedControls collection with the event argument containing the reference to the dropped control. The Is operator can be used to test the identity of Visual Basic object references: If you compare two different references to the same object, the Is operator returns True.

The example also uses the Add method of the Collection object to place a reference to the dropped control in the collection.

Unlike arrays, Collections are objects themselves. The variable colDroppedControls is declared As New, so that an instance of the Collection class will be created the first time the variable is referred to in code. The variable is also declared Static, so that the Collection object will not be destroyed when the event procedure ends.

For More Information See "Is Operator" in the Language Reference.

Properties and methods of the Collection object are discussed in "The Visual Basic Collection Object" later in this chapter.

To compare the code above with the code required to use arrays, see "Creating Arrays of Objects," earlier in this chapter.

To learn how to create more robust collections by wrapping the Collection object in your own collection class, see "Creating Your Own Collection Classes" later in this chapter.

"What You Need to Know About Objects in Visual Basic," earlier in this chapter, describes how objects are created and destroyed.


The Visual Basic Collection Object

A collection is a way of grouping a set of related items. Collections are used in Visual Basic to keep track of many things, such as the loaded forms in your program (the Forms collection), or all the controls on a form (the Controls collection).

Visual Basic provides the generic Collection class to give you the ability to define your own collections. You can create as many Collection objects — that is, instances of the Collection class — as you need. You can use Collection objects as the basis for your own collection classes and object models, as discussed in "Creating Your Own Collection Classes" and "Object Models" later in this chapter.

For example, collections are a good way to keep track of multiple forms. "Multiple Document Interface (MDI) Applications" in "Creating a User Interface" discusses applications in which the user can open any number of document windows. The following code fragment shows how you might use the Add method of a collection object to keep a list of MDI child windows the user has created. This code assumes that you have a form named mdiDocument, whose MDIChild property is set to True.

' Module-level collection in the parent MDIForm.
Public colDocuments As New Collection

' Code for creating a new MDI child document form.
Private Sub mnuFileNew()
   Dim f As New mdiDocument
   Static intDocumentNumber As Integer
   intDocumentNumber = intDocumentNumber + 1
   ' The following line creates the form.
   f.Caption = "Document" & intDocumentNumber
   ' Add the object reference to the collection.
   colDocuments.Add f
   f.Show
End Sub

The colDocuments collection acts like a subset of the built-in Forms collection, containing only instances of the form mdiDocument. The size of the collection is adjusted automatically as each new form is added. You can use For Each ... Next to iterate through the collection. If you want to give the form a key by which it can be retrieved, you can supply a text string as the second parameter of the Add method, as described later in this section.

The New keyword in the declaration for the variable colDocuments causes a Collection object to be created the first time the variable is referred to in code. Because Collection is a class, rather than a data type, you must create an instance of it and keep a reference to that instance (object) in a variable.

Like any other object, a Collection object will be destroyed when the last variable that contains a reference to it is set to Nothing or goes out of scope. All the object references it contains will be released. For this reason, the variable colDocuments is declared in the parent MDIForm, so that it exists throughout the life of the program.

Note If you use a collection to keep track of forms, use the collection's Remove method to delete the object reference from the collection after you unload the form. You cannot reclaim the memory the form was using as long as a reference to the form still exists, and the reference the Collection object is holding is just as good as a reference in an object variable.

What's a Collection Object Made Of?

A Collection object stores each item in a Variant. Thus the list of things you can add to a Collection object is the same as the list of things that can be stored in a Variant. This include standard data types, objects, and arrays — but not user-defined types.

Variants always take up 16 bytes, no matter what's stored in them, so using a Collection object is not as efficient as using arrays. However, you never have to ReDim a Collection object, which results in much cleaner, more maintainable code. In addition, Collection objects have extremely fast look-ups by key, which arrays do not.

Note To be precise, a Variant always takes up 16 bytes even if the data are actually stored elsewhere. For example, if you assign a string or an array to a Variant, the Variant contains a pointer to a copy of the string or array data. Only 4 bytes of the Variant is used for the pointer on 32-bit systems, and none of the data is actually inside the Variant.

If you store an object, the Variant contains the object reference, just as an object variable would. As with strings and arrays, only 4 bytes of the Variant are being used.

Numeric data types are stored inside the Variant. Regardless of the data type, the Variant still takes up 16 bytes.

Despite the size of Variants, there will be many cases where it makes sense to use a Collection object to store all of the data types listed above. Just be aware of the tradeoff you're making: Collection objects allow you to write very clean, maintainable code — at the cost of storing items in Variants.

Properties and Methods of the Collection Object

Each Collection object comes with properties and methods you can use to insert, delete, and retrieve the items in the collection.

Property or method | Description
Add method | Add items to the collection.
Count property | Return the number of items in the collection. Read-only.
Item method | Return an item, by index or by key.
Remove method | Delete an item from the collection, by index or by key.

These properties and methods provide only the most basic services for collections. For example, the Add method cannot check the type of object being added to a collection, to ensure that the collection contains only one kind of object. You can provide more robust functionality — and additional properties, methods, and events — by creating your own collection class, as described in "Creating Your Own Collection Classes" later in this chapter.

The basic services of adding, deleting, and retrieving from a collection depend on keys and indexes. A key is String value. It could be a name, a driver's license number, a social security number, or simply an Integer converted to a String. The Add method allows you to associate a key with an item, as described later in this section.

An index is a Long between one (1) and the number of items in the collection. You can control the initial value of an item's index, using the before and after named parameters, but its value may change as other items are added and deleted.

Note A collection whose index begins at 1 is called one-based, as explained in "Collections in Visual Basic."

You can use the index to iterate over the items in a collection. For example, the following code shows two ways to give all the employees in a collection of Employee objects a 10 percent raise, assuming that the variable mcolEmployees contains a reference to a Collection object.

Dim lngCt As Long
For lngCt = 1 To mcolEmployees.Count
   mcolEmployees(lngCt).Rate = _
      mcolEmployees(lngCt).Rate * 1.1
Next

Dim emp As Employee
For Each emp In mcolEmployees
   emp.Rate = emp.Rate * 1.1
Next

Tip For better performance, use For Each to iterate over the items in a Collection object. For Each is significantly faster than iterating with the index. This is not true of all collection implementations — it's dependent on the way the collection stores data internally.

Adding Items to a Collection

Use the Add method to add an item to a collection. The syntax is:

Sub Add (item As Variant [, key As Variant] [, before As Variant]

[, after As Variant] )

For example, to add a work order object to a collection of work orders using the work order's ID property as the key, you can write:

colWorkOrders.Add woNew, woNew.ID

This assumes that the ID property is a String. If the property is a number (for example, a Long), use the CStr function to convert it to the String value required for keys:

colWorkOrders.Add woNew, CStr(woNew.ID)

The Add method supports named arguments. To add an item as the third element, you can write:

colWorkOrders.Add woNew, woNew.ID, after:=2

You can use the before and after named arguments to maintain an ordered collection of objects. For example, before:=1 inserts an item at the beginning of the collection, because Collection objects are one-based.

Deleting Items from a Collection

Use the Remove method to delete an item from a collection. The syntax is:

object.Remove index

The index argument can either be the position of the item you want to delete, or the item's key. If the key of the third element in a collection is "W017493," you can use either of these two statements to delete it:

colWorkOrders.Remove 3

-or-

colWorkOrders.Remove "W017493"

Retrieving Items from a Collection

Use the Item method to retrieve specific items from a collection. The syntax is:

[Set] variable = object.Item(index)

As with the Remove method, the index can be either the position in the collection, or the item's key. Using the same example as for the Remove method, either of these statements will retrieve the third element in the collection:

Set woCurrent = colWorkOrders.Item(3)

-or-

Set woCurrent = colWorkOrders.Item("W017493")

If you use whole numbers as keys, you must use the CStr function to convert them to strings before passing them to the Item or Remove methods. A Collection object always assumes that a whole number is an index.

Tip Don't let Collection objects decide whether a value you're passing is an index or a key. If you want a value to be interpreted as a key, and the variable that contains the value is anything but String, use CStr to convert it. If you want a value to be interpreted as an index, and the variable that contains the value is not one of the integer data types, use CLng to convert it.

Item Is the Default Method

The Item method is the default method for a Collection object, so you can omit it when you access an item in a collection. Thus the previous code example could also be written:

Set woCurrent = colWorkOrders(3)

-or-

Set woCurrent = colWorkOrders("W017493")

Important Collection objects maintain their numeric index numbers automatically as you add and delete elements. The numeric index of a given element will thus change over time. Do not save a numeric index value and expect it to retrieve the same element later in your program. Use keys for this purpose.

Using the Item Method to Invoke Properties and Methods

You don't have to retrieve an object reference from a collection and place it in an object variable in order to use it. You can use the reference while it's still in the collection.

For example, suppose the WorkOrder object in the code above has a Priority property. The following statements will both set the priority of a work order:

colWorkOrders.Item("W017493").Priority = 3
colWorkOrders("W017493").Priority = 3

The reason this works is that Visual Basic evaluates the expression from left to right. When it comes to the Item method — explicit or implied — Visual Basic gets a reference to the indicated item (in this case, the WorkOrder object whose key is W017493), and uses this reference to evaluate the rest of the line.

Tip If you're going to invoke more than one property or method of an object in a collection, copy the object reference to a strongly typed object variable first. Using an object reference while it's still in a collection is slower than using it after placing it in a strongly typed object variable (for example, Dim woCurrent As WorkOrder), because the Collection object stores items in Variants. Object references in Variants are always late bound.

For More Information The Collection object is also a useful alternative to arrays for many ordinary programming tasks. See "Using Collections as an Alternative to Arrays" in "More About Programming."

Visual Basic provides a number of built-in collections. To compare them with the Collection object, see "Collections in Visual Basic."


Collections in Visual Basic

What is a collection? In "The Visual Basic Collection Object," a collection was defined as a way of grouping related objects. That leaves a lot of room for interpretation; it's more of a concept than a definition.

In fact, as you'll see when you begin comparing collections, there are a lot of differences even among the kinds of collections provided in Visual Basic. For example, the following code causes an error:

Dim col As Collection
Set col = Forms      ' Error!

What's happening here? The Forms collection is a collection; the variable col is declared As Collection; why can't you assign a reference to Forms to the variable col?

The reason for this is that the Collection class and the Forms collection are not polymorphic; that is, you can't exchange one for the other, because they were developed from separate code bases. They don't have the same methods, store object references in the same way, or use the same kinds of index values.

This makes the Collection class's name seem like an odd choice, because it really represents only one of many possible collection implementations. This topic explores some of the implementation differences you'll encounter.

Zero-Based and One-Based Collections

A collection is either zero-based or one-based, depending on what its starting index is. As you might guess, the former means that the index of the first item in the collection is zero, and the latter means it's one. Examples of zero-based collections are the Forms and Controls collections. The Collection object is an example of a one-based collection.

Older collections in Visual Basic are more likely to be zero-based, while more recent additions are more likely to be one-based. One-based collections are somewhat more intuitive to use, because the index ranges from one to Count, where Count is the property that returns the number of items in a collection.

The index of a zero-based collection, by contrast, ranges from zero to one less than the Count property.

Index and Key Values

Many collections in Visual Basic allow you to access an item using either a numeric index or a string key, as the Visual Basic Collection object does. (Visual Basic's Collection object allows you to add items without specifying a key, however.)

The Forms collection, by contrast, allows only a numeric index. This is because there's no unique string value associated with a form. For example, you can have multiple forms with the same caption, or multiple loaded forms with the same Name property.

Adding and Removing Items

Collections also differ in whether or not you can add items to them, and if so, how those items are added. You can't add a printer to the Printers collection using Visual Basic code, for example.

Because the Collection object is a general-purpose programming tool, it's more flexible than other collections. It has an Add method you can use to put items into the collection, and a Remove method for taking items out.

By contrast, the only way to get a form into the Forms collection is to load the form. If you create a form with the New operator, or by referring to a variable declared As New, it will not be added to the Forms collection until you use the Load statement to load it.

The Forms and Controls collections don't have Remove methods. You add and remove forms and controls from these collections indirectly, by using the Load and Unload statements.

What Has It Got In Its Pocketses?

As noted above, a form is not added to the Forms collection until it's loaded. Thus the most accurate specification of the Forms collection is that it contains all of the currently loaded forms in the program.

Even that's not completely accurate. If your project uses Microsoft Forms (included for compatibility with Microsoft Office), you'll find those forms in a separate collection named UserForms. So the Forms collection contains all of the currently loaded Visual Basic forms in the program.

The contents of the Collection class are very precisely specified: anything that can be stored in a Variant. Thus the Collection object can contain an object or an integer, but not a user-defined type.

Unfortunately, this specification covers a lot of territory — a given instance of the Collection class could store any mongrel assortment of data types, arrays, and objects.

Tip One of the most important reasons for creating your own collection classes, as discussed in "Creating Your Own Collection Classes," is so you can control the contents of your collections — a concept called type safety.

Enumerating a Collection

You can use For Each … Next to enumerate the items in a collection, without worrying about whether the collection is zero-based or one-based. Of course, this is hardly a defining characteristic of collections, because Visual Basic allows you to use For Each … Next to enumerate the items in an array.

What makes For Each … Next work is a tiny object called an enumerator. An enumerator keeps track of where you are in a collection, and returns the next item when it's needed.

When you enumerate an array, Visual Basic creates an array enumerator object on the fly. Collections have their own enumerator objects, which are also created as needed.

Enumerators Don't Skip Items

The enumerators of collections in Visual Basic don't skip items. For example, suppose you enumerate a collection containing "A," "B," and "C," and that while doing so you remove "B." Visual Basic collections will not skip over "C" when you do this.

Enumerators May Not Catch Added Items

If you add items to a collection while enumerating it, some enumerators will include the added items, while some will not. The Forms collection, for example, will not enumerate any forms you load while enumerating.

The Collection object will enumerate items you add while enumerating, if you allow them to be added at the end of the collection. Thus the following loop never ends (until you hit CTRL+BREAK, that is):

Dim col As New Collection
Dim vnt As Variant
col.Add "Endless"
col.Add "Endless"
For Each vnt In col
   MsgBox vnt
   col.Add "Endless"
Next

On the other hand, items you add at the beginning of the collection will not be included in the enumeration:

Dim col As New Collection
Dim vnt As Variant
col.Add "Will be enumerated"
For Each vnt In col
   MsgBox vnt
   ' Add the item at the beginning.
   col.Add "Won't be enumerated", Before:=1
Next

Why Enumerators?

By emitting a new enumerator each time a For Each … Next begins, a collection allows nested enumerations. For example, suppose you have a reference to a Collection object in the variable mcolStrings, and that the collection contains only strings. The following code prints all the combinations of two different strings:

Dim vnt1 As Variant
Dim vnt2 As Variant
For Each vnt1 In mcolStrings
   For Each vnt2 In mcolStrings
      If vnt1 <> vnt2 Then
         Debug.Print vnt1 & " " & vnt2
      End If
   Next
Next

For More Information See "Creating Your Own Collection Classes" later in this chapter.


Finding Out About Objects

The Object Browser is based on type libraries, resources that contain detailed descriptions of classes, including properties, methods, events, named constants, and more.

Visual Basic creates type library information for the classes you create, provides type libraries for the objects it includes, and lets you access the type libraries provided by other applications.

You can use the Object Browser to display the classes available in projects and libraries, including the classes you've defined. The objects you create from those classes will have the same members — properties, methods, and events — that you see in the Object Browser.

Figure 9.1 The Object Browser

To display the Object Browser

  • From the View menu, choose Object Browser.

    -or-

    Press F2.

    -or-

    Click the Object Browser button on the toolbar.

By default, the Object Browser cannot be docked to other windows. This allows you to move between the Object Browser and code windows using CTRL+TAB. You can change this by right-clicking the Object Browser to open its context menu, and clicking Dockable.

Note When the Object Browser is dockable, you cannot use CTRL+TAB to move to it from your code windows.

The following topics detail features of the Object Browser.

  • Contents of the Object Browser The object browser shows classes and their members. Description strings remind you what objects are for, and show function templates.

  • Finding and Browsing Objects You can locate objects and their members, even if you only remember part of a name.

  • Adding Descriptions for Your Objects Descriptions you add to your classes and their members may assist you when using your classes.

  • Moving Between Procedures You can locate modules and procedures in your projects, and jump to the code for them.

  • Browsing Objects from Other Applications You can use objects from other applications in your program; the Object Browser shows you the Automation objects other applications provide.


Contents of the Object Browser

The Object Browser displays information in a three-level hierarchy, as shown in Figure 9.2. Beginning from the top, you can select from available projects and libraries, including your own Visual Basic projects, using the Project/Library box.

Figure 9.2 Viewing a class's members in the Object Browser

  • Click on a class in the Classes list to view its description in the description pane at the bottom. The class's properties, methods, events, and constants will appear in the Members list on the right. The classes available are drawn from the project or library selected in the Project/Library box, or from all projects and libraries if <All Libraries> is selected.

  • You can view the arguments and return values of a member of the selected class, by clicking on the member in the Members list. The description pane at the bottom of the Object Browser shows this information.

  • You can jump to the library or object that includes a member by clicking the library or object name in the description pane. You can return by clicking the Go Back button at the top of the Object Browser.

Tip When you're in either the Classes list or the Members list, typing the first character of a name will move to the next name that begins with that character.

Controlling the Contents of the Object Browser

The context menu, shown in Figure 9.3, provides an alternative to the Copy and View Definition buttons on the Object Browser. It also allows you to open the References dialog box, and — if a class or member is selected — to view the properties of the selected item. You can set descriptions for your own objects using this menu item, as described in "Adding Descriptions for Your Objects."

Figure 9.3 The Object Browser's context menu

Right-clicking on the Object Browser brings up the context menu. In addition to the functions mentioned above, the context menu controls the contents of the Classes list and the Members list.

  • When Group Members is checked, all the properties of an object are grouped together, all the methods are grouped together, and so on. When Group Members is not checked, the Members list is alphabetical.

  • When Show Hidden Members is checked, the Class list and Members list display information marked as hidden in the type library. Normally you don't need to see this information. Hidden members are shown in light gray type.

Tip When Group Members is selected, typing the first letter of a name will jump to the next name that begins with that character, even if the name is in another group.


Finding and Browsing Objects

You can use the Object Browser to find objects and their members, and to identify the projects or libraries they come from.

Enter text in the Search Text box and then click the Search button (or press ENTER). The classes and members whose names include the text you specified will appear in the Search Results list.

For example, Figure 9.4 shows the results of typing "printer" in the Search Text box and clicking the Search button.

Figure 9.4 Using the Search button

You can select an item in the Search Results list, and view its description in the description pane at the bottom of the Object Browser. Clicking on the underlined jumps in the description pane selects the indicated library or navigates to the object or member.

You can restrict the search to items that exactly match the string in the Search box by checking Find Whole Word Only on the context menu.


Adding Descriptions for Your Objects

You can use the Object Browser to add descriptions and HelpContextIDs to your own procedures, modules, classes, properties, and methods. You may find these descriptions useful while working with your classes.

Note You can also enter descriptions for properties, methods, and events using the Procedure Attributes dialog box, accessed from the Tools menu.

To enter description strings and link your classes and their members to Help topics

  1. Press F2 to open the Object Browser. In the Project/Library box, select your project.

  2. In the Classes list, right click the name of a class to bring up the context menu, and click Properties to open the Member Options dialog box.

    Alternatively, in the Members list you can right click the name of a property, method, or event you added to the class. On the context menu, click Properties. If the member is Private or Friend, this will open the Member Options dialog box. If the member is Public — that is, part of the class's interface — it will open the Procedure Attributes dialog box instead.

    Note The difference between these two dialog boxes is that the Procedure Attributes dialog box has an Advanced button that can be used to make a member the default for the class, as described in "Making a Property or Method the Default" later in this chapter.

  3. In the Help Context ID box, type the context ID of the Help topic to be shown if you click the "?" button when this class or member is selected in the Object Browser.

    Note You can create a Help file for your own use, and link topics to your classes and their members. To specify a Help file for your project, use the General tab of the Project Properties dialog box, accessed from the Project menu.

  4. In the Description box, type a brief description of the class or member.

  5. Click OK to return to the Object Browser. The description string you entered should appear in the description pane at the bottom of the browser.

  6. Repeat steps 2 through 5 for each class and for each member of each class.

Note You cannot supply browser strings or Help topics for enumerations.

For More Information Enumerations are introduced in "Using Enumerations to Work with Sets of Constants" in "More About Programming."


Moving Between Procedures

You can use the Object Browser to move quickly to the code for a class, module, or procedure in your project.

To move to a class, module, or procedure

  1. (Optional) Select your project from the Project/Library box.

    Step 1 is optional if you have <All Libraries> selected in the Project/Library box, because all of your projects are included.

  2. Names of classes, modules, and members that belong to your projects are shown in bold type. Double-click any name shown in bold type to move to that class, module, or member. (Or right-click a name and then select View Definition from the context window.)

    The selected item is displayed in the Code window.


Browsing Objects from Other Applications

From within Visual Basic, you can access and control objects supplied by other applications. For example, if you have Microsoft Project and Microsoft Excel on your system, you could use a Graph object from Microsoft Excel and a Calendar object from Microsoft Project as part of your application.

You can use the Object Browser to explore the type libraries of other applications. An type library provides information about the objects provided by other applications.

Note In the Project/Library list, there are separate entries for Visual Basic (VB) and Visual Basic for Applications (VBA). Although we speak of "objects provided by Visual Basic," you'll notice that the Collection object is provided by VBA.

You can add libraries to your project by selecting References from the Object Browser's context menu, to open the References dialog box.

For More Information For more details on using Automation to combine and manipulate objects from other applications, see "Programming with Components."


Creating Your Own Classes

If you're an experienced programmer, you already have a library of useful functions you've written over the years. Objects don't replace functions — you'll still write and use utility functions — but they provide a convenient, logical way to organize procedures and data.

In particular, the classes from which you create objects combine data and procedures into a unit. The following topics explain how this increases the robustness of your code, and how it leads to new ways of programming.

  • Classes: Putting User-Defined Types and Procedures Together Classes are user-defined types with an attitude. Encapsulation puts code and data in the same module, giving rise to objects that can protect and validate their own data.

  • Customizing Form Classes You've been creating classes for years — every time you designed a form. This has interesting consequences for form design.

  • Class Module Step by Step A short introduction to class modules, including class creation, creating objects from classes, and the rules of object lifetime.

  • Debugging Class Modules Describes the Break in Class Module error trapping option, plus the ALT+F8 and ALT+F5 keystrokes for stepping or running past errors.

  • Life Cycle of Visual Basic Forms The lifetimes of forms, and the controls they contain, follow slightly different rules than those that govern other objects.

  • Class Modules vs. Standard Modules There are significant differences between class modules and standard modules. Understanding these will help you write better code for your objects.


Classes: Putting User-Defined Types and Procedures Together

User-defined types are a powerful tool for grouping related items of data. Consider, for example, the user-defined type named udtAccount defined here:

Public Type udtAccount
   Number As Long
   Type As Byte
   CustomerName As String
   Balance As Double
End Type

You can declare a variable of type udtAccount, set the values of its fields individually, and then pass the whole record to procedures that print it, save it to a database, perform computations on it, validate its fields, and so on.

Powerful as they are, user-defined types present the programmer with some problems. You may create a Withdrawal procedure that raises an error if a withdrawal exceeds the balance in the account, but there's nothing to prevent the Balance field from being reduced by other code in your program.

In other words, the connection between procedures and user-defined types depends on the discipline, memory, and knowledge of the programmer maintaining the code.

Objects: User-Defined Types with an Attitude

Object-oriented programming solves this problem by combining data and procedures in a single entity, as shown in Figure 9.5.

Figure 9.5 Objects combine data and procedures

When the user-defined type udtAccount becomes the Account class, its data become private, and the procedures that access them move inside the class and become properties and methods. This is what's meant by the term encapsulation — that is, an object is a unit (a capsule, if you will) containing both code and data.

When you create an Account object from the class, the only way you can access its data is through the properties and methods that make up its interface. The following code fragment shows how the procedures inside the Account class support encapsulation:

' The account balance is hidden from outside code.
Private mdblBalance As Double

' The read-only Balance property allows outside code
' to find out the account balance.
Public Property Get Balance() As Double
   Balance = mdblBalance
End Property

' The Withdrawal method changes the account balance,
' but only if an overdraft error doesn't occur.
Public Sub Withdrawal(ByVal Amount As Double)
   If Amount > Balance Then
      Err.Raise Number:=vbObjectError + 2081, _
      Description:="Overdraft"
   End If
   mdblBalance = mdblBalance - Amount
End Sub

For the moment, don't worry about how you get the procedures inside the class, or about understanding the syntax of property procedures and private variables. The important thing to remember is that you can define an object that encapsulates and validates its own data.

With the Account object, you never have be concerned about whether you've called the right procedures to update the account, because the only procedures you can call are built into the object.

For More Information "Customizing Form Classes" puts property and method creation into a framework you're already familiar with. Later, "Adding Properties and Methods to a Class" will explain the syntax.

You can read about user-defined types in "Creating Your Own Data Types" in "More About Programming."

For details about Sub and Function procedures, see "Introduction to Procedures" in "Programming Fundamentals."


Customizing Form Classes

It may surprise you to learn that you've been creating classes for as long as you've been programming in Visual Basic. It's true: Form1, that familiar denizen of every project you've ever started, is really — a class.

To see this, open a new Standard Exe project. Add a button to Form1, and place the following code in its Click event:

Private Sub Command1.Click()
   Dim f As New Form1
   f.Show
End Sub

Press F5 to run the project, and click the button. Holy smokes, there's another instance of Form1! Click its button. There's another! Every instance you create looks the same, and has the same behavior, because they're all instances of the Form1 class.

What's Going On Here?

If you've read "Working with Objects" in "Programming Fundamentals," you know that an object variable declared As New contains Nothing until the first time you refer to it in code. When you use the variable for the first time, Visual Basic notices that it contains the special value Nothing, and creates an instance of the class. (And a good thing it does, too, or f.Show would cause an error.)

Me and My Hidden Global Variable

You may be wondering how it is that you can refer to Form1 in code, as if it were an object variable. There's no magic involved. Visual Basic creates a hidden global object variable for every form class. It's as if Visual Basic had added the following declaration to your project:

Public Form1 As New Form1

When you select Form1 as your startup object, or type Form1.Show in code, you're referring to this hidden global object variable. Because it's declared As New, an instance of the Form1 class is created the first time you use this predeclared variable in code.

The reason this declaration is hidden is that Visual Basic changes it every time you change the Name property of a form. In this way, the hidden variable always has the same name as the form class.

A Very Short Quiz

Which of the instances of Form1 you created in the exercise above was associated with the hidden global variable? If you guessed the first one, you're right. Form1 is the default startup object for the project, and to Visual Basic that's just like using the predeclared global variable Form1 in code.

Tip After you unload a form, you should always set any references to the form to Nothing in order to free the memory and resources the form was using. The reference most often overlooked is the hidden global form variable.

What About All Those Other Instances of Form1?

In "Programming Fundamentals," you learned that to refer to an object, you need an object variable, and that an object exists only as long as there's at least one object variable containing a reference to it. So what was keeping all those other instances alive?

The second instance of Form1, and all the ones that followed, had an object variable for just as long as it took to call their Show methods. Then that variable went out of scope, and was set to Nothing. But Visual Basic keeps a special collection named Forms, which you can read about in "More About Forms" in "Creating a User Interface." The Forms collection contains a reference to each of the loaded forms in your project, so that you can always find and control them.

Note As you'll learn, this is not true of all classes. For example, the classes you design won't have hidden global variables or global collections to keep track of them — those are special features of form classes. However, you can declare your own global variables, and you can create your own collections — as described in "Creating Your Own Collection Classes."

Properties, Methods, and Events of Form Classes

The first time you added a property to a form class, you probably did it visually, by dropping a command button (or some other control) on Form1. In doing so, you added a read-only Command1 property to the form class. Thereafter, you invoked this property of Form1 whenever you needed to call a method or property of the command button:

Command1.Caption = "Click Me"

When you changed the Name property of any control on a form, Visual Basic quietly changed the name of the read-only property, so they always matched.

If you still have the project open from the earlier exercise, you can see this Command1 property by pressing F2 to open the Object Browser. In the Project/Library box, select Project1. You'll see Form1 in the Classes pane. In the Members pane, scroll down until you find Command1, and select it.

Command1 has a property symbol beside it, and if you look in the description pane, you'll see that it's a WithEvents property. As you'll learn in "Adding Events to a Class," this means that the property (or object variable) has event procedures associated with it. One of those event procedures, Command1_Click(), may have been the first place you ever wrote Visual Basic code.

But Wait, There's More

Dropping controls on a form is not the only way to add new members to the form class. You can add your own custom properties, methods, and events, as easily as you create new variables and procedures.

To see this, add the following code to the Declarations section of Form1:

' The Comment property of the Form1 class.
Public Comment As String

Add the following code to the Click event of Form1:

Private Sub Form_Click()
   MsgBox Comment, , "My comment is:"
End Sub

Finally, change the code in the Command1_Click() event procedure by adding a line, as follows:

Private Sub Command1.Click()
   Dim f As New Form1
   f.Comment = InputBox("What's my comment?")
   f.Show
End Sub

Press F5 to run the project. Click Command1, and when the input box appears, type in some racy comment and click OK. When the new instance of Form1 appears, click on it to play back its Comment property.

Click on the first instance of Form1, and notice that its Comment property is blank. Because Visual Basic created this instance as the Startup Object, you never got a chance to set its Comment property.

Forms Can Call Each Other's Methods

If you were watching closely, you may have noticed that the code you added to the Form1 class didn't set the object's own Comment property — it set the Comment property of the new instance of Form1 it was creating.

This ability of forms to set each other's properties and call each other's methods is a very useful technique. For example, when an MDIForm is opening a new child window, it can initialize the new window by setting its properties and calling its methods.

You can also use this technique to pass information between forms.

Tip You can create custom events for forms. "Adding an Event to a Form" later in this chapter, provides a step by step procedure.

Other Kinds of Modules

You add properties, methods, and events to form classes by putting code in their code modules. In the same way, you can add properties, methods, and events to class modules and — if you have the Professional or Enterprise Edition of Visual Basic — to UserControl and UserDocument code modules.

As you read "Adding Properties and Methods to a Class" and "Adding Events to a Class," remember that everything you read applies to form classes as well as to class modules.

For More Information What the heck is a class module? "Class Module Step by Step" shows how to define a class and illustrates the life cycle of the objects you create from that class.


Class Module Step by Step

This example shows how you can use class modules to define classes, from which you can then create objects. It will also show you how to create properties and methods for the new class, and demonstrate how objects are created and destroyed.

Open a new Standard Exe project, and insert a class module by selecting Add Class Module from the Project menu. Draw four command buttons on the form. The following table lists the property values you need to set for the objects in this example.

Object | Property | Setting
Class module | Name | Thing
Command1 | Caption | Show the Thing
Command2 | Caption | Reverse the Thing's Name
Command3 | Caption | Create New Thing
Command4 | Caption | Temporary Thing

Note Class modules are saved in files with the extension .cls.

In the class module Declarations section, add the following:

Option Explicit
Public Name As String
Private mdtmCreated As Date

The variable Name will be a property of the Thing object, because it's declared Public.

Note Don't confuse this Name property with the Name property of the class module, which the table above instructed you to set. (The Name property of the class module gives the Thing class its name.) Why would you give the Thing class a Name property? A better question might be, why not? You may want to give the Thing class a Name property because Things should have names! Remember that there's nothing special about the property and method names Visual Basic uses. You can use those same property and method names for your classes.

The variable mdtmCreated is a private data member that is used to store the value of the read-only Created property. The Created property returns the date and time a Thing object was created. To implement the Created property, add the following Property Get to the Declarations section of the class module:

Property Get Created() As Date
   Created = mdtmCreated
End Property

Note If you added the property procedure using the Add Procedure dialog box, on the Tools menu, be sure to delete the Property Let declaration that is automatically added by this dialog. Property Let is only required for read-write properties, as explained in "Putting Property Procedures to Work for You."

The Thing object has one method, ReverseName, which simply reverses the order of the letters in the Name property. It doesn't return a value, so it's implemented as a Sub procedure. Add the following Sub procedure to the class module.

Public Sub ReverseName()
   Dim intCt As Integer
   Dim strNew As String
   For intCt = 1 To Len(Name)
      strNew = Mid$(Name, intCt, 1) & strNew
   Next
   Name = strNew
End Sub

Class modules have two events, Initialize and Terminate. In the Object drop down of the class module, select Class. The Procedure drop down will show the events. Place the following code in the event procedures:

Private Sub Class_Initialize()
   ' Set date/time of object creation, to be returned
   '    by the read-only Created property.
   mdtmCreated = Now
   ' Display object properties.
   MsgBox "Name: " & Name & vbCrLf & "Created: " _
   & Created, , "Thing Initialize"
End Sub

Private Sub Class_Terminate()
   ' Display object properties.
   MsgBox "Name: " & Name & vbCrLf & "Created: " _
   & Created, , "Thing Terminate"
End Sub

Usually, the Initialize event procedure contains any code that needs to be executed at the moment the object is created, such as providing the time stamp for the Created property. The Terminate event contains any code you need to execute in order to clean up after the object when it is being destroyed.

In this example, the two events are being used primarily to give you a visual indication that a Thing object is being created or destroyed.

Using the Thing Object

Add this declaration to the Declarations section of the form module:

Option Explicit
Private mth As Thing

The variable mth will hold a reference to a Thing object, which will be created in the form's Load event. Put the following code in the Form_Load event procedure, and in the Click event procedures for the four buttons.

Private Sub Form_Load()
   Set mth = New Thing
   mth.Name = InputBox("Enter a name for the Thing")
End Sub

' Button "Show the Thing"
Private Sub Command1_Click()
   MsgBox "Name: " & mth.Name & vbCrLf _
   & "Created: " & mth.Created, , "Form Thing"
End Sub

' Button "Reverse the Thing's Name"
Private Sub Command2_Click()
   mth.ReverseName
   ' Click "Show the Thing"
   Command1.Value = True
End Sub

' Button "Create New Thing"
Private Sub Command3_Click()
   Set mth = New Thing
   mth.Name = InputBox( _
   "Enter a name for the new Thing")
End Sub

' Button "Temporary Thing".
Private Sub Command4_Click()
   Dim thTemp As New Thing
   thTemp.Name = InputBox( _
   "Enter a name for the Temporary Thing")
End Sub

Running the Project

Press F5 to run the project. Looking at the code in the Form_Load event procedure, you can see that the New operator is used to create a Thing object. A reference to this Thing is assigned to the variable mth.

You will see the InputBox asking you for a name for the Thing. When you type a name and press ENTER, the return value is assigned to the Name property of the Thing object.

Show the Form Thing

You can verify that the Name property has been assigned by pressing the first button, "Show the Thing," which displays a message box with all the properties of the Thing object.

Reverse the Thing's Name

Press the second button, "Reverse the Thing's Name." This button calls the ReverseName method to turn the Thing object's name around, and then clicks the first button to display the updated property values.

Create New Thing

Click the "Create New Thing" button to destroy the existing Thing object and create a new one. (Or, as it turns out, to create a new Thing and then destroy the old one.)

The New operator causes a new Thing to be created, so you'll see the MsgBox displayed by the new Thing's Initialize event. When you click OK, a reference to the new Thing is placed in the form-level variable mth.

This wipes out the reference to the old Thing. Because there are no more references to it, it's destroyed, and you'll see its Terminate event message box. When you click OK, the InputBox statement requests a name for the new Thing.

Note If you want to destroy the old Thing before creating the new one, you can add the line of code Set mth = Nothing at the beginning of the event procedure.

Temporary Thing

The fourth button demonstrates another aspect of object lifetime. When you press it, you'll be prompted for a name for the temporary Thing.

But wait — there isn't a temporary Thing object yet. You haven't seen its Initialize message box. How can you assign it a name?

Because the variable thTemp was declared As New, a Thing object will be created the moment one of its properties or methods is invoked. This will happen when the return value of the InputBox is assigned to the Name property. Type a name and click OK on the InputBox.

You'll now see the Thing Initialize message box, which shows you that the Name property is still blank. When you click OK to dismiss the message box, the value from the InputBox statement is finally assigned to the Name property. That's a lot of activity for one line of code.

Of course, as soon as you've done that, the Click event procedure ends, and the variable thTemp goes out of scope. The object reference for the temporary Thing is released, so you'll see the Thing Terminate message box. Notice that it contains the name you supplied.

Each time you click this button, another temporary Thing will be created, named, and destroyed.

Closing the Program

Close the program by clicking the form's close button. Do not use the End button on the toolbar. When the program closes, Form1 is destroyed. The variable mth goes out of scope, and Visual Basic cleans up the reference to the Thing. There are no remaining references to the Thing, so it's destroyed, and its Terminate event message box is displayed.

Run the program again, and this time end it using the End button on the toolbar. Notice that the Terminate message box for the Thing object is not displayed.

It's important to remember that ending your program with the End button, or with an End statement in your code, halts the program immediately, without executing the Terminate events of any objects. It's always better to shut down your program by unloading all the forms.

You may find it useful to run the example by pressing F8 to step through the code one line at a time. This is a good way to understand the order of events for object creation and destruction.

Important In an actual application, the Initialize and Terminate events should not contain message boxes, or any other code that allows Windows messages to be processed. In general, it's better to use Debug.Print statements when debugging object lifetimes.

For More Information Forms and controls are a bit different from other objects, as discussed in "Life Cycle of Visual Basic Forms."

You can read more about what you can do with classes and class modules in "Adding Properties and Methods to a Class" and "Adding Events to a Class."


Debugging Class Modules

Debugging class modules differs slightly from debugging ordinary programs. This is because an error in a property or method of a class module always acts like a handled error. (That is, there's always a procedure on the call stack that can handle the error — namely the procedure that called the class module's property or method.)

Visual Basic compensates for this difference by providing the error-trapping option Break in Class Module, in addition to the older options Break on Unhandled Errors and Break on All Errors.

Note You can set the Default Error Trapping State on the General tab of the Options dialog box, available from the Tools menu. The option you select affects the current session, and becomes the default for all subsequent instances of Visual Basic. To change the setting only for the current session, without affecting the default, select Toggle from the Code window context menu (which is available by right-clicking on the Code window).

For example, suppose the class module Class1 contains the following code:

Public Sub Oops()
   Dim intOops As Integer
   intOops = intOops / 0
End Sub

Now suppose a procedure in another class module, form, or standard module calls the member Oops:

Private Sub Command1_Click()
   Dim c1 As New Class1
   c1.Oops
End Sub

If the error trapping option is set to Break on Unhandled Errors, execution will not stop on the zero divide. Instead, the error will be raised in the calling procedure, Command1_Click. Execution will stop on the call to the Oops method.

You could use Break on All Errors to stop in the zero divide, but Break on All Errors is a very inconvenient option for most purposes. It stops on every error, even errors for which you've written error handling code.

Break in Class Module is a compromise setting:

  • Execution will not stop on class module code for which you've written an error handler.

  • Execution only stops on an error that's unhandled in the class module, and therefore would be returned to the caller of the method.

  • When the Visual Basic development environment is started, it defaults to Break in Class Module.

  • If there are no class modules involved, Break in Class Module is exactly the same as Break on Unhandled Errors.

Tip When you hit a break point using Break in Class Module or Break on All Errors, you can step or run past the error — into your error handling code or into the code that called procedure in which the error occurred — by pressing ALT+F8 or ALT+F5.

For More Information Debugging is discussed in detail in "Debugging Your Code and Handling Errors."


Life Cycle of Visual Basic Forms

Because they're visible to the user, forms and controls have a different life cycle than other objects. For example, a form will not close just because you've released all your references to it. Visual Basic maintains a global collection of all forms in your project, and only removes a form from that collection when you unload the form.

In similar fashion, Visual Basic maintains a collection of controls on each form. You can load and unload controls from control arrays, but simply releasing all references to a control is not sufficient to destroy it.

For More Information The Forms and Controls collections are discussed in "Collections in Visual Basic" earlier in this chapter.

States a Visual Basic Form Passes Through

A Visual Basic form normally passes through four states in its lifetime:

  1. Created, but not loaded.

  2. Loaded, but not shown.

  3. Shown.

  4. Memory and resources completely reclaimed.

There's a fifth state a form can get into under certain circumstances: Unloaded and unreferenced while a control is still referenced.

This topic describes these states, and the transitions between them.

Created, But Not Loaded

The beginning of this state is marked by the Initialize event. Code you place in the Form_Initialize event procedure is therefore the first code that gets executed when a form is created.

In this state, the form exists as an object, but it has no window. None of its controls exist yet. A form always passes through this state, although its stay there may be brief.

For example, if you execute Form1.Show, the form will be created, and Form_Initialize will execute; as soon as Form_Initialize is complete, the form will be loaded, which is the next state.

The same thing happens if you specify a form as your Startup Object, on the General tab of the Project Properties dialog box (which is available from the Project menu). A form specified as the Startup Object is created as soon as the project starts, and is then immediately loaded and shown.

Note You can cause your form to load from within Form_Initialize, by calling its Show method or by invoking its built-in properties and methods, as described below.

Remaining Created, But Not Loaded

By contrast, the following code creates an instance of Form1 without advancing the form to the loaded state:

Dim frm As Form1
Set frm = New Form1

Once Form_Initialize has ended, the only procedures you can execute without forcing the form to load are Sub, Function, and Property procedures you've added to the form's code window. For example, you might add the following method to Form1:

Public Sub ANewMethod()
   Debug.Print "Executing ANewMethod"
End Sub

You could call this method using the variable frm (that is, frm.ANewMethod) without forcing the form on to the next state. In similar fashion, you could call ANewMethod in order to create the form:

Dim frm As New Form1
frm.ANewMethod

Because frm is declared As New, the form is not created until the first time the variable is used in code — in this case, when ANewMethod is invoked. After the code above is executed, the form remains created, but not loaded.

Note Executing Form1.ANewMethod, without declaring a form variable, has the same effect as the example above. As explained in "Customizing Form Classes," Visual Basic creates a hidden global variable for each form class. This variable has the same name as the class; it's as though Visual Basic had declared Public Form1 As New Form1.

You can execute as many custom properties and methods as you like without forcing the form to load. However, the moment you access one of the form's built-in properties, or any control on the form, the form enters the next state.

Note You may find it helpful to think of a form as having two parts, a code part and a visual part. Before the form is loaded, only the code part is in memory. You can call as many procedures as you like in the code part without loading the visual part of the form.

The Only State All Forms Pass Through

Created, But Not Loaded is the only state all forms pass through. If the variable frm in the examples above is set to Nothing, as shown here, the form will be destroyed before entering the next state:

Dim frm As New Form1
frm.ANewMethod
Set frm = Nothing      ' Form is destroyed.

A form used in this fashion is no better than a class module, so the vast majority of forms pass on to the next state.

Loaded, But Not Shown

The event that marks the beginning of this state is the familiar Load event. Code you place in the Form_Load event procedure is executed as soon as the form enters the loaded state.

When the Form_Load event procedure begins, the controls on the form have all been created and loaded, and the form has a window — complete with window handle (hWnd) and device context (hDC) — although that window has not yet been shown.

Any form that becomes visible must first be loaded.

Many forms pass automatically from the Created, But Not Loaded state into the Loaded, but Not Shown state. A form will be loaded automatically if:

  • The form has been specified as the Startup Object, on the General tab of the Project Properties dialog box.

  • The Show method is the first property or method of the form to be invoked, as for example Form1.Show.

  • The first property or method of the form to be invoked is one of the form's built-in members, as for example the Move method.

    Note This case includes any controls on the form, because each control defines a property of the form; that is, in order to access the Caption property of Command1, you must go through the form's Command1 property: Command1.Caption.

  • The Load statement is used to load the form, without first using New or As New to create the form, as described earlier.

Forms That Are Never Shown

In the first two cases listed above, the form will continue directly on to the visible state, as soon as Form_Load completes. In the last two cases, the form will remain loaded, but not shown.

It has long been common coding practice in Visual Basic to load a form but never show it. This might be done for several reasons:

  • To use the Timer control to generate timed events.

  • To use controls for their functionality, rather than their user interface — for example, for serial communications or access to the file system.

  • To execute DDE transactions.

Note With the Professional or Enterprise edition, you can create ActiveX components (formerly called OLE servers), which are often better at providing code-only functionality than controls are. See Creating ActiveX Components in the Component Tools Guide.

Always Coming Home

Forms return from the visible state to the loaded state whenever they're hidden. Returning to the loaded state does not re-execute the Load event, however. Form_Load is executed only once in a form's life.

Shown

Once a form becomes visible, the user can interact with it. Thereafter, the form may be hidden and shown as many times as you like before finally being unloaded.

Interlude: Preparing to Unload

A form may be either hidden or visible when it's unloaded. If not explicitly hidden, it remains visible until unloaded.

The last event the form gets before unloading is the Unload event. Before this event occurs, however, you get a very important event called QueryUnload. QueryUnload is your chance to stop the form from unloading. If there's data the user might like to save, this is the time to prompt the user to save or discard changes.

Important Setting the Cancel argument of the QueryUnload to True will stop the form from unloading, negating an Unload statement.

One of most powerful features of this event is that it tells you how the impending unload was caused: By the user clicking the Close button; by your program executing the Unload statement; by the application closing; or by Windows closing. Thus QueryUnload allows you to offer the user a chance to cancel closing the form, while still letting you close the form from code when you need to.

Important Under certain circumstances, a form will not receive a QueryUnload event: If you use the End statement to terminate your program, or if you click the End button (or select End from the Run menu) in the development environment.

For More Information See "QueryUnload Event" in the Language Reference.

Returning to the Created, But Not Loaded State

When the form is unloaded, Visual Basic removes it from the Forms collection. Unless you've kept a variable around with a reference to the form in it, the form will be destroyed, and its memory and resources will be reclaimed by Visual Basic.

If you kept a reference to the form in a variable somewhere, such as the hidden global variable described in "Customizing Form Classes," then the form returns to the Created, But Not Loaded state. The form no longer has a window, and its controls no longer exist.

The object is still holding on to resources and memory. All of the data in the module-level variables in the form's code part are still there. (Static variables in event procedures, however, are gone.)

You can use that reference you've been keeping to call the methods and properties that you added to the form, but if you invoke the form's built-in members, or access its controls, the form will load again, and Form_Load will execute.

Memory and Resources Completely Reclaimed

The only way to release all memory and resources is to unload the form and then set all references to Nothing. The reference most commonly overlooked when doing this is the hidden global variable mentioned earlier. If at any time you have referred to the form by its class name (as shown in the Properties Window by the Name property), you've used the hidden global variable. To free the form's memory, you must set this variable to Nothing. For example:

Set Form1 = Nothing

Your form will receive its Terminate event just before it is destroyed.

Tip Many professional programmers avoid the use of the hidden global variable, preferring to declare their own form variables (for example, Dim dlgAbout As New frmAboutBox) to manage form lifetime.

Note Executing the End statement unloads all forms and sets all object variables in your program to Nothing. However, this is a very abrupt way to terminate your program. None of your forms will get their QueryUnload, Unload, or Terminate events, and objects you've created will not get their Terminate events.

Unloaded and Unreferenced, But a Control Is Still Referenced

To get into this odd state, you have to unload and free the form while keeping a reference to one of its controls. If this sounds like a silly thing to do, rest assured that it is.

Dim frm As New Form1
Dim obj As Object
frm.Show vbModal
' When the modal form is dismissed, save a
' reference to one of its controls.
Set obj = frm.Command1
Unload frm
Set frm = Nothing

The form has been unloaded, and all references to it released. However, you still have a reference to one of its controls, and this will keep the code part of the form from releasing the memory it's using. If you invoke any of the properties or methods of this control, the form will be reloaded:

obj.Caption = "Back to life"

The values in module-level variables will still be preserved, but the property values of all the controls will be set back to their defaults, as if the form were being loaded for the first time. Form_Load will execute.

Note In some previous versions of Visual Basic, the form did not completely re-initialize, and Form_Load did not execute again.

Note Not all forms behave as Visual Basic forms do. For example, the Microsoft Forms provided in Microsoft Office don't have Load and Unload events; when these forms receive their Initialize events, all their controls exist and are ready to use.

For More Information Forms are discussed in "Designing a Form" in "Forms, Controls, and Menus" and in "More About Forms" in "Creating a User Interface."


Class Modules vs. Standard Modules

Classes differ from standard modules in the way their data is stored. There's never more than one copy of a standard module's data. This means that when one part of your program changes a public variable in a standard module, and another part of your program subsequently reads that variable, it will get the same value.

Class module data, on the other hand, exists separately for each instance of the class (that is, for each object created from the class).

By the same token, data in a standard module has program scope — that is, it exists for the life of your program — while class module data for each instance of a class exists only for the lifetime of the object; it's created when the object is created, and destroyed when the object is destroyed.

Finally, variables declared Public in a standard module are visible from anywhere in your project, whereas Public variables in a class module can only be accessed if you have an object variable containing a reference to a particular instance of a class.

All of the above are also true for public procedures in standard modules and class modules. This is illustrated by the following example. You can run this code by opening a new Standard Exe project and using the Project menu to add a module and a class module.

Place the following code in Class1:

' The following is a property of Class1 objects.
Public Comment As String

' The following is a method of Class1 objects.
Public Sub ShowComment()
   MsgBox Comment, , gstrVisibleEverywhere
End Sub

Place the following code in Module1:

' Code in the standard module is global.
Public gstrVisibleEverywhere As String

Public Sub CallableAnywhere(ByVal c1 As Class1)
   ' The following line changes a global variable
   ' (property) of an instance of Class1. Only the
   ' particular object passed to this procedure is
   ' affected.
   c1.Comment = "Touched by a global function."
End Sub

Put two command buttons on Form1, and add the following code to Form1:

Private mc1First As Class1
Private mc1Second As Class1

Private Sub Form_Load()
   ' Create two instances of Class1.
   Set mc1First = New Class1
   Set mc1Second = New Class1
   gstrVisibleEverywhere = "Global string data"
End Sub

Private Sub Command1_Click()
   Call CallableAnywhere(mc1First)
   mc1First.ShowComment
End Sub

Private Sub Command2_Click()
   mc1Second.ShowComment
End Sub

Press F5 to run the project. When Form1 is loaded, it creates two instances of Class1, each having its own data. Form1 also sets the value of the global variable gstrVisibleEverywhere.

Press Command1, which calls the global procedure and passes a reference to the first Class1 object. The global procedure sets the Comment property, and Command1 then calls the ShowComment method to display the object's data.

As Figure 9.6 shows, the resulting message box demonstrates that the global procedure CallableAnywhere set the Comment property of the object that was passed to it, and that the global string is visible from within Class1.

Figure 9.6 Message box from the first Class1 object

Press Command2, which simply calls the ShowComment method of the second instance of Class1.

As Figure 9.7 shows, both objects have access to the global string variable; but the Comment property of the second object is blank, because calling the global procedure CallableAnywhere only changed the Comment property for the first object.

Figure 9.7 Message box from the second Class1 object

Important Avoid making the code in your classes dependent on global data — that is, public variables in standard modules. Many instances of a class can exist simultaneously, and all of these objects share the global data in your program.

Using global variables in class module code also violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all their data.

Static Class Data

There may be occasions when you want a single data item to be shared among all objects created from a class module. This is sometimes referred to as static class data.

You cannot implement true static class data in a Visual Basic class module. However, you can simulate it by using Property procedures to set and return the value of a Public data member in a standard module, as in the following code fragment:

' Read-only property returning the application name.
Property Get CommonString() As String
   ' The variable gstrVisibleEverywhere is stored in a
   ' standard module, and declared Public.
   CommonString = gstrVisibleEverywhere
End Property

Note You cannot use the Static keyword for module-level variables in a class module. The Static keyword can only be used within procedures.

It's possible to simulate static class data that's not read-only by providing a corresponding Property Let procedure — or Property Set for a property that contains an object reference — to assign a new value to the standard module data member. Using global variables in this fashion violates the concept of encapsulation, however, and is not recommended.

For example, the variable gstrVisibleEverywhere can be set from anywhere in your project, even from code that doesn't belong to the class that has the CommonString property. This can lead to subtle errors in your program.

For More Information Global data in ActiveX components requires different handling than in ordinary programs. If you have the Professional or Enterprise Edition of Visual Basic, see "Standard Modules vs. Class Modules" in "General Principles of Component Design" of Creating ActiveX Components in the Component Tools Guide.


Adding Properties and Methods to a Class

The properties and methods of a class make up its default interface. The default interface is the most common way of manipulating an object.

In general, properties represent data about an object, while methods represent actions an object can take. To put it another way, properties provide the description of an object, while methods are its behavior.

The following topics describe the mechanisms for adding properties and methods to your classes, and address some of the issues you'll meet.

  • Adding Properties to a Class Discusses the two ways of adding properties to your classes — the easy way and the right way.

  • Property Procedures vs. Public Variables Although property procedures are the best way to implement most properties, public variables have their uses.

  • Putting Property Procedures to Work for You Provides full details of the way property procedures work, and describes the implementation of common property types — for example, read-only, read-write, object, and Variant.

  • Adding Methods to a Class Methods — the behavior of an object — are just public procedures you add to your class module.

  • Is It a Property or a Method? Sometimes it's not obvious whether the functionality you envision is best implemented as a property or a method. This topic provides some guidelines.

  • Making a Property or Method the Default You can use the Procedure Attributes dialog to give your class a default member.

  • Friend Properties and Methods Friend procedures are midway between Public and Private. Like Private, they aren't members of a class's interface, but like Public they're visible throughout your project.

    Note Events aren't part of the default interface. Events are outgoing interfaces (that is, interfaces that reach out and touch other objects), while properties and methods belong to incoming interfaces (that is, interfaces whose members are invoked by other objects). The default interface of a Visual Basic object is an incoming interface.

Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.

For More Information Events are discussed in "Adding Events to a Class" later in this chapter.


Adding Properties to a Class

The easiest way to define properties for a class is by adding public variables to the class module. For example, you could very easily create an Account class by declaring two public variables in a class module named Account:

Public Balance As Double
Public Name As String

This is pretty easy. It's just as easy to create private data for a class; simply declare a variable Private, and it will be accessible only from code within the class module:

Private mstrMothersMaidenName As String
Private mintWithdrawalsMonthToDate As Integer

Data Hiding

The ability to protect part of an object's data, while exposing the rest as properties, is called data hiding. This is one aspect of the object-oriented principle of encapsulation, as explained in "Classes: Putting User-Defined Types and Procedures Together."

Data hiding means that you can make changes in the implementation of a class — for example, increasing the Account class's private variable mintWithdrawalsMonthToDate from an Integer to a Long — without affecting existing code that uses the Account object.

Data hiding also allows you to define properties that are read-only. For example, you could use a Property Get procedure to return the value of the private variable containing the number of withdrawals in a month, while only incrementing the variable from within the Account object's code. Which brings us to property procedures.

Property Procedures

Data hiding wouldn't be much use if the only way you could create properties was by declaring public variables. How much good would it do you to give the Account class a Type property, if any code that had a reference to an Account object could blithely set the account type to any value at all?

Property procedures allow you to execute code when a property value is set or retrieved. For example, you might want to implement the Type property of the Account object as a pair of Property procedures:

Public Enum AccountTypes
   atSavings = 1
   atChecking
   atLineOfCredit
End Enum

' Private data storage for the Type property.
Private mbytType As AccountTypes

Public Property Get Type() As AccountTypes
   Type = mbytType
End Property

Public Property Let Type(ByVal NewType As AccountTypes)
   Select Case NewType
      Case atChecking, atSavings, atLineOfCredit
         ' No need to do anything if NewType is valid.
      Case Else
         Err.Raise Number:=vbObjectError + 32112, _
         Description:="Invalid account type"
   End Select
   If mbytType > NewType Then
      Err.Raise Number:=vbObjectError + 32113, _
      Description:="Cannot downgrade account type"
   Else
      mbytType = NewType
   End If
End Property

Now suppose you have a variable named acct that contains a reference to an Account object. When the code x = acct.Type is executed, the Property Get procedure is invoked to return the value stored in the class module's private data member mbytType.

When the code acct.Type = atChecking is executed, the Property Let is invoked. If the Account object is brand new, mbytType will be zero, and any valid account type can be assigned. If the current account type is atSavings, the account will be upgraded.

However, if the current account type is atLineOfCredit, the Property Let will raise an error, preventing the downgrade. Likewise, if the code acct.Type = 0 is executed, the Select statement in the Property Let will detect the invalid account type and raise an error.

In short, property procedures allow an object to protect and validate its own data.

For More Information Are public variables good for anything, then? "Property Procedures vs. Public Variables" outlines the appropriate uses of both.

The capabilities of property procedures are explored further in "Putting Property Procedures to Work for You."


Property Procedures vs. Public Variables

Property procedures are clearly such a powerful means for enabling encapsulation that you may be wondering if you should even bother with public variables. The answer, as always in programming, is "Of course — sometimes." Here are some ground rules:

Use property procedures when:

  • The property is read-only, or cannot be changed once it has been set.

  • The property has a well-defined set of values that need to be validated.

  • Values outside a certain range — for example, negative numbers — are valid for the property's data type, but cause program errors if the property is allowed to assume such values.

  • Setting the property causes some perceptible change in the object's state, as for example a Visible property.

  • Setting the property causes changes to other internal variables or to the values of other properties.

Use public variables for read-write properties where:

  • The property is of a self-validating type. For example, an error or automatic data conversion will occur if a value other than True or False is assigned to a Boolean variable.

  • Any value in the range supported by the data type is valid. This will be true of many properties of type Single or Double.

  • The property is a String data type, and there's no constraint on the size or value of the string.

Note Don't implement a property as a public variable just to avoid the overhead of a function call. Behind the scenes, Visual Basic will implement the public variables in your class modules as pairs of property procedures anyway, because this is required by the type library.

For More Information The capabilities of property procedures are explored further in "Putting Property Procedures to Work for You."


Putting Property Procedures to Work for You

Visual Basic provides three kinds of property procedures, as described in the following table.

Procedure | Purpose
Property Get | Returns the value of a property.
Property Let | Sets the value of a property.
Property Set | Sets the value of an object property (that is, a property that contains a reference to an object).

As you can see from the table, each of these property procedures has a particular role to play in defining a property. The typical property will be made up of a pair of property procedures: A Property Get to retrieve the property value, and a Property Let or Property Set to assign a new value.

These roles can overlap in some cases. The reason there are two kinds of property procedures for assigning a value is that Visual Basic has a special syntax for assigning object references to object variables:

Dim wdg As Widget
Set wdg = New Widget

The rule is simple: Visual Basic calls Property Set if the Set statement is used, and Property Let if it is not.

Tip To keep Property Let and Property Set straight, hearken back to the Basics of yore, when instead of x = 4 you had to type Let x = 4 (syntax supported by Visual Basic to this very day). Visual Basic always calls the property procedure that corresponds to the type of assignment — Property Let for Let x = 4, and Property Set for Set c1 = New Class1 (that is, object properties).

For More Information "Working with Objects" in "Programming Fundamentals" explains the use of the Set statement with object variables.

Read-Write Properties

The following code fragment shows a typical read-write property:

' Private storage for property value.
Private mintNumberOfTeeth As Integer

Public Property Get NumberOfTeeth() As Integer
   NumberOfTeeth = mintNumberOfTeeth
End Property

Public Property Let NumberOfTeeth(ByVal NewValue _
   As Integer)
   ' (Code to validate property value omitted.)
   mintNumberOfTeeth = NewValue
End Property

The name of the private variable that stores the property value is made up of a scope prefix (m) that identifies it as a module-level variable; a type prefix (int); and a name (NumberOfTeeth). Using the same name as the property serves as a reminder that the variable and the property are related.

As you've no doubt noticed, here and in earlier examples, the names of the property procedures that make up a read-write property must be the same.

Note Property procedures are public by default, so if you omit the Public keyword, they will still be public. If for some reason you want a property to be private (that is, accessible only from within the object), you must declare it with the Private keyword. It's good practice to use the Public keyword, even though it isn't required, because it makes your intentions clear.

Property Procedures at Work and Play

It's instructive to step through some property procedure code. Open a new Standard Exe project and add a class module, using the Project menu. Copy the code for the NumberOfTeeth property, shown above, into Class1.

Switch to Form1, and add the following code to the Load event:

Private Sub Form_Load()
   Dim c1 As Class1
   Set c1 = New Class1
   ' Assign a new property value.
   c1.NumberOfTeeth = 42
   ' Display the property value.
   MsgBox c1.NumberOfTeeth
End Sub

Press F8 to step through the code one line at a time. Notice that when the property value is assigned, you step into the Property Let, and when it's retrieved, you step into the Property Get. You may find it useful to duplicate this exercise with other combinations of property procedures.

Arguments of Paired Property Procedures Must Match

The property procedure examples you've seen so far have been simple, as they will be for most properties. However, property procedures can have multiple arguments — and even optional arguments. Multiple arguments are useful for properties that act like arrays, as discussed below.

When you use multiple arguments, the arguments of a pair of property procedures must match. The following table demonstrates the requirements for arguments in property procedure declarations.

Procedure | Declaration syntax
Property Get | Property Get propertyname(1,..., n) As type
Property Let | Property Let propertyname(1,..., n, n+1)
Property Set | Property Set propertyname(1,..., n, n+1)

The first argument through the second-to-last argument (1,..., n) must share the same names and data types in all Property procedures with the same name. As with other procedure types, all of the required parameters in this list must precede the first optional parameter.

You've probably noticed that a Property Get procedure declaration takes one less argument than the related Property Let or Property Set. The data type of the Property Get procedure must be the same as the data type of the last argument (n+1) in the related Property Let or Property Set.

For example, consider this Property Let declaration, for a property that acts like a two-dimensional array:

Public Property Let Things(ByVal X As Integer, _
ByVal Y As Integer, ByVal Thing As Variant)
   ' (Code to assign array element omitted.)
End Property

The Property Get declaration must use arguments with the same name and data type as the arguments in the Property Let procedure:

Public Property Get Things(ByVal X As Integer, _
ByVal Y As Integer) As Variant
   ' (Code for retrieval from array omitted.)
End Property

The data type of the final argument in a Property Set declaration must be either an object type or a Variant.

Matching Up the Arguments

The reason for these argument matching rules is illustrated in Figure 9.8, which shows how Visual Basic matches up the parts of the assignment statement with the arguments of a Property Let.

Figure 9.8 Calling a Property Let procedure

The most common use for property procedures with multiple arguments is to create property arrays.

Read-Only Properties

To create a read-only property, simply omit the Property Let or (for object properties) the Property Set.

Object Properties

If you're creating a read-write object property, you use a Property Get and a Property Set, as here:

Private mwdgWidget As Widget

Public Property Get Widget() As Widget
   ' The Set statement must be used to return an
   ' object reference.
   Set Widget = mwdgWidget
End Property

Public Property Set Widget(ByVal NewWidget As Widget)
   Set mwdgWidget = NewWidget
End Property

Variant Properties

Read-write properties of the Variant data type are the most complicated. They use all three property procedure types, as shown here:

Private mvntAnything As Variant

Public Property Get Anything() As Variant
   ' The Set statement is used only when the Anything
   ' property contains an object reference.
   If IsObject(mvntAnything) Then
      Set Anything = mvntAnything
   Else
      Anything = mvntAnything
   End If
End Property

Public Property Let Anything(ByVal NewValue As Variant)
   ' (Validation code omitted.)
   mvntAnything = NewWidget
End Property

Public Property Set Anything(ByVal NewValue As Variant)
   ' (Validation code omitted.)
   Set mvntAnything = NewWidget
End Property

The Property Set and Property Let are straightforward, as they're always called in the correct circumstances. However, the Property Get must handle both of the following cases:

strSomeString = objvar1.Anything
Set objvar2 = objvar1.Anything

In the first case, the Anything property contains a string, which is being assigned to a String variable. In the second, Anything contains an object reference, which is being assigned to an object variable.

The Property Get can be coded to handle these cases, by using the IsObject function to test the private Variant before returning the value.

Of course, if the first line of code is called when Anything contains an object reference, an error will occur, but that's not Property Get's problem — that's a problem with using Variant properties.

Write-Once Properties

There are many possible combinations of property procedures. All of them are valid, but some are relatively uncommon, like write-only properties (only a Property Let, no Property Get). And some depend on factors other than the kinds of property procedures you combine.

For example, when you organize the objects in your program by creating an object model, as described in "Object Models" later in this chapter, you may want an object to be able to refer back to the object that contains it. You can do this by implementing a Parent property.

You need to set this Parent property when the object is created, but thereafter you may want to prevent it from being changed — accidentally or on purpose. The following example shows how the Account object might implement a Parent property that points to the Department object that contains the account.

' Private data storage for Parent property.
Private mdeptParent As Department

Property Get Parent() As Department
   ' Use the Set statement for object references.
   Set Parent = mdeptParent
End Property

' The property value can only be set once.
Public Property Set Parent(ByVal NewParent _
As Department)
   If deptParent Is Nothing Then
      ' Assign the initial value.
      Set mdeptParent = NewParent
   Else
      Err.Raise Number:=vbObjectError + 32144, _
      Description:="Parent property is read-only"
   End If
End Property

When you access the parent of an Account object, for example by coding strX = acctNew.Parent.Name to get the department name, the Property Get is invoked to return the reference to the parent object.

The Property Set in this example is coded so that the Parent property can be set only once. For example, when the Department object creates a new account, it might execute the code Set acctNew.Parent = Me to set the property. Thereafter the property is read-only.

For More Information Because forms in Visual Basic are classes, you can add custom properties to forms. See "Customizing Form Classes" earlier in this chapter.


Adding Methods to a Class

The methods of a class are just the public Sub or Function procedures you've declared. Since Sub and Function procedures are public by default, you don't even have to explicitly specify the Public keyword to create a method.

For example, to create a Withdrawal method for the Account class, you could add this Public Function procedure to the class module:

Public Function WithDrawal(ByVal Amount As Currency, _
ByVal TransactionCode As Byte) As Double
   ' (Code to perform the withdrawal and return the
   ' new balance, or to raise an Overdraft error.)
End Function

Tip Although you don't have to type the Public keyword, doing so is good programming practice, because it makes your intent clear to people maintaining your code later.

Declaring Methods as Public Subs

Returning the new balance is optional, since you could easily call the Balance property of the Account object after calling the Withdrawal method. You could thus code Withdrawal as a Public Sub procedure.

Tip If you find yourself calling Balance almost every time you call Withdrawal, returning the new balance will be slightly more efficient. This is because, as noted in "Adding Properties to Class Modules," any property access, even reading a public variable, means a function call — an explicit or implicit Property Get.

Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.

For More Information For more information on Sub and Function procedures, see "Introduction to Procedures" in "Programming Fundamentals."

Protecting Implementation Details

The public interface of a class is defined by the property and method declarations in the class module. As with data hiding, procedures you declare as Private are not part of the interface. This means that you can make changes to utility procedures that are used internally by a class module, without affecting code that uses the objects.

Even more important, you can also change the code inside the public Sub or Function procedure that implements a method, without affecting code that uses the method. As long as you don't change the data types of the procedure's arguments, or the type of data returned by a Function procedure, the interface is unchanged.

Hiding the details of an object's implementation behind the interface is another facet of encapsulation. Encapsulation allows you to enhance the performance of methods, or completely change the way a method is implemented, without having to change code that uses the method.

Note The guidelines for naming interface elements — discussed in "Naming Properties, Methods, and Events" — apply not only to property and method names, but to the names of parameters in the Sub and Function procedures that define your methods. These parameter names are visible when you view the methods in the Object Browser, and can be used as named parameters (that is, parametername:=value) when the methods are invoked.

For More Information Named arguments are introduced in "Passing Arguments to Procedures" in "Programming Fundamentals."

Adding methods to form classes is a powerful programming technique, discussed in "Customizing Form Classes."

Sometimes it's not clear whether a member should be a property or a method. "Is It a Property or a Method?" offers some guidelines.


Is It a Property or a Method?

In general, a property is data about an object, while a method is an action the object can be asked to perform. Some things are obviously properties, like Color and Name, and some are obviously methods, like Move and Show.

As with any facet of human endeavor, however, there's a gray area in which an argument can be made either way.

For example, why is the Item method of the Visual Basic Collection class a method and not an indexed property? Aren't the items in the collection just data? The Item method of a hypothetical Widgets collection class could be implemented either way, as shown here:

' Private storage for the objects in the Widgets
' collection (same for both implementations).
Private mcol As New Collection

Public Property Get Item(Index As Variant) As Widget
   Set Item = mcol.Item(Index)
End Function

- or -

Public Function Item(Index As Variant) As Widget
   Set Item = mcol.Item(Index)
End Function

There's not a whole lot of difference between these two implementations. Both are read-only, so both depend on the Add method of the Widgets class to get Widget objects into the collection. Both delegate everything to a Collection object — even their errors are generated by the Collection!

For More Information Delegation is explained in "The Many (Inter)Faces of Code Reuse" and "Creating Your Own Collection Classes" later in this chapter.

You can get really nit-picky trying to decide whether a member is data about the object or object behavior. For example, you could argue that Item is a method because the collection is doing something for you — looking up the Widget you want. This kind of argument can usually be made with equal validity on either side, however.

You may find it more useful to turn the argument on its head, and ask yourself how you want to think of the member. If you want people to think of it as data about the object, make it a property. If you want them to think of it as something the object does, make it a method.

The Syntax Argument

A strong reason for implementing a member using property procedures depends on the way you want to use the member in code. That is, will the user of a Widgets collection be allowed to code the following?

Set Widgets.Item(4) = wdgMyNewWidget

If so, implement the member as a read-write property, using Property Get and Property Set, because methods don't support this syntax.

Note In most collection implementations you encounter, this syntax is not allowed. Implementing a Property Set for a collection is not as easy as it looks.

The Property Window Argument

You can also suppose for a moment that your object is like a control. Can you imagine the member showing up in the Property window, or on a property page? If that doesn't make sense, don't implement the member as a property.

The Sensible Error Argument

If you forget that you made Item a read-only property and try to assign a value to it, you'll most likely find it easier to understand the error message Visual Basic raises for a Property Get — "Can't assign to read-only property" — than the error message it raises for a Function procedure — "Function call on left-hand side of assignment must return Variant or Object."

The Argument of Last Resort

As a last resort, flip a coin. If none of the other arguments in this topic seem compelling, it probably doesn't make much difference.

For More Information Property procedures are introduced in "Adding Properties to Classes" earlier in this chapter. Methods are discussed in "Adding Methods to Classes."


Making a Property or Method the Default

You can give objects created from your classes default properties, like the default properties of objects provided by Visual Basic. The best candidate for default member is the one you use most often.

To set a property or method as the default

  1. On the Tools menu, select Procedure Attributes to open the Procedure Attributes dialog box.

  2. Click Advanced to expand the Procedure Attributes dialog box.

  3. In the Name box, select the property or method that is currently the default for the class. If the class does not currently have a default member, skip to step 5.

    Note You can use the Object Browser to find out what the current default member of a class is. When you select the class in the Classes list, you can scroll through the members in the Members list; the default member will be marked with a small blue globe beside its icon.

  4. In the Procedure ID box, select None to remove the default status of the property or method.

  5. In the Name box, select the property or method you want to be the new default.

  6. In the Procedure ID box, select (Default), then click OK.

Important A class can have only one default member. If a property or method is already marked as the default, you must reset its procedure ID to None before making another property or method the default. No compile errors will occur if two members are marked as default, but there is no way to predict which one Visual Basic will pick as the default.

You can also open the Procedure Attributes dialog box from the Object Browser. This is convenient when you're changing the default member of a class, because it allows you to locate the existing default member quickly.

To change a default property using the Object Browser

  1. Press F2 to open the Object Browser.

  2. In the Classes list, select the class whose default you want to change.

  3. In the Members list, right-click the member with the small blue globe beside its icon to open the context menu. Click Properties to show the Property Attributes dialog box.

  4. Click Advanced to expand the Procedure Attributes dialog box.

  5. In the Procedure ID box, select None to remove the default status of the property or method, then click OK.

  6. In the Members list, right-click the member you want to be the new default to open the context menu. Click Properties to show the Property Attributes dialog box.

  7. Click Advanced to expand the Procedure Attributes dialog box.

  8. In the Procedure ID box, select (Default), then click OK.

Note You cannot use the Procedure Attributes dialog box to change the default member of a class provided by Visual Basic.

Fixing Defaults You Have Accidentally Made Private or Friend

The Procedure Attributes dialog box only allows you to select public properties and methods as the default for a class. If you make a public property or method the default for a class, and later change the declaration to Private or Friend, the property or method may continue to behave as if it were still declared Public.

To correct this problem, you must make the property or method Public again, because the Procedure Attributes dialog box will not show procedures declared Private and Friend. Once you have changed the declaration to Public, you can use the Procedure Attributes dialog to remove the Default attribute. You can then change the declaration back to Friend or Private.


Friend Properties and Methods

In addition to declaring properties and methods Public and Private, you can declare them Friend. Friend members look just like Public members to other objects in your project. That is, they appear to be part of a class's interface. They are not.

In the ActiveX components you can create with the Professional and Enterprise editions of Visual Basic, Friend members play an important role. Because they're not part of an object's interface, they can't be accessed by programs that use the component's objects. They're visible to all the other objects within the component, however, so they allow safe internal communication within the component.

Important Because Friend members aren't part of an object's public interface, they can't be accessed late bound — that is, through variables declared As Object. To use Friend members, you must declare variables with early binding — that is, As classname.

Standard Exe projects can't be ActiveX components, because their class modules can't be Public, and thus can't be used by other applications. All communication between objects in a Standard Exe project is therefore private, and there's no need for Friend members.

However, Friend members have one particularly useful feature. Because they're not part of an ActiveX interface, they can be used to pass user-defined types between objects without exposing them publicly. For example, suppose you have the following user-defined type in a standard module:

Public Type udtDemo

   intA As Integer
   lngB As Long
   strC As String
End Type

You can define the following private variable and Friend members in Class1:

Private mDemo As udtDemo

Friend Property Get Demo() As udtDemo
   Demo = mDemo
End Property

' Note that udtDemo must be passed by reference.
Friend Property Let Demo(NewDemo As udtDemo)
   mDemo = NewDemo
End Property

Friend Sub SetDemoParts(ByVal A As Integer, _
      ByVal B As Long, ByVal C As String)
   mDemo.intA = A
   mDemo.lngB = B
   mDemo.strC = C
End Sub

Public Sub ShowDemo()
   MsgBox mDemo.intA & vbCrLf _
   & mDemo.lngB & vbCrLf & mDemo.strC
End Sub

Note When you pass user-defined types as Sub, Function, or property procedure arguments, you must pass them by reference. (ByRef is the default for procedure arguments.)

You can then write the following code to use Class1:

Private Sub Command1_Click()
   Dim c1A As New Class1
   Dim c1B As New Class1
   c1A.SetDemoParts 42, 1138, "Howdy"
   c1B.Demo = c1A.Demo
   c1B.ShowDemo
End Sub

The message box will display 42, 1138, and "Howdy."

Note Because Friend procedures are not part of a class's interface, they are not included when you use the Implements statement to implement multiple interfaces, as described in "Polymorphism."

For More Information The use of Friend members in components is discussed in "Private Communication Between Your Objects" in "General Principles of Component Design" in the Component Tools Guide.


Adding Events to a Class

Okay, let's say you've created a dinosaur simulation, complete with Stegosaur, Triceratops, and Tyrannosaur classes. As the final touch, you want the Tyrannosaur to roar, and when it does you want every other dinosaur in your simulation to sit up and take notice.

If the Tyrannosaur class had a Roar event, you could handle that event in all your other dinosaur classes. This topic discusses the declaration and handling of events in your class modules.

Note Kids, don't try this at home, at least with more than a few dinosaurs. Connecting every dinosaur with every other dinosaur using events could make your dinosaurs so slow that mammal objects would take over the simulation.

Properties and methods are said to belong to incoming interfaces, because they're invoked from outside the object. By contrast, events are called outgoing interfaces, because they're initiated within the object, and handled elsewhere.

The following topics describe the process of declaring, raising, and handling events, with examples.

  • Declaring and Raising Events Like procedures, events (and their arguments) have to be declared. In order for a declared event to occur, the object must raise it.

  • Handling an Object's Events The events your objects raise can be handled by other objects using variables declared using the WithEvents keyword.

  • Comparing WithEvents to Control Events on Forms Similarities and differences between event procedures associated with WithEvents variables and those associated with controls on forms.

  • Adding an Event to a Form A short step by step example of adding a custom event to a form.

  • Summary of Declaring, Raising, and Handling Events A summary of the process of using events in classes.

For More Information Creating ActiveX Components, in the Component Tools Guide provided with the Professional and Enterprise editions, discusses the use of events in designing your own software components.

For a discussion of a better way to handle dinosaurs, see "Polymorphism" later in this chapter.


Declaring and Raising Events

Assume for the moment that you have a Widget class. Your Widget class has a method that can take a long time to execute, and you'd like your application to be able to put up some kind of completion indicator.

Of course, you could make the Widget object show a percent-complete dialog box, but then you'd be stuck with that dialog box in every project in which you used the Widget class. A good principle of object design is to let the application that uses an object handle the user interface — unless the whole purpose of the object is to manage a form or dialog box.

The Widget's purpose is to perform other tasks, so it's reasonable to give it a PercentDone event, and to let the procedure that calls the Widget's methods handle that event. The PercentDone event can also provide a mechanism for canceling the task.

You can start building the code example for this topic by opening a Standard Exe project, and adding two buttons and a label to Form1. On the Project menu, select Add Class Module to add a class module to the project. Name the objects as shown in the following table.

Object | Property | Setting
Class module | Name | Widget
First Button | Caption | Start Task
Second Button | Caption | Cancel
Label | Name Caption | lblPercentDone "0"

The Widget Class

You declare an event in the Declarations section of a class module, using the Event keyword. An event can have ByVal and ByRef arguments, as the Widget's PercentDone event demonstrates:

Option Explicit
Public Event PercentDone(ByVal Percent As Single, _
ByRef Cancel As Boolean)

When the calling object receives a PercentDone event, the Percent argument contains the percentage of the task that's complete. The ByRef Cancel argument can be set to True to cancel the method that raised the event.

Note You can declare event arguments just as you do arguments of procedures, with the following exceptions: Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values.

Raising the PercentDone Event

The PercentDone event is raised by the LongTask method of the Widget class. The LongTask method takes two arguments: the length of time the method will pretend to be doing work, and the minimum time interval before LongTask pauses to raise the PercentDone event.

Public Sub LongTask(ByVal Duration As Single, _
ByVal MinimumInterval As Single)
   Dim sngThreshold As Single
   Dim sngStart As Single
   Dim blnCancel As Boolean

   ' The Timer function returns the fractional number
   ' of seconds since Midnight, as a Single.
   sngStart = Timer
   sngThreshold = MinimumInterval

   Do While Timer < (sngStart + Duration)
      ' In a real application, some unit of work would
      ' be done here each time through the loop.

      If Timer > (sngStart + sngThreshold) Then
         RaiseEvent PercentDone( _
         sngThreshold / Duration, blnCancel)
         ' Check to see if the operation was canceled.
         If blnCancel Then Exit Sub
         sngThreshold = sngThreshold + MinimumInterval
      End If
   Loop
End Sub

Every MinimumInterval seconds, the PercentDone event is raised. When the event returns, LongTask checks to see if the Cancel argument was set to True.

Note For simplicity, LongTask assumes you know in advance how long the task will take. This is almost never the case. Dividing tasks into chunks of even size can be difficult, and often what matters most to users is simply the amount of time that passes before they get an indication that something is happening.

For More Information Now that you've declared an event and raised it, how do you get another object to handle it? "Handling an Object's Events" continues the saga of the Widget object.


Handling an Object's Events

An object that raises events is called an event source. To handle the events raised by an event source, you can declare a variable of the object's class using the WithEvents keyword.

This topic continues the Widget object example begun in "Declaring and Raising Events." To handle the PercentDone event of a Widget, place the following code in the Declarations section of Form1:

Option Explicit
Private WithEvents mWidget As Widget
Private mblnCancel As Boolean

The WithEvents keyword specifies that the variable mWidget will be used to handle an object's events. You specify the kind of object by supplying the name of the class from which the object will be created.

The variable mWidget is declared in the Declarations section of Form1 because WithEvents variables must be module-level variables. This is true regardless of the type of module you place them in.

The variable mblnCancel will be used to cancel the LongTask method.

Limitations on WithEvents Variables

You should be aware of the following limitations on the use of WithEvents variables:

  • A WithEvents variable cannot be a generic object variable. That is, you cannot declare it As Object — you must specify the class name when you declare the variable.

  • You cannot declare a WithEvents variable As New. The event source object must be explicitly created and assigned to the WithEvents variable.

  • You cannot declare WithEvents variables in a standard module. You can declare them only in class modules, form modules, and other modules that define classes.

  • You cannot create arrays of WithEvents variables.

Writing Code to Handle an Event

As soon as you declare a variable WithEvents, the variable name appears in the left-hand drop down of the module's code window. When you select mWidget, the Widget class's events will appear in the right-hand drop down, as shown in Figure 9.9.

Figure 9.9 An event associated with a WithEvents variable

Selecting an event will display the corresponding event procedure, with the prefix mWidget_. All the event procedures associated with a WithEvents variable will have the variable name as a prefix. Add the following code to the mWidget_PercentDone event procedure.

Private Sub mWidget_PercentDone(ByVal Percent As _
Single, Cancel As Boolean)
   lblPercentDone.Caption = CInt(100 * Percent) & "%"
   DoEvents
   If mblnCancel Then Cancel = True
End Sub

Whenever the PercentDone event is raised, the event procedure displays the percent complete in a Label control. The DoEvents statement allows the label to repaint, and also gives the user the opportunity to click the Cancel button. Add the following code for the Click event of the button whose caption is Cancel.

Private Sub Command2_Click()
   mblnCancel = True
End Sub

If the user clicks the Cancel button while LongTask is running, the Command2_Click event will be executed as soon as the DoEvents statement allows event processing to occur. The module-level variable mblnCancel is set to True, and the mWidget_PercentDone event then tests it and sets the ByRef Cancel argument to True.

Connecting a WithEvents Variable to an Object

Form1 is all set up to handle a Widget object's events. All that remains is to find a Widget somewhere.

When you declare a variable WithEvents at design time, there is no object associated with it. A WithEvents variable is just like any other object variable. You have to create an object and assign a reference to the object to the WithEvents variable. Add the following code to the Form_Load event procedure to create the Widget.

Private Sub Form_Load()
   Set mWidget = New Widget
End Sub

When the code above is executed, Visual Basic creates a Widget and connects its events to the event procedures associated with mWidget. From that point on, whenever the Widget raises its PercentDone event, the mWidget_PercentDone event procedure will be executed.

To call the LongTask method, add the following code to the Click event of the button whose caption is Start Task.

' Start Task button.
Private Sub Command1_Click()
   mblnCancel = False
   lblPercentDone.Caption = "0%"
   lblPercentDone.Refresh

   Call mWidget.LongTask(14.4, 0.66)

   If Not mblnCancel Then lblPercentDone.Caption = 100
End Sub

Before the LongTask method is called, the label that displays the percent complete must be initialized, and the module-level Boolean flag for canceling the method must be set to False.

LongTask is called with a task duration of 14.4 seconds. The PercentDone event is to be raised once every two-thirds of a second. Each time the event is raised, the mWidget_PercentDone event procedure will be executed.

When LongTask is done, mblnCancel is tested to see if LongTask ended normally, or if it stopped because mblnCancel was set to True. The percent complete is updated only for the former case.

Running the Program

Press F5 to put the project in Run mode. Click the Start Task button. Each time the PercentDone event is raised, the label is updated with the percentage of the task that's complete. Click the Cancel button to stop the task. Notice that the appearance of the Cancel button doesn't change immediately when you click it. The Click event can't happen until the DoEvents statement allows event processing.

You may find it instructive to run the program with F8, and step through the code a line at a time. You can clearly see how execution enters LongTask, and then re-enters Form1 briefly each time the PercentDone event is raised.

What would happen if, while execution was back in Form1's code, the LongTask method was called again? Confusion, chaos, and eventually (if it happened every time the event was raised) a stack overflow.

Handling Events for a Different Widget

You can cause the variable mWidget to handle events for a different Widget object by assigning a reference to the new Widget to mWidget. In fact, you can make the code in Command1 do this every time you click the button, by adding two lines of code:

Set mWidget = New Widget      '<- New line.
Call mWidget.LongTask(14.4, 0.66)
Set mWidget = Nothing         '<- New line.

The code above creates a new Widget each time the button is pressed. As soon as the LongTask method completes, the reference to the Widget is released by setting mWidget to Nothing, and the Widget is destroyed.

A WithEvents variable can only contain one object reference at a time, so if you assign a different Widget object to mWidget, the previous Widget object's events will no longer be handled. If mWidget is the only object variable containing a reference to the old Widget, the object will be destroyed.

Note You can declare as many WithEvents variables as you need, but arrays of WithEvents variables are not supported.

Terminating Event Handling for a WithEvents Variable

As long as there is a Widget object assigned to the variable mWidget, the event procedures associated with mWidget will be called whenever the Widget raises an event. To terminate event handling, you can set mWidget to Nothing, as shown in the following code fragment.

' Terminate event handling for mWidget.
Set mWidget = Nothing

When a WithEvents variable is set to Nothing, Visual Basic disconnects the object's events from the event procedures associated with the variable.

Important A WithEvents variable contains an object reference, just like any other object variable. This object reference counts toward keeping the object alive. When you are setting all references to an object to Nothing in order to destroy it, don't forget the variables you declared WithEvents.

For More Information The event procedures associated with WithEvents variables look a lot like event procedures for controls on forms. "Comparing WithEvents to Control Events on Forms" discusses the similarities and differences.


Comparing WithEvents to Control Events on Forms

You've probably noticed some similarities between the way you use WithEvents variables and the way you handle the events raised by controls on a form. In both cases, when you select the event in the right-hand drop down of a code window, you get an event procedure containing the correct arguments for the event.

In fact, the mechanism is exactly the same. A control is treated as a property of the form class, and the name of that property is the value you assigned to the control's Name property in the Properties window.

It's as if there's a Public module-level variable with the same name as the control, and all of the control's event procedure names begin with that variable name, just as they would with a WithEvents variable.

You can easily see this by declaring the variable mWidget Public instead of Private. The moment you do this, mWidget will show up in the Object Browser as a property of Form1, just like the controls on the form.

The difference between the two cases is that Visual Basic automatically creates instances of all the controls on a form when the form is created, whereas you have to create your own instances of classes whose events you want to handle, and assign references to those objects to WithEvents variables.

For More Information You can add your own events to forms, as discussed in "Adding an Event to a Form."


Adding an Event to a Form

The following step by step procedure shows how you can create custom events for forms. To try this exercise, open a new Standard Exe project and do the following:

To add an event to Form1

  1. On the Project menu, select Add Class Module to add a class module to the project. Place the following code in the Declarations section of Class1:
    Public Property Get Form1() As Form1
   Set Form1 = mForm1
End Property

Public Property Set Form1(ByVal NewForm1 As Form1)
   Set mForm1 = NewForm1
End Property

    If you're using Procedure View, the property procedures can't be viewed at the same time. Click the Full Module View button at the bottom left corner of the code window to switch to Full Module View. You can return to Procedure View by clicking the Procedure View button next to it. (Hover the mouse over the buttons to see which is which.)

  2. Add the following code to the Declarations section of Form1:
    Event Gong
Private mc1 As Class1

    Now that Class1 has been created, it's possible to create a variable of type Class1. This procedure switches between Form1 and Class1 several times, because a step in one module requires first adding code to the other.

  3. Go back to Class1 and add the following code to the Declarations section.
    Private WithEvents mForm1 As Form1

    As discussed in "Adding Events to a Class," the WithEvents keyword means this instance of Form1 is associated with events. Note that this step wasn't possible until the Gong event had been created.

  4. In the left-hand (Object) drop down on Class1's Code window, select mForm1 to get the event procedure for the Gong event. Add the following code to the event procedure:
    Private Sub mForm1_Gong()
   MsgBox "Gong!"
End Sub
  5. Go back to Form1. In the Object drop down, select Form. In the right-hand (Procedure) drop down, select Load. Add the following code to the event procedure:
    Private Sub Form_Load()
   Set mc1 = New Class1
   Set mc1.Form1 = Me
End Sub

    The first line creates a Class1 object, and the second assigns to its Form1 property (created in step 1) a reference to Form1 (that is, Me — when you're in Form1's Code window, Me refers to Form1; when you're in Class1's Code window, Me refers to Class1).

  6. Put three text boxes on Form1. Use the Object and Procedure drop downs to select the Change event procedure for each control in turn, and place the same line of code in each:
    Private Sub Text1_Change()
   RaiseEvent Gong
End Sub

    Each time the contents of a text box change, the form's Gong event will be raised.

  7. Press F5 to run the project. Each time you type a character in one of the text boxes, the message box rings a bell. It's very annoying, but it shows how you can add an event to a form, and thus get notifications from several controls.

As shown in "Declaring and Raising Events," you can add arguments to events. For example, you might pass the name of the control — or better still, a reference to the control — to the receiver of the event.


Summary of Declaring, Raising, and Handling Events

To add an event to a class and then use the event, you must:

  • In the Declarations section of the class module that defines the class, use the Event statement to declare the event with whatever arguments you want it to have. Events are always Public.

    Note Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values.

  • At appropriate places in the class module's code, use the RaiseEvent statement to raise the event, supplying the necessary arguments.

  • In the Declarations section of the module that will handle the event, add a variable of the class type, using the WithEvents keyword. This must be a module-level variable.

  • In the left-hand drop down of the code window, select the variable you declared WithEvents.

  • In the right-hand drop down, select the event you wish to handle. (You can declare multiple events for a class.)

  • Add code to the event procedure, using the supplied arguments.

For More Information Details and code examples are provided in "Adding Events to a Class."


Creating Data-Aware Classes

If you’ve read the preceding material on creating classes, you know by now that a class is an object that encapsulates data and code, and that the properties of a class are the data that describe an object. You also know that you can use property procedures or public properties to expose the data represented by those properties.

So far, so good — all of the examples thus far have dealt with transient data, that is, data that is created and consumed at run time. For many programs, this may be all that you need, but what if you need to store data between sessions, or utilize data that already exists outside of your program? In order to work with external sources of data, you need to make your class data-aware.

Data-aware classes can be divided into two categories — data consumers and data sources. Class modules have two design-time properties, DataBindingBehavior and DataSourceBehavior, that determine how a class will interact with external data. The BindingCollection object is used to bind data-aware classes to controls or to each other.

Data Sources

A data source is a class that provides data from an external source to be consumed by other objects. A Data control is in reality an instance of a class that is a data source, but classes that have been set up to act as data sources can be much more powerful than a Data control. Unlike the Data control, a data-aware class doesn’t have to have a visual representation, and it isn’t limited to a particular data interface such as Data Access Objects (DAO) or Remote Data Objects (RDO). In fact, a data-aware class can act as a data source for any type of data, including traditional ODBC sources, ActiveX Data Objects (ADO), or any OLE DB provider.

The DataSourceBehavior property determines whether or not a class can act as a data source. By setting the DataSourceBehavior to 1 (vbDataSource), your class can act as a source of data for other objects.

Data Consumers

Simply put, a data consumer is a class that can be bound to an external source of data, much as a TextBox control can be bound to a Data control. In earlier versions of Visual Basic, controls were the only objects that could be bound to a data source. Data-aware classes set up as data consumers allow you to bind any object to any object created from a class that has been set up as a data source.

The DataBindingBehavior property allows a class to bind to external data. By setting this property to 1 (vbSimpleBound), an object created from your class will be bound to a single data field in an external data source. By setting the DataBindingBehavior to 2 (vbComplexBound), your class will be bound to a row of data in an external data source. Think of it this way — if your objects were controls, a TextBox control would be simple bound, whereas a grid control would be complex bound.

The BindingCollection Object

Just as you would bind a control to a database through a Data control, data-aware classes need a central object to bind them together. That object is the BindingCollection object. Just as it sounds, the BindingCollection is a collection of bindings between a data source and one or more data consumers.

In order to use the BindingCollection object you must first add a reference to the Microsoft Data Binding Collection by selecting it in the References dialog, available from the Project menu. As with any object, you’ll need to create an instance of the BindingCollection object at run time.

The DataSource property of the BindingCollection object is used to specify the object that will provide the data. This object must be a class or UserControl with its DataSourceBehavior property set to vbDataSource.

Once the BindingCollection has been instantiated and its DataSource set, you can use the Add method to define the binding relationships. The Add method takes three required arguments: the name of the consumer object, the property of that object to be bound to the source, and the field from the source that will be bound to the property. You can add multiple bindings to the BindingCollection by repeating the Add method; you can use the Remove method to delete a binding.

For More Information For step-by step examples of creating data-aware classes, see "Creating a Data Source" and "Creating a Data Consumer."


Creating a Data Source

In this section, we’ll walk step-by-step through the process of creating a data-aware class that acts as a data source. This example will bind a TextBox control to our data source class in order to display the data. The next section, "Creating a Data Consumer," demonstrates how to bind our data source class to a data consumer class.

The code examples in this section are taken from the Data-aware Classes (Dataware.vbp) sample. You'll find this application in the Samples directory.

Creating a data source is a two-step process. In the first step we’ll create the data source class; in the second step we’ll hook it up to a TextBox control in order to display the output.

Creating the Source Class

The first step in creating a source class is to define a new class and give it the properties and methods necessary to provide data:

  1. Open a new Standard EXE project, and insert a class module by selecting Add Class Module from the Project menu.

  2. In the Properties window, set the properties of the class as follows:
    Property | Setting
    Name | MySource
    DataSourceBehavior | vbDataSource

    When DataSourceBehavior is set to vbDataSource, a new Sub procedure GetDataMember is added to the class module. You can see this by selecting Class from the Object list in the code editor, then selecting the Event list.

  3. Select References from the Project menu, and add a reference to the Microsoft ActiveX Data Objects 2.0 Library.

  4. Add the following to the Declarations section of the class module:
    Option Explicit
Private rs As ADODB.Recordset

    This declares an object variable for the ADO Recordset object.

  5. Add the following code to the class module’s Initialize event procedure:
    Private Sub Class_Initialize()
   Dim strPath As String, strName As String
   Dim i As Integer

   ' Create an instance of the Recordset.
   Set rs = New ADODB.Recordset

   ' Set the properties of the Recordset.
   With rs
      .Fields.Append "DirID", adInteger
      .Fields.Append "Directory", adBSTR, 255
      .CursorType = adOpenStatic
      .LockType = adLockOptimistic
      .Open
   End With

   ' Loop through the directories and populate
   ' the Recordset.
   strPath = "C:\"
   strName = Dir(strPath, vbDirectory)
   i = 0
   Do While strName <> ""
      If strName <> "." And strName <> ".." Then
         If (GetAttr(strPath & strName) And _
            vbDirectory) = vbDirectory Then
            i = i + 1
            With rs
               .AddNew
               .Fields.Item("DirID") = i
               .Fields.Item("Directory") = strName
               .Update
            End With
         End If
      End If
      strName = Dir
   Loop

   ' Return to the first record.
   rs.MoveFirst
End Sub

    In this example we’re creating a ADO Recordset object on the fly and populating it with a list of directories. Alternatively, you could use an existing recordset by assigning to the Connect property of the ADO Recordset in the Initialize event.

  6. Select Class from the Object list in the Code Editor, then select GetDataMember from the Event list. Add the following code to the GetDataMember Sub procedure:
    Private Sub Class_GetDataMember(DataMember As String, Data As Object)
   ' Assign the Recordset to the Data object.
   Set Data = rs
End Sub

    The GetDataMember procedure sets the source of the data for the class. Your data source class can provide multiple data sources by adding a Select Case statement to the GetDataMember procedure and passing in a source name in the DataMember argument.

  7. Add a new Sub procedure to provide a public method to loop through the Recordset:
    Public Sub Cycle()
   ' Cycle through the Recordset.
   rs.MoveNext
   If rs.EOF = True Then
      rs.MoveFirst
   End If
End Sub

    In order to move through the recordset, we need to expose the navigation methods for our class. For simplicity, this example can only loop forward through the recordset. To make the class more useful, you might want to expose methods such as MoveFirst, MoveNext, Add, and Delete.

Using the Source Class

Now that the source class is defined, we can do something useful with it. In this example we’ll bind it to a TextBox control so that we can see its output; we’ll also use a CommandButton to execute our Cycle method.

  1. Select Form1 and add a TextBox control and a CommandButton control to the form.

  2. In the Properties window, set the properties of the TextBox as follows:
    Property | Setting
    Name | txtConsumer
    Text | (blank)

  3. In the Properties window, set the properties of the CommandButton as follows:
    Property | Setting
    Name | cmdCycle
    Caption | Cycle

  4. Select References from the Project menu, and add a reference to the Microsoft Data Binding Collection.

    The DataBinding object provided by the Data Binding Collection is the "glue" that binds a data source to a data consumer.

  5. Add the following to the Declarations section of the class module:
    Option Explicit
Private objSource As MySource
Private objBindingCollection As BindingCollection

    We need to declare our source class (MySource) and the BindingCollection object using early binding.

  6. Add the following code to the Form_Load event procedure:
    Private Sub Form_Load()
   Set objSource = New MySource
   Set objBindingCollection = New BindingCollection

   ' Assign the source class to the Binding
   ' Collection’s DataSource property.
   Set objBindingCollection.DataSource = objSource
   ' Add a binding.
   ObjBindingCollection.Add txtConsumer, "Text", "Directory"

    In the Load event we create instances of the source class and the BindingCollection object, then we assign the source object to the DataSource property of the BindingCollection. Finally, we add a binding by specifying the name of the consumer (txtConsumer), the Property of the consumer to be bound (the Text property), and the Field property of the source object that we are binding to (Directory).

  7. Add the following code to the cmdCycle Click event procedure:
    Private cmdCycle_Click()
   ' Call the Cycle method of the data source.
   ObjSource.Cycle
End Sub

    This will execute the Cycle method of our source class.

    8.

  8.
Press F5 to run the project.

As you click the Cycle button, directory names from the recordset created in our source class will appear in the TextBox. Congratulations — you’ve just bound a control to a data source class without using a Data control!
    9.

  9.
Save the project. When prompted for filenames, use the following names.

Save the source class as "MySource.cls".

Save the form as "Dataform.frm".

Save the project as "Dataware.vbp".

These files will be used later in "Creating a Data Consumer."
    10.

In the next section "Creating a Data Consumer," we’ll look at the process of creating a data-aware class that acts a consumer of data.


Creating a Data Consumer

In this section, we’ll walk step-by-step through the process of creating a data-aware class that acts as a data consumer. The previous section, "Creating a Data Source," demonstrates how to create a data source to which a data consumer can be bound. This example shows how to create a data consumer class and bind it to the data source created in the previous section.

The code examples in this section are taken from the Data-aware Classes (Dataware.vbp) sample. You'll find this application in the Samples directory.

Binding a Data Consumer to a Data Source Object

This example demonstrates how to create a data consumer class and bind it to a data source class. The example uses the MySource class created in the "Creating a Data Source" topic.

  1. Open the Dataware.vbp project. (Select Open Project from the File menu.)

    Note If you haven’t previously completed the "Creating a Data Source" example this project won’t exist. You can also find a completed version of the Dataware.vbp project in the Samples directory.

  2. Insert a new class module by selecting Add Class Module from the Project menu.

  3. In the Properties window, set the properties of the new class as follows:
    Property | Setting
    Name | MyConsumer
    DataBindingBehavior | vbSimpleBound

  4. Add the following to the Declarations section of the class module:
    Option Explicit
Private mDirectory As String
  5. Add a pair of Property Get / Property Let procedures for a public DirName property:
    Public Property Get DirName() As String
   DirName = mDirectory
End Property

Public Property Let DirName(mNewDir As String)
   mDirectory = mNewDir
   ' Display the new value in the Immediate window.
   Debug.Print mDirectory
End Property

    Since MySource is a nonvisual class, we need to use a Debug.Print statement in the Property Let procedure to prove that it’s retrieving new values from the data source.

  6. Select Form1 and add the following code to the Declarations section:
    Option Explicit
Private objSource As MySource
Private objBindingCollection As BindingCollection
Private objConsumer As MyConsumer

    The new line of code adds a reference to our consumer class.

  7. Add the following code to the Form Load event procedure:
    Private Sub Form_Load()
   Set objSource = New MySource
   Set objBindingCollection = New BindingCollection
   Set objConsumer = New MyConsumer

   ' Assign the source class to the Binding
   ' Collection’s DataSource property.
   Set objBindingCollection.DataSource = objSource
   ' Add a binding.
   objBindingCollection.Add txtConsumer, "Text", "Directory"
   objBindingCollection.Add objConsumer, "DirName", "Directory"

    The new code creates an instance of the consumer class and adds it to the Binding Collection, binding the DirName property of the consumer to the Directory field of the data source.

  8. Press F5 to run the project. Make sure that the Immediate window is visible.

    As you click the Cycle button, the directory names provided by MySource will appear in both the TextBox and the Immediate window, proving that MyConsumer is bound to MySource.


Naming Properties, Methods, and Events

The properties, methods, and events you add to a class module define the interface that will be used to manipulate objects created from the class. When naming these elements, and their arguments, you may find it helpful to follow a few simple rules.

  • Use entire words whenever possible, as for example SpellCheck. Abbreviations can take many forms, and hence can be confusing. If whole words are too long, use complete first syllables.

  • Use mixed case for your identifiers, capitalizing each word or syllable, as for example ShortcutMenus or AsyncReadComplete.

  • Use the correct plural for collection class names, as for example Worksheets, Forms, or Widgets. If the collection holds objects with a name that ends in "s," append the word "Collection," as for example SeriesCollection.

  • Use either verb/object or object/verb order consistently for your method names. That is, use InsertWidget, InsertSprocket, and so on, or always place the object first, as in WidgetInsert and SprocketInsert.

Note While it’s possible to use an underscore character in a property name, an underscore in an event name will cause an error. The underscore character is used in Event procedures to separate the event name from the object name. For this reason, it’s best to avoid the underscore character altogether when naming properties, methods, and events.

One of the chief benefits of programming with objects is code reuse. Following the rules above, which are part of the ActiveX guidelines for interfaces, makes it easier to remember the names and purposes of properties, methods, and events.

For More Information If you have the Professional or Enterprise Edition of Visual Basic, see the expanded list in "What's In a Name?" in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.


Polymorphism

Polymorphism means that many classes can provide the same property or method, and a caller doesn't have to know what class an object belongs to before calling the property or method.

For example, a Flea class and a Tyrannosaur class might each have a Bite method. Polymorphism means that you can invoke Bite without knowing whether an object is a Flea or a Tyrannosaur — although you'll certainly know afterward.

The following topics describe Visual Basic's approach to polymorphism and how you can use it in your programs.

  • How Visual Basic Provides Polymorphism Most object-oriented languages provide polymorphism via inheritance; Visual Basic uses the multiple interface approach of the Component Object Model (COM).

  • Creating and Implementing an Interface An extended code example shows how to create an abstract Animal interface and implement it for Tyrannosaur and Flea classes.

  • Implementing Properties The interfaces you implement can have properties as well as methods, although there are some differences in the way properties are implemented.

  • Time Out for a Brief Discussion of Objects and Interfaces Clarifies the terms object and interface, introduces the concept of querying for an interface, and describes other sources of interfaces to implement.

  • The Many (Inter)Faces of Code Reuse In addition to implementing abstract interfaces, you can reuse your code by implementing the interface of an ordinary class, and then selectively delegating to a hidden instance of the class.

For More Information With the Professional and Enterprise editions of Visual Basic, Polymorphism becomes a powerful mechanism for evolving systems of software components. This is discussed in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.


How Visual Basic Provides Polymorphism

Most object-oriented programming systems provide polymorphism through inheritance. That is, the hypothetical Flea and Tyrannosaur classes might both inherit from an Animal class. Each class would override the Animal class's Bite method, in order to provide its own bite characteristics.

The polymorphism comes from the fact that you could call the Bite method of an object belonging to any class that derived from Animal, without knowing which class the object belonged to.

Providing Polymorphism with Interfaces

Visual Basic doesn't use inheritance to provide polymorphism. Visual Basic provides polymorphism through multiple ActiveX interfaces. In the Component Object Model (COM) that forms the infrastructure of the ActiveX specification, multiple interfaces allow systems of software components to evolve without breaking existing code.

An interface is a set of related properties and methods. Much of the ActiveX specification is concerned with implementing standard interfaces to obtain system services or to provide functionality to other programs.

In Visual Basic, you would create an Animal interface and implement it in your Flea and Tyrannosaur classes. You could then invoke the Bite method of either kind of object, without knowing which kind it was.

Polymorphism and Performance

Polymorphism is important for performance reasons. To see this, consider the following function:

Public Sub GetFood(ByVal Critter As Object, _
ByVal Food As Object)
   Dim dblDistance As Double
   ' Code to calculate distance to food (omitted).
   Critter.Move dblDistance   ' Late bound
   Critter.Bite Food            ' Late bound
End Sub

The Move and Bite methods are late bound to Critter. Late binding happens when Visual Basic can't determine at compile time what kind of object a variable will contain. In this example, the Critter argument is declared As Object, so at run time it could contain a reference to any kind of object — like a Car or a Rock.

Because it can't tell what the object will be, Visual Basic compiles some extra code to ask the object if it supports the method you've called. If the object supports the method, this extra code invokes it; if not, the extra code raises an error. Every method or property call incurs this additional overhead.

By contrast, interfaces allow early binding. When Visual Basic knows at compile time what interface is being called, it can check the type library to see if that interface supports the method. Visual Basic can then compile in a direct jump to the method, using a virtual function table (vtable). This is many times faster than late binding.

Now suppose the Move and Bite methods belong to an Animal interface, and that all animal classes implement this interface. The Critter argument can now be declared As Animal, and the Move and Bite methods will be early bound:

Public Sub GetFood(ByVal Critter As Animal, _
ByVal Food As Object)
   Dim dblDistance As Double
   ' Code to calculate distance to food (omitted).
   Critter.Move dblDistance   ' Early bound (vtable).
   Critter.Bite Food            ' Early bound (vtable).
End Sub

For More Information "Creating and Implementing an Interface" creates an Animal interface and implements it in Flea and Tyrannosaur classes.


Creating and Implementing an Interface

As explained in "How Visual Basic Provides Polymorphism," an interface is a set of properties and methods. In the following code example, you'll create an Animal interface and implement it in two classes, Flea and Tyrannosaur.

You can create the Animal interface by adding a class module to your project, naming it Animal, and inserting the following code:

Public Sub Move(ByVal Distance As Double)

End Sub

Public Sub Bite(ByVal What As Object)

End Sub

Notice that there's no code in these methods. Animal is an abstract class, containing no implementation code. An abstract class isn't meant for creating objects — its purpose is to provide the template for an interface you add to other classes. (Although, as it turns out, sometimes it's useful to implement the interface of a class that isn't abstract; this is discussed later in this topic.)

Note Properly speaking, an abstract class is one from which you can't create objects. You can always create objects from Visual Basic classes, even if they contain no code; thus they are not truly abstract.

Now you can add two more class modules, naming one of them Flea and the other Tyrannosaur. To implement the Animal interface in the Flea class, you use the Implements statement:

Option Explicit
Implements Animal

As soon as you've added this line of code, you can click the left-hand (Object) drop down in the code window. One of the entries will be Animal. When you select it, the right-hand (Procedure) drop down will show the methods of the Animal interface.

Select each method in turn, to create empty procedure templates for all the methods. The templates will have the correct arguments and data types, as defined in the Animal class. Each procedure name will have the prefix Animal_ to identify the interface.

Important An interface is like a contract. By implementing the interface, a class agrees to respond when any property or method of the interface is invoked. Therefore, you must implement all the properties and methods of an interface.

You can now add the following code to the Flea class:

Private Sub Animal_Move(ByVal Distance As Double)
   ' (Code to jump some number of inches omitted.)
   Debug.Print "Flea moved"
End Sub

Private Sub Animal_Bite(ByVal What As Object)
   ' (Code to suck blood omitted.)
   Debug.Print "Flea bit a " & TypeName(What)
End Sub

You may be wondering why the procedures are declared Private. If they were Public, the procedures Animal_Jump and Animal_Bite would be part of the Flea interface, and we'd be stuck in the same bind we were in originally, declaring the Critter argument As Object so it could contain either a Flea or a Tyrannosaur.

Multiple Interfaces

The Flea class now has two interfaces: The Animal interface you've just implemented, which has two members, and the default Flea interface, which has no members. Later in this example you'll add a member to one of the default interfaces.

You can implement the Animal interface similarly for the Tyrannosaur class:

Option Explicit
Implements Animal

Private Sub Animal_Move(ByVal Distance As Double)
   ' (Code to pounce some number of yards omitted.)
   Debug.Print "Tyrannosaur moved"
End Sub

Private Sub Animal_Bite(ByVal What As Object)
   ' (Code to take a pound of flesh omitted.)
   Debug.Print "Tyrannosaur bit a " & TypeName(What)
End Sub

Exercising the Tyrannosaur and the Flea

Add the following code to the Load event of Form1:

Private Sub Form_Load()
   Dim fl As Flea
   Dim ty As Tyrannosaur
   Dim anim As Animal

   Set fl = New Flea
   Set ty = New Tyrannosaur
   ' First give the Flea a shot.
   Set anim = fl
   Call anim.Bite(ty)   'Flea bites dinosaur.
   ' Now the Tyrannosaur gets a turn.
   Set anim = ty
   Call anim.Bite(fl)   'Dinosaur bites flea.
End Sub

Press F8 to step through the code. Notice the messages in the Immediate window. When the variable anim contains a reference to the Flea, the Flea's implementation of Bite is invoked, and likewise for the Tyrannosaur.

The variable anim can contain a reference to any object that implements the Animal interface. In fact, it can only contain references to such objects. If you attempt to assign a Form or PictureBox object to anim, an error will occur.

The Bite method is early bound when you call it through anim, because Visual Basic knows at compile time that whatever object is assigned to anim will have a Bite method.

Passing Tyrannosaurs and Fleas to Procedures

Remember the GetFood procedure from "How Visual Basic Provides Polymorphism?" You can add the second version of the GetFood procedure — the one that illustrates polymorphism — to Form1, and replace the code in the Load event with the following:

Private Sub Form_Load()
   Dim fl As Flea
   Dim ty As Tyrannosaur

   Set fl = New Flea
   Set ty = New Tyrannosaur
   'Flea dines on dinosaur.
   Call GetFood(fl, ty)
   ' And vice versa.
   Call GetFood(ty, fl)
End Sub

Stepping through this code shows how an object reference that you pass to an argument of another interface type is converted into a reference to the second interface (in this case, Animal). What happens is that Visual Basic queries the object to find out whether it supports the second interface. If the object does, it returns a reference to the interface, and Visual Basic places that reference in the argument variable. If the object does not support the second interface, an error occurs.

Implementing Methods That Return Values

Suppose the Move method returned a value. After all, you know how far you want an Animal to move, but an individual specimen might not be able to move that far. It might be old and decrepit, or there might be a wall in the way. The return value of the Move method could be used to tell you how far the Animal actually moved.

Public Function Move(ByVal Distance As Double) _
As Double

End Function

When you implement this method in the Tyrannosaur class, you assign the return value to the procedure name, just as you would for any other Function procedure:

Private Function Animal_Move(ByVal Distance _
As Double) As Double
   Dim dblDistanceMoved As Double
   ' Code to calculate how far to pounce (based on
   ' age, state of health, and obstacles) is omitted.
   ' This example assumes that the result has been
   ' placed in the variable dblDistanceMoved.
   Debug.Print "Tyrannosaur moved"; dblDistanceMoved
   Animal_Move = dblDistanceMoved
End Function

To assign the return value, use the full procedure name, including the interface prefix.

For More Information The interfaces you implement can have properties as well as methods. "Implementing Properties" discusses some differences in the way properties are implemented.


Implementing Properties

This topic continues the code example begun in "Creating and Implementing an Interface," adding properties to the Animal interface that was implemented in the Flea and Tyrannosaur classes. You may find it helpful to read that topic before beginning this one.

Suppose we give the Animal class an Age property, by adding a Public variable to the Declarations section:

Option Explicit
Public Age As Double

The Procedure drop downs in the code modules for the Tyrannosaur and Flea classes now contain property procedures for implementing the Age property, as shown in Figure 9.10.

Figure 9.10 Implementing property procedures

This illustrates a point made in "Adding Properties to a Class" earlier in this chapter. Using a public variable to implement a property is strictly a convenience for the programmer. Behind the scenes, Visual Basic implements the property as a pair of property procedures.

You must implement both procedures. The property procedures are easily implemented by storing the value in a private data member, as shown here:

Private mdblAge As Double

Private Property Get Animal_Age() As Double
   Animal_Age = mdblAge
End Property

Private Property Let Animal_Age(ByVal RHS As Double)
   mdblAge = RHS
End Property

The private data member is an implementation detail, so you have to add it yourself.

Note When Implements provides the template for a Property Set or Property Let, it has no way of determining the name of the last argument, so it substitutes the name RHS, as shown in the code example above.

There's no data validation on a property implemented as a public data member, but that doesn't mean you can't add validation code to the Property Let for Animal_Age. For example, you might want to restrict the values to ages appropriate for a Tyrannosaur or a Flea, respectively.

In fact, this shows the independence of interface and implementation. As long as the interface matches the description in the type library, the implementation can be anything.

Before you go on to the next step, remove the implementation of the read-write Age property from both class modules.

Implementing a Read-Only Property

Of course, allowing the age of an animal to be set arbitrarily is bad object design. The object should know its own age, and provide it to the user as a read-only property. Remove the public variable Age from the Animal class, and add the template for a read-only age property, like this:

Public Property Get Age() As Double

End Property

Now the Procedure drop downs in the code windows for the Tyrannosaur and Flea classes contain only a single entry, Age [PropertyGet]. You might implement this for the Tyrannosaur as follows:

Private mdblBirth As Double

Private Property Get Animal_Age() As Double
   Animal_Age = Now - mdblBirth
End Property

The code above returns the age of the Tyrannosaur in days. You could set mdblBirth in the Initialize event of the Tyrannosaur class, as here:

Private Sub Class_Initialize()
   mdblBirth = Now
End Sub

And of course you could return the property value in more commonly used units, such as dog years.

For More Information We've been tossing interfaces and objects around like they were the same thing, seemingly putting references to objects into one object variable, and references to interfaces into another. "Time Out for a Brief Discussion of Objects and Interfaces" clears matters up.


Time Out for a Brief Discussion of Objects and Interfaces

This topic completes the code example begun in "Creating and Implementing an Interface," and continued in "Implementing Properties." You may find it helpful to read those topics before beginning this one.

The Tyrannosaur and Flea code example seems to play fast and loose with interfaces and objects. References to objects are assigned to one object variable, and references to interfaces to another.

In fact, all of the references are object references. A reference to an interface is also a reference to the object that implements the interface. Furthermore, an object may have multiple interfaces, but it's still the same object underneath.

In Visual Basic, each class has a default interface that has the same name as the class. Well, almost the same. By convention, an underscore is prefixed to the class name. The underscore indicates that this interface is hidden in the type library.

Thus the Tyrannosaur class has a default interface called _Tyrannosaur. Because Tyrannosaur also implements Animal, the class has a second interface named Animal.

However, underneath it all, the object is still a Tyrannosaur. Place a command button on Form1, and add the following code:

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal

   Set ty = New Tyrannosaur
   Set anim = ty
   MsgBox TypeName(anim)
End Sub

You might expect the message box to display "Animal," but in fact it displays "Tyrannosaur."

Querying for Interfaces

When you assign a Tyrannosaur object to variable of type Animal, Visual Basic asks the Tyrannosaur object if it supports the Animal interface. (The method used for this is called QueryInterface, or QI for short; you may sometimes hear QI used as a verb.) If the answer is no, an error occurs.

If the answer is yes, the object is assigned to the variable. Only the methods and properties of the Animal interface can be accessed through this variable.

Generic Object Variables and Interfaces

What happens if you assign the object reference to a generic object variable, as in the following code?

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal
   Dim obj As Object

   Set ty = New Tyrannosaur
   Set anim = ty
   Set obj = anim
   MsgBox TypeName(obj)
End Sub

The result is again Tyrannosaur. Now, what interface do you get when you call properties and methods through the variable obj? Add the following method to the Tyrannosaur class:

Public Sub Growl()
   Debug.Print "Rrrrrr"
End Sub

The Growl method belongs to the Tyrannosaur object's default interface. In the code for the command button's Click event, replace the MsgBox statement with the following two lines of code:

obj.Move 42
obj.Growl

When you run the project and click the button, execution stops on the Growl method, with the error "Object does not support this property or method." Clearly, the interface is still Animal.

This is something to bear in mind when using variables of type Object with objects that have multiple interfaces. The interface the variable will access is the last interface assigned. For example:

Private Sub Command1_Click()
   Dim ty As Tyrannosaur
   Dim anim As Animal
   Dim obj As Object

   Set ty = New Tyrannosaur
   Set anim = ty
   Set obj = anim
   obj.Move 42      ' Succeeds
   obj.Growl      ' Fails

   Set obj = ty
   obj.Move 42      ' Fails
   obj.Growl      ' Succeeds
End Sub

Fortunately, there's very little reason to use the slower, late-bound Object data type with objects that have multiple interfaces. One of the main reasons for using multiple interfaces is to gain the advantage of early binding through polymorphism.

Other Sources of Interfaces

Visual Basic class modules are not your only source of interfaces to implement. You can implement any interface contained in a type library, as long as that interface supports Automation.

If you have the Professional or Enterprise Edition of Visual Basic, you can create your own type libraries of abstract classes. These type libraries can be used in many projects, as described in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.

The Professional and Enterprise editions also include the MkTypLib (Make Type Library) utility in the Tools directory. If you've used this utility with Microsoft Visual C++, you may find it a more congenial way to create interfaces.

Using Interfaces in Your Project

To use an interface in your project, click References on the Project menu to open the References dialog box. If the type library is registered, it will appear in the list of references, and you can check it. If the type library is not in the list, you can use the Browse button to locate it.

Once you have a reference to a type library, you can use Implements to implement any Automation interfaces the type library contains.

For More Information You're not limited to implementing abstract interfaces. "The Many (Inter)Faces of Code Reuse" describes how you can implement an interface and selectively reuse the properties and methods of the class that provides the interface.


The Many (Inter)Faces of Code Reuse

There are two main forms of code reuse — binary and source. Binary code reuse is accomplished by creating and using an object, while source code reuse is achieved by inheritance, which isn't supported by Visual Basic. (Source code reuse can also be achieved by copying and modifying the source code, but this technique is nothing new, and has many well-known problems.)

Visual Basic has been a pioneer of binary code reuse — controls being the classic example. You reuse the code in a control by placing an instance of the control on your form. This is known as a containment relationship or a has-a relationship; that is, the form contains or has a CommandButton.

For More Information Containment relationships are discussed in "Object Models" later in this chapter.

Delegating to an Implemented Object

Implements provides a powerful new means of code reuse. You can implement an abstract class (as discussed in "Creating and Implementing an Interface"), or you can implement the interface of a fully functional class. You can create the inner object (that is, the implemented object) in the Initialize event of the outer object (that is, the one that implements the inner object's interface).

As noted in "Creating and Implementing an Interface," an interface is like a contract — you must implement all the members of the inner object's interface in the outer object's class module. However, you can be very selective in the way you delegate to the properties and methods of the inner object. In one method you might delegate directly to the inner object, passing the arguments unchanged, while in another method you might execute some code of your own before calling the inner object — and in a third method you might execute only your own code, ignoring the inner object altogether!

For example, suppose you have a OneManBand class and a Cacophony class, both of which generate sounds. You'd like to add the functionality of the Cacophony class to the OneManBand class, and reuse some of the implementation of the Cacophony class's methods.

' OneManBand implements the Cacophony interface.
Implements Cacophony

' Object variable to keep the reference in.
Private mcac As Cacophony

Private Sub Class_Initialize()
   ' Create the object.
   Set mcac = New Cacophony
End Sub

You can now go to the Object drop down and select Cacophony, and then get procedure templates for the methods of the Cacophony interface. To implement these methods, you can delegate to the Cacophony object. For example, the Beep method might look like this:

Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
   ' Delegate to the inner Cacophony object.
   Call mcac.Beep(Frequency, Duration)
End Sub

The implementation above is very simple. The outer object (OneManBand) delegates directly to the inner (Cacophony), reusing the Cacophony object's Beep method without any changes. This is a good thing, but it's only the beginning.

The Implements statement is a very powerful tool for code reuse, because it gives you enormous flexibility. You might decide to alter the effects of the OneManBand class's Beep method, by inserting your own code before (or after) the call to the inner Cacophony object:

Private Sub Cacophony_Beep(ByVal Frequency As Double, _
ByVal Duration As Double)
   ' Bump everything up an octave.
   Frequency = Frequency * 2
   ' Based on another property of the OneManBand
   ' class, Staccato, cut the duration of each beep.
   If Staccato Then Duration = Duration * 7 / 8
   Call mcac.Beep(Frequency, Duration)
   ' You can even call other methods of OneManBand.
   If Staccato Then Pause(Duration * 1 / 8)
End Sub

For some of the methods, your implementation may delegate directly to the inner Cacophony object, while for others you may interpose your own code before and after delegating — or even omit delegation altogether, using entirely your own code to implement a method.

Because the OneManBand class implements the Cacophony interface, you can use it with any musical application that calls that interface. Your implementation details are hidden from the calling application, but the resulting sounds are all your own.

Note COM provides another mechanism for binary code reuse, called aggregation. In aggregation, an entire interface is reused, without any changes, and the implementation is provided by an instance of the class being aggregated. Visual Basic does not support this form of code reuse.

Doesn't This Get Tedious?

Writing delegation code can indeed become tedious, especially if most of the outer object's properties and methods simply delegate directly to the corresponding properties and methods of the inner object.

If you have the Professional or Enterprise Edition of Visual Basic, you can use the Visual Basic Extensibility model to create your own delegation wizard to automate the task, similar to the Class Wizard that's included in the Professional and Enterprise editions.

For More Information The use of polymorphism and multiple interfaces in component software is discussed in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.

Using the Extensibility Model is documented in Extending the Visual Basic Environment with Add-Ins in the Component Tools Guide.


Programming with Your Own Objects

You can start using objects gradually, finding useful tasks for which combining code and data is an advantage. You can use the functionality of these objects by declaring object variables, assigning new objects to them, and calling the objects' properties and methods.

As you add more and more objects to your programs, you'll start to see relationships between them. You can begin making program design more dependent on objects and their relationships, and you can begin using more robust techniques — like creating custom collection classes — for expressing those relationships in code.

At some point, you'll suddenly see how linking objects together changes the very nature of your program, and you'll be ready to start designing object-based programs from the ground up.

The following topics provide an overview of these evolutionary changes in your coding style. Read them now, to give yourself a rough picture of where you're headed, and read them again when your ideas of object-based programming begin to gel.

  • Object References and Reference Counting The more you use objects, the more you need to manage your object references, so your component frees memory when it should and shuts down in an orderly fashion.

  • Object Models How do the objects you create from your classes relate to each other? Object models express relationships in terms of containment — that is, one kind of object contains one or more of another kind. This is a powerful organizing principle for your programs.

  • Creating Your Own Collection Classes Exposes some problems with the Visual Basic Collection object, and shows how you can create your own collection classes — that is, classes that delegate to a Collection object (thus gaining the ability to work with For Each … Next) while providing increased robustness and more functionality.

For More Information ActiveX components open up yet another dimension of code reuse and object-based programming. If you have the Professional or Enterprise Edition of Visual Basic, you can begin to explore that dimension through Creating ActiveX Components in the Component Tools Guide.


Object References and Reference Counting

The primary rule for object lifetime is very simple: An object is destroyed when the last reference to it is released. However, as with so much of life, simple doesn't always mean easy.

As you use more objects, and keep more variables containing references to those objects, you may go through periods when it seems impossible to get your objects to go away when you want them to.

At some point, it will occur to you that Visual Basic must be keeping track of object references — otherwise how could it know when the last reference to an object is released? You may start thinking that if only you could get access to Visual Basic's reference counts, debugging would be much easier.

Unfortunately, that's not true. To make using objects more efficient, the Component Object Model (COM) specifies a number of complex shortcuts to its reference counting rules. The net result is that you couldn't trust the value of the reference count even if you had access to it.

According to COM rules, the only information you can depend on is whether or not the reference count is zero. You know when the reference count reaches zero, because your object's Terminate event occurs. Beyond that, there's no reliable information to be gleaned from reference counts.

Note The fact that you don't have to remember the COM reference counting rules is no small thing. Managing reference counts yourself is a lot more difficult than keeping track of which object variables in your program contain references to objects.

Tip Declare your object variables as class types, instead of As Object. That way, if you have a Widget object that isn't terminating, the only variables you need to worry about are those declared As Widget.

For collections of object references, don't use the Visual Basic Collection object by itself. Object references in a Visual Basic Collection object are stored in Variants — which, like variables declared As Object, can hold references to objects of any class. Instead create collection classes of your own that accept objects of only one class, as described in "Creating Your Own Collection Classes." That way, the only collections you need to search for your Widget object are those of type Widget.

Organize your object into a hierarchy, as described in "Object Models." If all of your objects are connected, It's easy to write a procedure that walks through the whole model and reports on all the existing objects.

Don't declare variables As New. They're like those birthday candles that re-ignite after you blow them out: If you use one after you've set it to Nothing, Visual Basic obligingly creates another object.

For More Information Circular references are the most difficult kind to shut down cleanly. See "Object Models."


Object Models

Once you've defined a class by creating a class module and giving it properties and methods, you can create any number of objects from that class. How do you keep track of the objects you create?

The simplest way to keep track of objects is to declare an object variable for each object you plan to create. Of course, this places a limit on the number of objects you can create.

You can keep multiple object references in an array or a collection, as discussed in "Creating Arrays of Objects" and "Creating Collections of Objects" earlier in this chapter.

In the beginning, you'll probably locate object variables, arrays, and collections in forms or standard modules, as you do with ordinary variables. As you add more classes, though, you'll probably discover that the objects you're using have clear relationships to each other.

Object Models Express Containment Relationships

Object models give structure to an object-based program. By defining the relationships between the objects you use in your program, an object model organizes your objects in a way that makes programming easier.

Typically, an object model expresses the fact that some objects are "bigger," or more important than others — these objects can be thought of as containing other objects, or as being made up of other objects.

For example, you might create a SmallBusiness object as the core of your program. You might want the SmallBusiness object to have other types of objects associated with it, such as Employee objects and Customer objects. You would probably also want it to contain a Product object. An object model for this program is shown in Figure 9.11.

Figure 9.11 An object model

You can define four class modules, named SmallBusiness, Employee, Customer, and Product, and give them each appropriate properties and methods, but how do you make the connections between objects? You have two tools for this purpose: Object properties and the Collection object. The following code fragment shows one way to implement the hierarchy in Figure 9.11.

' Code for the Declarations section of the
' SmallBusiness class module.
Public Name As String
Public Product As New Product
Public Employees As New Collection
Public Customers As New Collection

The first time you refer to the Product property, the object will be created, because it was declared As New. For example, the following code might create and set the name and price of the SmallBusiness object's Product object.

' Code for a standard module.
Public sbMain As New SmallBusiness
Sub Main
   sbMain.Name = "Velociraptor Enterprises, Inc."
   ' The first time the Product variable is used in
   ' code, the Product object is created.
   sbMain.Product.Name = "Inflatable Velociraptor"
   sbMain.Product.Price = 1.98
   .
   .   ' Code to initialize and show main form.
   .
End Sub

Note Implementing an object property with public variables is sloppy. You could inadvertently destroy the Product object by setting the property to Nothing somewhere in your code. It's better to create object properties as read-only properties, as shown in the following code fragment.

' Code for a more robust object property. Storage for
' the property is private, so it can't be set to
' Nothing from outside the object.
Private mProduct As New Product

Property Get Product() As Product
   ' The first time this property is called, mProduct
   ' contains Nothing, so Visual Basic will create a
   ' Product object.
   Set Product = mProduct
End If

One-to-Many Object Relationships

Object properties work well when the relationship between objects is one-to-one. It frequently happens, however, that an object of one type contains a number of objects of another type. In the SmallBusiness object model, the Employees property is implemented as a Collection object, so that the SmallBusiness object can contain multiple Employee objects. The following code fragment shows how new Employee objects might be added to this collection.

Public Function NewEmployee(Name, Salary, HireDate, _
ID) As Employee
   Dim empNew As New Employee
   empNew.Name = Name      ' Implicit object creation.
   empNew.Salary = Salary
   empNew.HireDate = HireDate
   ' Add to the collection, using the ID as a key.
   sbMain.Employees.Add empNew, CStr(ID)
   ' Return a reference to the new Employee.
   Set NewEmployee = empNew
End Function

The NewEmployee function can be called as many times as necessary to create employees for the business represented by the SmallBusiness object. The existing employees can be listed at any time by iterating over the Employees collection.

Note Once again, this is not a very robust implementation. Better practice is to create your own collection classes, and expose them as read-only properties. This is discussed in "Creating Your Own Collection Classes."

Tip The Class Builder utility, included in the Professional and Enterprise editions of Visual Basic, can generate much of the code you need to implement an object model. Class Builder creates robust object properties and collection classes, and allows you to rearrange your model easily.

Parent Properties

When you have a reference to an object, you can get to the objects it contains by using its object properties and collections. It's also very useful to be able to navigate up the hierarchy, to get to the object that contains the object you have a reference to.

Navigating upward is usually done with Parent properties. The Parent property returns a reference to the object's container. For a discussion of object model navigation, see "Navigating Object Models" in "Programming with Components."

You can find an example of a Parent property in "Adding Properties to Classes" earlier in this chapter.

Tip When you assign a Parent property to an object in a collection, don't use a reference to the Collection object. The real parent of the object is the object that contains the collection. If the Parent property points to the collection, you'll have to use two levels of indirection to get to the real parent — that is, obj.Parent.Parent instead of obj.Parent.

Parent Properties, Circular References, and Object Teardown

One of the biggest problems with Parent properties is that they create circular references. The "larger" object has a reference to the object it contains, and the contained object has a reference through its Parent property, creating a loop as shown in Figure 9.12.

Figure 9.12 A case of circular references

What's wrong with this picture? The way you get rid of objects when you're done with them is to release all references to them. Assuming the reference to the SmallBusiness object is in a variable named sbMain, as earlier in this topic, you might write the following code:

Set sbMain = Nothing

Unfortunately, there's still a reference to the SmallBusiness object — in fact, there may be many references, because each Employee object's Parent property will hold a reference to the SmallBusiness object.

Since the SmallBusiness object's Employees collection holds a reference to each Employee object, none of the objects ever get destroyed.

TearDown Methods

One solution is to give the SmallBusiness object a TearDown method. This could set all of the SmallBusiness object's object properties to Nothing, and also set all the Collection objects (Employees, Customers) to Nothing.

When a Collection object is destroyed, Visual Basic sets all the object references it was holding to Nothing. If there are no other references to the Employee and Customer objects that were contained in the Employees and Customers collections, they'll be destroyed.

Of course, if the Employee object is made up of finer objects, it will have the same circular reference problem its parent does. In that case, you'll have to give the Employee class a TearDown method. Instead of just setting the Employees Collection object to Nothing, the SmallBusiness object will first have to iterate through the collection, calling the TearDown method of each Employee object.

It's Not Over Yet

Even then, not all the objects may be destroyed. If there are variables anywhere in your program that still contain references to the SmallBusiness object, or to any of the objects it contains, those objects won't be destroyed. Part of the cleanup for your program must be to ensure that all object variables everywhere are set to Nothing.

To test whether this is happening, you may want to add some debugging code to your objects. For example, you can add the following code to a standard module:

' Global debug collection
Public gcolDebug As New Collection

' Global function to give each object a unique ID.
Public Function DebugSerial() As Long
   Static lngSerial As Long
   lngSerial = lngSerial + 1
   DebugSerial = lngSerial
End Function

In each class module, you can put code similar to the following. Each class provides its own name where "Product" appears.

' Storage for the debug ID.
Private mlngDebugID As Long

Property Get DebugID() As Long
   DebugID = mlngDebugID
End Property

Private Sub Class_Initialize()
   mlngDebugID = DebugSerial
   ' Add a string entry to the global collection.
   gcolDebug.Add "Product Initialize; DebugID=" _
   & DebugID, CStr(DebugID)
End Sub

Private Sub Class_Terminate()
   ' Remove the string entry, so you know the object
   ' isn't around any more.
   gcolDebug.Remove CStr(DebugID)
End Sub

As each object is created, it places a string in the global collection; as it's destroyed it removes the string. At any time, you iterate over the global collection to see what objects haven't been destroyed.

For More Information Object models assume new importance, and a different set of problems, when you use the Professional or Enterprise Edition of Visual Basic to create ActiveX components. See "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.


Creating Your Own Collection Classes

There are three general approaches you can take to implementing object containment using collections. Consider the Employees collection of the SmallBusiness object discussed in "Object Models." To implement this collection you might:

  • In the SmallBusiness class module, declare an Employees variable As Collection, and make it Public. This is the cheap solution.

  • In the SmallBusiness class module, declare an mcolEmployees variable As Collection, and make it Private. Give the SmallBusiness object a set of methods for adding and deleting objects. This is the least object-oriented of the three designs.

  • Implement your own collection class, by creating a collection class module named Employees, as described later in this chapter. Give the SmallBusiness object a read-only property of the Employees class.

The strategies are listed in order of increasing robustness. They could be characterized as the house of straw, house of sticks, and house of bricks approaches.

  • Public Collection Example: The House of Straw The Collection object's very flexibility betrays it — you can put anything into a Collection, including the KitchenSink object.

  • Private Collection Example: The House of Sticks You can gain some robustness by making the Collection object private, but you lose the ability to use For Each … Next with the collection.

  • Creating Your Own Collection Class: The House of Bricks Creating your own collection class gives you the robustness of encapsulation, and as a bonus you get back the ability to use For Each … Next.

  • The Benefits of Good Object-Oriented Design Collection classes lead to cleaner, safer code.


Public Collection Example: The House of Straw

To create the example, open a new project and insert two class modules. Draw five command buttons, a list box, two text boxes, and two labels on the form, as shown in Figure 9.13.

Figure 9.13 Employees collection example

The following table lists the property values you need to set for this example.

Object | Property | Setting
Class module | Name | Employee
Class module | Name | SmallBusiness
Form | Caption | Employees Collection
First command button | Caption Name | Add cmdAddEmployee
Second command button | Caption Name | Delete cmdDeleteEmployee
Third command button | Caption Name | Refresh List cmdListEmployees
Fourth command button | Caption Name | Trouble cmdTrouble
Fifth command button | Caption Name | Close cmdClose
First label control | Caption | Name
Second label control | Caption | Salary
First text box | Name Text | txtName (blank)
Second text box | Name Text | txtSalary (blank)
List Box | Name | lstEmployees

In the Employee class module, add the following declarations and property procedures:

Option Explicit
' Properties of the Employee class.
Public Name As String
Public Salary As Long

' Private data for the write-once ID property.
Private mstrID As String

Property Get ID() As String
   ID = mstrID
End Property

' The first time the ID property is set, the static
' Boolean is also set.  Subsequent calls do nothing.
' (It would be better to raise an error, instead.)
Property Let ID(strNew As String)
   Static blnAlreadySet As Boolean
   If Not blnAlreadySet Then
      blnAlreadySet = True
      mstrID = strNew
   End If
End Property

The ID property is the key for retrieving or deleting an Employee object from the collection, so it must be set once and never changed. This is accomplished with a Static Boolean variable that is set to True the first time the property is set. The property can always be read, because there is a Property Get.

In the SmallBusiness class module, add the following declaration. The collection object will be created the first time the Employees variable is referred to in code.

Option Explicit
Public Employees As New Collection

The Form Does All the Work

All of the remaining code goes into the form module. Add the following declaration in the Declarations section.

Option Explicit
Public sbMain As New SmallBusiness

The code in the cmdEmployeeAdd_Click event adds a member to the collection.

Private Sub cmdEmployeeAdd_Click()
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' Using With makes your code faster and more
   ' concise (.ID vs. empNew.ID).
   With empNew
      ' Generate a unique ID for the new employee.
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = txtName.Text
      .Salary = CDbl(txtSalary.Text)
      ' Add the Employee object reference to the
      ' collection, using the ID property as the key.
      sbMain.Employees.Add empNew, .ID
   End With
   txtName.Text = ""
   txtSalary.Text = ""
   ' Click the Refresh List button.
   cmdListEmployees.Value = True
End Sub

The code in the cmdListEmployees_Click event procedure uses a For Each ... Next statement to add all the employee information to the ListBox control.

Private Sub cmdListEmployees_Click()
   Dim emp As Employee
   lstEmployees.Clear
   For Each emp In sbMain.Employees
      lstEmployees.AddItem emp.ID & ", " & emp.Name _
      & ", " & emp.Salary
   Next
End Sub

The cmdEmployeeDelete_Click event uses the Collection object's Remove method to delete the collection member currently selected in the ListBox control.

Private Sub cmdEmployeeDelete_Click()
   ' Check to make sure there's an employee selected.
   If lstEmployees.ListIndex > -1 Then
      ' The first six characters are the ID.
      sbMain.Employees.Remove _
      Left(lstEmployees.Text, 6)
   End If
   ' Click the Refresh List button.
   cmdListEmployees.Value = True
End Sub

Add the following code to the Trouble button.

Private Sub cmdTrouble_Click()
   ' Say what!?
   sbMain.Employees.Add Me
End Sub

The cmdClose_Click event closes the application. When you close projects that use objects, do so by unloading all the forms, to ensure that any Terminate event procedures in your class modules will get executed. By contrast, using the End statement stops a program abruptly, without executing Terminate events.

Private Sub cmdClose_Click()
   Unload Me
End Sub

To add employees in the example, run the application, enter values in the two text boxes, and then choose the Add button. Add a few employees, and then experiment with the delete and list buttons.

Robust as a Straw House

This simple implementation is not very robust. Because the Employees property is just a public Collection object, you could inadvertently access it from anywhere in your program. Furthermore, the Add method of the Collection object doesn't do any type checking. For example, the code in the Trouble button's Click event blithely inserts an object reference to the form into the collection of employees.

Click the Trouble button, and notice that no error occurs. Now click the Refresh List button. When the For Each ... Next loop encounters the unexpected object type, it causes error 13, Type mismatch.

This is an example of the kind of error you're exposed to when you build an object model with public Collection objects. Objects can be added from anywhere in your project, and there's no guarantee that they'll be properly initialized. If a programmer clones the code to add an employee, and the original code is later changed, the resulting errors can be very difficult to track down.

For More Information The example begun in this topic is continued in "Private Collection Example: The House of Sticks."


Private Collection Example: The House of Sticks

This topic continues the code example begun in "Public Collection Example: The House of Straw." You may want to read that topic before beginning this one.

A somewhat more robust way to link Employee objects with the SmallBusiness object is to make the Collection object private. For this example, you'll reuse the form and most of the code from the "Public Collection" example.

The Employee class module is unchanged. The SmallBusiness class module, however, gets a complete facelift. Replace the declaration of the public Collection object with the following declaration, and add the Sub and Function procedures described in the following paragraphs.

Option Explicit
Private mcolEmployees As New Collection

As before, the code that adds an employee does most of the work. (You can take the block of code between the dotted lines out of the cmdEmployeeAdd_Click event procedure in the previous example.)

The important change is that the Add method of the Collection object can no longer be called from any module in your program, because mcolEmployees is Private. You can only add an Employee object using the EmployeeAdd method, which correctly initializes the new object:

' Method of the SmallBusiness class.
Public Function EmployeeAdd(ByVal Name As String, _
ByVal Salary As Double) As Employee
   ' - - - - - - - - - - - - - - - -
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' Using With makes your code faster and more
   ' concise (.ID vs. empNew.ID).
   With empNew
      ' Generate a unique ID for the new employee.
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = Name
      .Salary = Salary
      ' Add the Employee object reference to the
      ' collection, using the ID property as the key.
      ' - - - - - - - - - - - - - - - -
      mcolEmployees.Add empNew, .ID
   End With
   ' Return a reference to the new Employee.
   Set EmployeeAdd = empNew
End Function

The EmployeeAdd method returns a reference to the newly added Employee object. This is a good practice, because as soon as you create an object you will most likely want to do something with it.

The EmployeeCount, EmployeeDelete, and Employees methods delegate to the corresponding methods of the Collection object. Delegation means that the Collection object does all the work.

' Methods of the SmallBusiness class.
Public Function EmployeeCount() As Long
   EmployeeCount = mcolEmployees.Count
End Function

Public Sub EmployeeDelete(ByVal Index As Variant)
   mcolEmployees.Remove Index
End Sub

Public Function Employees(ByVal Index As Variant) _
As Employee
   Set Employees = mcolEmployees.Item(Index)
End Function

Note You can add extra functionality to these methods. For example, you can raise your own errors if an index is invalid.

The last method is Trouble. This method attempts to add an uninitialized Employee object to the collection. Any guesses what will happen?

' Method of the SmallBusiness class.
Public Sub Trouble()
   Dim x As New Employee
   mcolEmployees.Add x
End Sub

Changes to the Form

You'll have to make a few changes to the form module. You can use the same module-level declarations used for the previous example, and the Click event for the Close button is the same, but the other event procedures have changed — the Add button code is much shorter, while the code for the Delete and List Employees buttons have changed in small but significant ways:

Private Sub cmdEmployeeAdd_Click()
   sbMain.EmployeeAdd txtName.Text, txtSalary.Text
   txtName.Text = ""
   txtSalary.Text = ""
   cmdListEmployees.Value = True
End Sub

Private Sub cmdEmployeeDelete_Click()
   ' Check to make sure there's an employee selected.
   If lstEmployees.ListIndex > -1 Then
      ' The first six characters are the ID.
      sbMain.EmployeeDelete Left(lstEmployees.Text, 6)
   End If
   cmdListEmployees.Value = True
End Sub

Private Sub cmdListEmployees_Click()
   Dim lngCt As Long
   lstEmployees.Clear
   For lngCt = 1 To sbMain.EmployeeCount
      With sbMain.Employees(lngCt)
         lstEmployees.AddItem .ID & ", " & .Name _
         & ", " & .Salary
      End With
   Next
End Sub

But what's all this extra code in cmdListEmployees_Click? Unfortunately, in pursuit of robustness you've given up the ability to use For Each ... Next to iterate through the items in the collection, because the Collection object is now declared Private. If you try to code the following, you'll just get an error:

' Won't work, because Employees isn't really a
' collection.
For Each emp In sbMain.Employees

Fortunately, the EmployeeCount method can be used to delimit the iteration range.

The Trouble button changes a little, too, but it's still, well, Trouble.

Private Sub cmdTrouble_Click()
   sbMain.Trouble
End Sub

Run the project and experiment with the Add, Delete, and Refresh List buttons. Everything works just like before.

When you click the Trouble button, once again no error is generated. However, if you now click the Refresh List button, you can see that the uninitialized Employee object has somehow been added to the collection.

How can this be? By making the Collection object private, you protect it from all the code in your program that's outside the SmallBusiness object, but not from the code inside. The SmallBusiness object may be large and complex, with a great deal of code in it. For example, it will very likely have methods like CustomerAdd, ProductAdd, and so on.

A coding error, or the creation of a duplicate of the EmployeeAdd method, can still result in erroneous data — even invalid objects — being inserted into the collection, because the private variable is visible throughout the class module.

For More Information This example is continued in "Creating Your Own Collection Class: The House of Bricks."


Creating Your Own Collection Class: The House of Bricks

This topic continues the code example begun in "Public Collection Example: The House of Straw" and "Private Collection Example: The House of Sticks." You may want to read those topics before beginning this one.

The most robust way to implement a collection is by making it a class module. In contrast to the preceding examples, moving all the code for object creation into the collection class follows good object design principles.

This example uses the same form and the same Employee class module as the previous examples. Insert a new class module, and set its Name property to "Employees." Insert the following declarations and code into the new class module.

Option Explicit
Private mcolEmployees As New Collection

The Add, Count, and Delete methods of the Employees class are essentially the same as those of the old SmallBusiness class. You can simply remove them from the SmallBusiness class module, paste them into the Employees class module, and change their names.

The names can change because it's no longer necessary to distinguish EmployeeAdd from, say, CustomerAdd. Each collection class you implement has its own Add method.

' Methods of the Employees collection class.
Public Function Add(ByVal Name As String, _
ByVal Salary As Double) As Employee
   Dim empNew As New Employee
   Static intEmpNum As Integer
   ' Using With makes your code faster and more
   ' concise (.ID vs. empNew.ID).
   With empNew
      ' Generate a unique ID for the new employee.
      intEmpNum = intEmpNum + 1
      .ID = "E" & Format$(intEmpNum, "00000")
      .Name = Name
      .Salary = Salary
      ' Add the Employee object reference to the
      ' collection, using the ID property as the key.
      mcolEmployees.Add empNew, .ID
   End With
   ' Return a reference to the new Employee.
   Set Add = empNew
End Function

Public Function Count() As Long
   Count = mcolEmployees.Count
End Function

Public Sub Delete(ByVal Index As Variant)
   mcolEmployees.Remove Index
End Sub

The Employees method of the SmallBusiness object becomes the Item method of the collection class. It still delegates to the Collection object, in order to retrieve members by index or by key.

' Method of the Employees collection class.
Public Function Item(ByVal Index As Variant) _
As Employee
   Set Item = mcolEmployees.Item(Index)
End Function

There's a nice touch you can add here. By making Item the default method of the Employees class, you gain the ability to code Employees("E00001"), just as you could with the Collection object.

To make Item the default property

  1. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box. In Name box, select the Item method.

  2. Click Advanced to show the advanced features. In the Procedure ID box, select (Default) to make the Item method the default. Click OK.

    Note A class can have only one default member (property or method).

Enabling For Each … Next

Along with robustness, you get For Each … Next back. Once again you can delegate all the work to the Collection object, by adding the following method:

' NewEnum must return the IUnknown interface of a
' collection's enumerator.
Public Function NewEnum() As IUnknown
   Set NewEnum = mcolEmployees.[_NewEnum]
End Function

The important thing you're delegating to the Collection object is its enumerator. An enumerator is a small object that knows how to iterate through the items in a collection. You can't write an enumerator object with Visual Basic, but because the Employees class is based on a Collection object, you can return the Collection object's enumerator — which naturally enough knows how to enumerate the items the Collection object is holding.

The square brackets around the Collection object's _NewEnum method are necessary because of the leading underscore in the method name. This leading underscore is a convention indicating that the method is hidden in the type library. You can't name your method _NewEnum, but you can hide it in the type library and give it the procedure ID that For Each … Next requires.

To hide the NewEnum method and give it the necessary procedure ID

  1. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box. In Name box, select the NewEnum method.

  2. Click Advanced to show the advanced features. Check Hide this member to make NewEnum hidden in the type library.

  3. In the Procedure ID box, type –4 (minus four) to give NewEnum the procedure ID required by For Each … Next. Click OK.

    Important In order for your collection classes to work with For Each … Next, you must provide a hidden NewEnum method with the correct procedure ID.

Not Much Left of the SmallBusiness Class

The SmallBusiness class will have considerably less code in it now. To replace the Collection object and all the methods you removed, there's a new declaration and a read-only property:

Option Explicit
Private mEmployees As New Employees

Public Property Get Employees() As Employees
   Set Employees = mEmployees
End If

This deserves a word of explanation. Suppose for a moment that you left out the Property Get, and simply declared Public Employees As New Employees.

Everything would work fine as long as nobody made any mistakes, but what if you accidentally coded Set sbMain.Employees = Nothing? That's right, the Employees collection would be destroyed. By making Employees a read-only property, you avert that possibility.

Changes to the Form

The code for the form module is very similar to the preceding example. You can use the same module-level declarations, and the Click event for the Close button is the same.

The only change in most of the event procedures is replacing the old methods of the SmallBusiness class with the new methods of the Employees collection object:

Private Sub cmdEmployeeAdd_Click()
   sbMain.Employees.Add txtName.Text, txtSalary.Text
   txtName.Text = ""
   txtSalary.Text = ""
   cmdListEmployees.Value = True
End Sub

Private Sub cmdEmployeeDelete_Click()
   ' Check to make sure there's an employee selected.
   If lstEmployees.ListIndex > -1 Then
      ' The first six characters are the ID.
      sbMain.Employees.Delete _
      Left(lstEmployees.Text, 6)
   End If
   cmdListEmployees.Value = True
End Sub

Private Sub cmdListEmployees_Click()
   Dim emp As Employee
   lstEmployees.Clear
   For Each emp In sbMain.Employees
      lstEmployees.AddItem emp.ID & ", " & emp.Name _
      & ", " & emp.Salary
   Next
End Sub

Notice that you can use For Each … Next again to list the employees.

Run the project and verify that everything works. There's no code for the Trouble button this time, because encapsulation has banished trouble.

For More Information Read "The Visual Basic Collection Object" and "Collections in Visual Basic" for background on collections.

The Class Builder utility included in the Professional and Enterprise editions will create collection classes for you.

The lessons of the House of Straw, House of Sticks, and House of Bricks examples are summed up in "The Benefits of Good Object-Oriented Design."


The Benefits of Good Object-Oriented Design

This topic summarizes the results of the code example begun in "Public Collection Example: The House of Straw," and continued in "Private Collection Example: The House of Sticks" and "Creating Your Own Collection Class: The House of Bricks." You may want to read those topics before beginning this one.

Creating the Employees collection class results in a very clean, modular coding style. All the code for the collection is in the collection class (encapsulation), reducing the size of the SmallBusiness class module. If collections of Employee objects appear in more than one place in your object hierarchy, reusing the collection class requires no duplication of code.

Enhancing Collection Classes

You can implement additional methods and properties for your collection classes. For example, you could implement Copy and Move methods, or a read-only Parent property that contains a reference to the SmallBusiness object.

You could also add an event. For example, every time the Add or Remove method changed the number of items in your collection, you could raise a CountChanged event.

Robustness, Robustness, Robustness

You don't always have to implement collections in the most robust way possible. However, one of the benefits of programming with objects is code reuse; it's much easier to reuse objects than to copy source code, and it's much safer to use robust, encapsulated code.

A wise man once said, "If you want to write really robust code, you have to assume that really bad things will happen."

Collection Classes and Component Software

If you're using the Professional or Enterprise Edition of Visual Basic, you can turn your project into an ActiveX component, so that other programmers in your organization can use the objects you've created.

Steps to Implement a Collection Class

The following list summarizes the steps required to create a collection class.

  1. Add a class module to your project, and give it a name — usually the plural of the name of the object the collection class will contain. (See "Naming Properties, Methods, and Events" earlier in this chapter.)

  2. Add a private variable to contain a reference to the Collection object your properties and methods will delegate to.

  3. In the Class_Initialize event procedure, create the Collection object. (If you want to defer creation of this object until it's needed, you can declare the private variable in step 2 As New Collection. This adds a small amount of overhead each time the Collection is accessed.)

  4. Add a Count property and Add, Item, and Remove methods to your class module; in each case, delegate to the private Collection by calling its corresponding member.

  5. When you implement the Add method, you can override the behavior of the Collection object's undiscriminating Add method by accepting only objects of one type. You can even make it impossible to add externally created objects to your collection, so that your Add method completely controls the creation and initialization of objects.

  6. Use the Procedure Attributes dialog box to make the Item method the default for your collection class.

  7. Add a NewEnum method, as shown below. Use the Procedure Attributes dialog box to mark it as hidden, and to give it a Procedure ID of –4 so that it will work with For Each … Next.
    Public Function NewEnum() As IUnknown
   Set NewEnum = mcol.[_NewEnum]
End Function

    Note The code above assumes that the private variable in step 2 is named mcol.

  8. Add custom properties, methods, and events to the collection class.

Note The Class Builder utility, included in the Professional and Enterprise editions of Visual Basic, will create collection classes for you. You can customize the resulting source code.

For More Information You can read more about software components in Creating ActiveX Components, in the Component Tools Guide.


ActiveX Designers

A designer provides a visual design window in the Visual Basic development environment. You can use this window to design new classes visually. Visual Basic has built-in designers for forms and — in the Professional and Enterprise editions — ActiveX controls and documents.

Objects created from the classes you design in this fashion have separate design-time and run-time behavior and appearance, although many objects — such as forms and controls — look very similar in the two modes.

In addition to its built-in designers, Visual Basic allows third parties to develop designers for use in the Visual Basic development environment. These ActiveX designers work just like the built-in designers in Visual Basic, making them easy to learn and use.

  • What Are ActiveX Designers? Compares and contrasts ActiveX designers with Visual Basic's built-in designers.

  • Adding an ActiveX Designer to the Project Menu Before you can use an ActiveX designer, you have to add it to your Project menu.

  • Inserting a New Instance of an ActiveX Designer How to use ActiveX designers in your project.


What Are ActiveX Designers?

ActiveX designers can provide visual interfaces for tasks that otherwise might require a great deal of code. For example, the UserConnection designer included in the Enterprise Edition of Visual Basic provides visual tools for defining complex database queries. At run time, these queries can be invoked with very little code.

Similarities between ActiveX Designers and Built-in Designers

ActiveX designers are like form designers in the following ways:

  • ActiveX designers produce classes from which you can create objects. These classes appear in the Project window, just like form classes.

  • Each class you create with an ActiveX designer has its own code module, in which you can write code for the event procedures provided by the designer.

  • You can customize a class, by adding your own properties, methods, and events to the ones provided by the ActiveX designer.

  • The objects created from classes you design can have different characteristics at design time and run time.

  • An ActiveX designer's design window is fully integrated into the development environment. It can be sized and arranged just like built-in design windows.

  • You can add as many instances of an ActiveX designer to your project as you need, just as you can add as many form designers as you need.

Figure 9.14 compares the built-in Visual Basic form designer with the UserConnection Designer, an ActiveX designer included in the Enterprise Edition of Visual Basic.

Figure 9.14 An ActiveX designer and a built-in Visual Basic designer

Comparing ActiveX Designer Classes to other Visually Designed Classes

ActiveX designers are extremely flexible. Some, like the UserConnection designer, create classes whose run-time instances are programmable, but not visible. Others, like the Microsoft Forms designer used by Microsoft Office, produce visible objects similar to Visual Basic forms.

ActiveX designers that have visible run-time components may be able to host ActiveX controls. In effect, they become alternate forms packages, which can be used in addition to Visual Basic's native forms.

The following list compares classes produced with ActiveX designers to those produced with built-in Visual Basic designers.

  • If an object created from an ActiveX designer class is visible at run time, it has its own window. It is not contained within another form, as ActiveX controls are.

  • Like form classes, but unlike ActiveX controls, the classes produced by ActiveX designers are private classes. If you're using the Professional or Enterprise Edition of Visual Basic to create ActiveX components, you cannot declare public methods that use these classes as argument types or return types.

    For example, the following method declarations produce compile-time errors if they appear in a public class:

    Public Function A() As UseConnection1       'Error
Public Sub B(CallBack As UseConnection1)    'Error

    Caution Although it is possible to pass references to private objects outside your project, by declaring return values As Object, this is very bad practice, and may destabilize your program. For more information, see Creating ActiveX Components in the Component Tools Guide.

Using ActiveX Designer Objects at Run Time

Like the built-in form designer, ActiveX designers are available only in the development environment. Once you make your project into an executable, it only uses the ActiveX designer's run-time .dll. This may be much smaller than the design-time .dll, because it doesn't include the visual design tool. Figure 9.15 illustrates this concept.

Figure 9.15 Designer components in memory

As noted earlier, ActiveX designers may produce classes whose objects are not visible at run time. The UserConnection designer shown in Figure 9.14 is an example. The UserConnection designer produces classes whose objects manage connections to SQL databases at run time. There is no reason for these objects to be visible at run time.

To use a class created with the UserConnection designer, declare a variable of the class type and create an instance of the class. For example, if you added a UserConnection designer and set its Name property to GeneralLedger, you could create a GeneralLedger object as shown in the following code fragment:

' Global variable in a standard module, to keep a
' reference to the GeneralLedger object.
Public gGeneralLedger As GeneralLedger

' Code in a Form module to create the GeneralLedger
' object and establish a database connection.
Private Sub Command1_Click()
   Set gGeneralLedger = New gGeneralLedger
   gGeneralLedger.EstablishConnection
   ' (Code that uses the object.)
End Sub

Creating ActiveX Designers

You can use the ActiveX Designer Software Development Kit to create new ActiveX designers for use with Visual Basic. The SDK includes full instructions and sample code. You can find it on the Microsoft Development Network under the heading "SDK Documentation."

Note The ActiveX Designer SDK requires a C++ compiler, such as Microsoft Visual C++. ActiveX designers cannot be written using Visual Basic.

For More Information Procedures for incorporating ActiveX designers in your project are provided in "Adding an ActiveX Designer to the Project Menu" and "Inserting a New Instance of an ActiveX Designer."


Adding an ActiveX Designer to the Project Menu

After you install a new ActiveX designer, using the Setup program supplied by the vendor, you must make the designer available to your projects by adding it to the project menu.

Installing the ActiveX designer will register it in the Windows Registry, under the appropriate component category. It will then be available from the Designers tab of the Components dialog box.

To add an ActiveX designer to the Project menu

  1. On the Project menu, click Components to open the Components dialog box.

  2. Click the Designers tab and select the designer you want to use, as shown in the following figure. Click OK.

Note The Microsoft Forms designer is included in all versions of Visual Basic, to allow forms created in Microsoft Office applications to be ported easily. Like all designers, the Microsoft Forms designer has its own run-time .dll. Using this designer in a Visual Basic project will therefore increase the memory requirements of the resulting executable.

For More Information To add a designer to your project, see "Inserting a New Instance of an ActiveX Designer."


Inserting a New Instance of an ActiveX Designer

Once you've used the Components dialog box to add a designer to the Project menu, you can insert as many instances of the designer as you need.

To insert an instance of an ActiveX Designer

  • On the Project menu, click Add designer (where designer is the name of the designer) to display a list of installed designers. Pick the designer you want from the list.

    Note If more than four kinds of ActiveX designers are loaded, the later ones will be displayed from the More ActiveX Designers submenu on the Project menu.

Once you've added an instance of an ActiveX designer to your project, you can use its visual interface to design a class. You can click the Code button on the Project window to open the code module for the designer, and add code to event procedures. You can further customize the class by adding by adding your own properties, methods, and events, just as you would in a Visual Basic class module.

ActiveX Designers and SourceCode Control

When using ActiveX designers with a SourceCode control package such as Visual SourceSafe, the behavior may differ from that of built-in designers. When attempting to edit a file created by a designer, you may not be prompted to check out the file before editing. Additionally, some designers write files out to the file system; SourceCode control may not be aware of these files.

To add a designer-authored file to SourceCode control:

  1. Choose Add File from the Project menu.

  2. Select the file you wish to add from the Add File dialog box, then check the Add As Related Document checkbox and click Open.

    This will add the file to your project as a related document. The SourceCode control package will prompt you to add the file (this may occur when you add the file or when you next save the project, depending on the SourceCode control package).

Note Files created with Visual Basic's HTML Page Designer can optionally store the HTML that it writes with the designer (.dsr) file. If you wish to add these files to SourceCode control, you need to check the Save HTML as part of the VB Project checkbox on the Page Designer's property page.

For More Information Designers are introduced in "ActiveX Designers." To add a designer to the Project menu, see "Adding an ActiveX Designer to the Project Menu."


Programming with Components

Do you sometimes need to provide the same analysis and calculation capabilities as Microsoft Excel in your Visual Basic application? Or, perhaps you'd like to format a document using Microsoft Word formatting tools, or store and manage data using the Microsoft Jet database engine. Even better, would you like to be able to create or buy standard components, then use them in multiple applications without having to modify them?

All this and more can be accomplished by building your applications using ActiveX components. An ActiveX component is a reusable piece of programming code and data made up of one or more objects created using ActiveX technology. Your applications can use existing components, such as those included in Microsoft Office applications, code components, ActiveX documents, or ActiveX controls (formerly called OLE controls) provided by a variety of vendors. Or, if you have the Visual Basic, Professional or Enterprise Edition, you can create your own ActiveX controls.

For components that support object linking and embedding, you can insert objects into your application without writing any code by using the component's visual interface. You can insert an OLE-enabled object into your application by using the OLE container control or by adding the object's class to the Toolbox.

To fully understand ActiveX components, you should first be familiar with how to work with classes, objects, properties, and methods, which are explained in "Programming with Objects."

Topics

Types of ActiveX Components

Introducing the different types of ActiveX components.
In-Process and Out-of-Process Servers

The difference between in-process and out-of-process components.
Working with ActiveX Components

The basics of working with most objects provided by ActiveX components.
Creating a Reference to an Object

How to declare and set object variables for objects provided by components.
Using an Object's Properties, Methods, and Events

The basics of manipulating an object's properties, methods, and events.
Releasing an ActiveX Component

Clearing an object variable so it can be released from memory.
Navigating Object Models

Working with objects in large components that supply an object hierarchy.
Handling Run-Time Errors in ActiveX Components

Trapping errors when you work with ActiveX components.
Handling Requests Pending to an ActiveX Component

Handling requests to an ActiveX component that may not be finished immediately.
Using an ActiveX Component's Visual Interface

How to link or embed component objects in your application without writing code.

Sample applications

Geofacts.vbp and Olecont.vbp

Many of the concepts in this chapter are demonstrated in the sample applications Geofacts.vbp and Olecont.vbp. The sample applications are listed in the Samples directory.


Types of ActiveX Components

ActiveX components give you the power to put together sophisticated applications from pieces that already exist. Your Visual Basic applications can include several types of ActiveX components:

  • Applications that support ActiveX technology, such as Microsoft Excel, Microsoft Word, and Microsoft Access, provide objects that you can manipulate programmatically from within your Visual Basic application. For example, you can use the properties, methods, and events of a Microsoft Excel spreadsheet, Microsoft Word document, or Microsoft Access database in your application.

  • Code components provide libraries of programmable objects. For example, a code component could include a library of specialized financial functions for spreadsheet users, or user-interface elements, such as dialog boxes, that are common to multiple applications. Unlike an object in an ActiveX-enabled application, an object in a code component can run in the same process as your application, allowing faster access to the object.

  • You can add features without having to create them yourself by using ActiveX controls as components. ActiveX controls are available from a variety of vendors to provide many specialized features, such as displaying a calendar on a form or reading data in a particular format.

  • ActiveX documents let you create interactive Internet applications. You can create forms that can be contained within Internet Explorer. ActiveX documents can show message boxes and secondary forms and contain ActiveX controls. ActiveX documents can also function as code components. For a step-by-step introduction to ActiveX documents, see "Creating an ActiveX Document" in the Component Tools Guide, available in the Professional and Enterprise editions.

Note In addition to Internet applications using ActiveX documents, you can create both client-based and server-based Internet applications using a combination of Visual Basic code and HTML pages. See "Introduction to Internet Applications" in "Building Internet Applications" in the Components Tools Guide for more information on your options for creating Internet and intranet applications.

Some ActiveX components run in the same process as your application, while others run in a separate process. For more information, see "In-Process and Out-of-Process Servers."

In addition to components in existing ActiveX-enabled applications, code component libraries, ActiveX controls, and ActiveX documents, you can create your own components. For more information on creating your own ActiveX components, see "Creating ActiveX Components" in the Component Tools Guide, available in the Professional and Enterprise editions.


In-Process and Out-of-Process Servers

ActiveX components interact with your application — and with each other — through a client/server relationship. The client is the application code or component that uses the features of a component. The server is the component and its associated objects. For example, suppose your application uses an ActiveX control to provide a standard Employee form for multiple applications in your company. The ActiveX control that provides the Employee form is the server; the applications that use the control are its clients.

Depending on how an ActiveX component has been implemented, it may run in the same process as its client applications, or in a different process. For example, if your application uses a component that is part of an ActiveX-enabled application, it runs in a separate process. If the component has been implemented as a programmable object in a dynamic-link library (.dll file), it runs in the same process as your application.

In general, if an ActiveX component has been implemented as part of an executable file (.exe file), it is an out-of-process server and runs in its own process. If it has been implemented as a dynamic-link library, it is an in-process server and runs in the same process as the client application. Applications that use in-process servers usually run faster than those that use out-of-process servers because the application doesn't have to cross process boundaries to use an object's properties, methods, and events.

The following table shows how you can implement the different types of components:

Component | Server Type
ActiveX-enabled application | Out-of-process
Code component | Either in-process or out-of-process
ActiveX control | In-process
ActiveX document | Either in-process or out-of-process

Using in-process components is one way to optimize the performance of your application. Another way to optimize performance is to use early binding. For more information, see "Speeding Object References" later in this chapter.


Working with ActiveX Components

You work with object provided by ActiveX components in much the same way that you work with other objects. You assign an object reference to a variable, then write code that uses the object's methods, properties, and events. However, there are some things you need to be aware of when you work with objects provided by components.

This topic provides an overview of the top-level tasks for working with objects provided by components and an example of using objects in an ActiveX-enabled application. For details on each task, see the appropriate topic described under each task item.

To use most objects provided by ActiveX components

  1. Create a reference to the object you want to use. How you do this depends on the type of object and whether the ActiveX component supplies a type library.

    For more information See "Creating a Reference to an Object" later in this chapter.

  2. Write code using the object's methods, properties, and events.

    For more information See "Using an Object's Properties, Methods, and Events" later in this chapter.

  3. Release the object when you are finished using it.

    For more information See "Releasing an ActiveX Component" later in this chapter.

  4. Create error-handlers.

    For more information See "Handling Run-Time Errors in ActiveX Components" later in this chapter.

For example, suppose you have created a form with three text boxes (Text1, Text2, and Text3) and a command button (Command1), and added a reference in your project to the Microsoft Excel 8.0 Object Library. You can then add code to the command button's Command1_Click event procedure that uses the Microsoft Excel Formula method to add two numbers entered in Text1 and Text2, displaying the result in Text3. (To avoid a type mismatch error, you may want to remove the default text value of each text box by setting its Text property to an empty string):

Private Sub Command1_Click()
   ' Declare object variables for Microsoft Excel,
   ' application workbook, and worksheet objects.
   Dim xlApp As Excel.Application
   Dim xlBook As Excel.Workbook
   Dim xlSheet As Excel.Worksheet

   ' Assign object references to the variables. Use
   ' Add methods to create new workbook and worksheet
   ' objects.
   Set xlApp = New Excel.Application
   Set xlBook = xlApp.Workbooks.Add
   Set xlSheet = xlBook.Worksheets.Add

   ' Assign the values entered in the text boxes to
   ' Microsoft Excel cells.
   xlSheet.Cells(1, 1).Value = Text1.Text
   xlSheet.Cells(2, 1).Value = Text2.Text

   ' Use the Formula method to add the values in
   ' Microsoft Excel.
   xlSheet.Cells(3, 1).Formula = "=R1C1 + R2C1"
   Text3.Text = xlSheet.Cells(3, 1)

   ' Save the Worksheet.
   xlSheet.SaveAs "c:\Temp.xls"

   ' Close the Workbook
   xlBook.Close
   ' Close Microsoft Excel with the Quit method.
   xlApp.Quit

   ' Release the objects.
   Set xlApp = Nothing
   Set xlBook = Nothing
   Set xlSheet = Nothing
End Sub

For simplicity, this example doesn't include error handling. However, it is highly recommended that you include error handling in applications that use objects provided by ActiveX components.


Creating a Reference to an Object

Before you can use an object's properties, methods, and events in your application, you must declare an object variable, then assign an object reference to the variable. How you assign an object reference depends on two factors:

  • Whether the ActiveX component supplies a type library. An ActiveX component's type library contains definitions of all the objects the component provides, including definitions for all available methods, properties, and events. If an ActiveX component provides a type library, you need to add a reference to the type library in your Visual Basic project before you can use the library's objects.

  • Whether the object is a top-level, externally creatable object, or a dependent object. You can assign a reference to an externally created object directly, while references to dependent objects are assigned indirectly.

If an object is externally creatable, you can assign an object reference to a variable by using the New keyword, CreateObject, or GetObject in a Set statement from outside the component. If the object is a dependent object, you assign an object reference by using a method of a higher-level object in a Set statement.

In Microsoft Excel, for example, an Application object is an externally creatable object — you can assign a reference to it directly from your Visual Basic application by using the New keyword, CreateObject, or GetObject in a Set statement. A Range object, by contrast, is a dependent object — you assign a reference to it by using the Cells method of a Worksheet object in a Set statement. For more information on externally creatable and dependent objects, see "Navigating Object Models" later in this chapter.

If the object's class is included in a type library, you can make your application run faster by creating an object reference using a variable of that specific class. Otherwise, you must use a variable of the generic Object class, which results in late binding. For more information, see "Speeding Object References."

To create a reference to an object defined in a type library

  1. From the Project menu, choose References.

  2. In the References dialog box, select the name of the ActiveX component containing the objects you want to use in your application.

  3. You can use the Browse button to search for the type library file containing the object you need. Type libraries can have a .tlb or .olb file-name extension. Executable (.exe) files and dynamic link libraries (dlls) can also supply type libraries, so you can also search for files with these file-name extensions.

    If you are not sure if an application is ActiveX-enabled and supplies a type library, try adding a reference to it using the Browse button. If the reference fails, Visual Basic displays the error message, "Can't add a reference to the specified file," indicating that the type library doesn't exist. For more information about working with objects that aren't associated with a type library, see "Creating a Reference to an Object."

  4. From the View menu, choose Object Browser to view the referenced type library. Select the appropriate type library from the Project/Library list. You can use all the objects, methods, and properties listed in the Object Browser in your application.

    For more information See "Browsing ActiveX Component Type Libraries."

  5. Declare an object variable of the object's class. For example, you could declare a variable of the class Excel.Chart to refer to a Microsoft Excel Chart object.
    Dim xlChart As Excel.Chart

    For more information See "Declaring an Object Variable" later in this chapter.

  6. Assign an object reference to the variable by using the New keyword, CreateObject, or GetObject in a Set statement. For more information, see "Assigning an Object Reference to a Variable" later in this chapter.

    If the object is a dependent object, assign an object reference by using a method of a higher-level object in a Set statement.

To create a reference to an object not defined in a type library

  1. Declare an object variable of the Object data type.

    Because the object isn't associated with a type library, you won't be able to use the Object Browser to view the properties, methods, and events of the object. You need to know what properties, methods, and events the object provides, including any methods for creating a reference to a dependent object.

    For more information See "Declaring an Object Variable" later in this chapter.

  2. Assign an object reference to the variable by using CreateObject or GetObject in a Set statement. For more information, see "Assigning an Object Reference to a Variable" later in this chapter.

    If the object is a dependent object, assign an object reference by using a method of a higher-level object in a Set statement.

Ambiguous References and Reference Priority

When you refer to a constant or object in code, Visual Basic searches for the constant or object class in each type library selected in the References dialog box in the order the type libraries are displayed. If two type libraries contain constants or classes with identical names, Visual Basic uses the definition provided by the type library listed higher in the Available References box.

Figure 10.1 The References dialog box

The best way to handle potentially ambiguous references is to explicitly specify the type library that supplies the constant or class when you use it. For example, the constant vbCancel evaluates to different values in the Visual Basic and Visual Basic for Applications type libraries. The following code shows fully qualified and ambiguous references to the constant vbCancel:

' Print the Visual Basic vbCancel.
Debug.Print "VB.vbCancel = "; VB.vbCancel
' Print the Visual Basic for Applications vbCancel.
Debug.Print "VBA.vbCancel = "; VBA.vbCancel
' Ambiguous reference prints the value of vbCancel
' that appears highest in the type library highest
' in the Available References list.
Debug.Print "vbCancel = "; vbCancel

The following code example shows fully qualified and ambiguous declarations for an Application object variable. If Microsoft Word appears higher in the Available References box than Microsoft Excel, xlApp2 is declared using the Microsoft Word Application class rather than the Microsoft Excel Application class.

' Fully qualified object variable declaration.
Dim xlApp1 As Excel.Application
' Ambiguous object variable declaration.
Dim xlApp2 As Application

' Assign an object reference.
Set xlApp1 = New Excel.Application
' The following generates a type mismatch error.
Set xlApp2 = xlApp1

You may be tempted to handle potentially ambiguous references by changing the order in which Visual Basic searches for references. The References dialog box includes two Priority buttons that let you move a type library higher in the list, so that its constants and classes will be found sooner than constants or classes with identical names lower on the list. However, changing the priority order can cause unexpected problems in your applications if there are other ambiguous references. In general, it's better to explicitly specify the type library in any references.

Note The Excel.Application syntax for referring to the Microsoft Excel Application class is not supported in versions prior to Microsoft Excel 97. To refer to the Microsoft Excel Application class in Microsoft Excel 5.0 and Microsoft Excel 95, use the syntax [_ExcelApplication] instead. For example:

Set xlApp = New [_ExcelApplication]


Browsing ActiveX Component Type Libraries

If an ActiveX component provides a type library, you can use the Object Browser to view the component's classes, as well as the properties, methods, events, and constants associated with the objects of each class.

To view the classes available in an ActiveX Component's type library

  1. If you haven't already done so, add a reference to the type library to your Visual Basic project.

    For more information, see "Creating a Reference to an Object," later in this chapter.

  2. Open the Object Browser and select the name of the type library from the Project/Library list.

    The Object Browser displays the available classes in the Classes list.

Figure 10.2 The Object Browser

For example, to view the classes available in the Data Access Object (DAO) type library, add a reference to the library in the References dialog box, then select DAO in the Project/Library list in the Object Browser.

To view the members of a class

  • Select the name of the class from the Classes list in the Object Browser.

    The Object Browser displays the members of the class in the Members of list.

If you're looking for information about a particular class or member in a type library, use the Object Browser's Search feature.

To use the Search feature

  • Type what you're looking for in the Search Text box, and then click the Search button.

    The Object Browser displays a Search Results box showing the libraries, classes, and members returned by the search.


Declaring an ActiveX Component Object Variable

Declaring an Object Variable

Before you can use the properties, methods, and events of an object provided by an ActiveX component, you must first declare an object variable. The way you declare an object variable depends on whether or not the ActiveX component supplies a type library.

To declare a variable for an object defined in a type library

  1. Add a reference to the type library to your Visual Basic project. For more information on adding a reference to a type library, see "Creating a Reference to an Object" earlier in this chapter.

  2. Specify the name of a class supplied by that type library in your variable declaration. Declaring an object variable of a specific class can speed object references. Use the following syntax:

    Dim variable As [New] class

    The class argument can be composed of two parts, in the form component.class.

    Part | Description
    component | The name of the component that supplies the object. Choices are displayed in the Project/Library list of the Object Browser.
    class | The object's class name (provided by the component's type library). Choices are shown in the Classes/Modules box of the Object Browser.

For example, you can declare a variable for a Microsoft Excel Chart object in either of the following ways:

Dim xlChart As Chart
Dim xlChart As Excel.Chart

If you declare an object variable using the New keyword, Visual Basic will automatically create an object and assign an object reference the first time you use the variable. For example, the following statements assign a reference to a new DAO table object to the variable tdfOrders, setting the table's Name property to "Orders":

Dim tdfOrders As New TableDef
tdfOrders.Name = "Orders"

Note Using variables declared using the New keyword can slow your application. Every time Visual Basic encounters a variable declared using New, it must test whether or not an object reference has already been assigned to the variable.

To declare an object variable for an object not defined in a type library

  • Declare an object variable of the generic Object class, as follows:

    Dim variable As Object

For example, the variable objAny in the following declaration can be used for a Microsoft Excel Chart object or any other object provided by an ActiveX component:

Dim objAny As Object

The main difference between declaring a variable of a specific class and declaring a variable of the generic Object class is in how ActiveX binds the variable to the object. When you declare a variable of the generic Object class, ActiveX must use late binding. When you declare an object variable of a specific class, ActiveX uses early binding, which can speed object references. For more information, see "Speeding Object References" later in this chapter.

For More Information For more information on declaring object variables, see "Dim Statement." For more information on assigning an object reference to a variable, see "Assigning an Object Reference to a Variable."


Assigning an Object Reference to a Variable

After you declare an object variable, you must assign an object reference to the variable before you can use the object's properties, methods, and events. You can assign a new object reference in several ways:

  • If you declared the variable using the New keyword, Visual Basic will automatically assign a new object reference the first time you use the variable.

  • You can assign a reference to a new object in a Set statement by using the New keyword or CreateObject function.

  • You can assign a reference to a new or existing object in a Set statement by using the GetObject function.

Assigning an Object Reference Using the New Keyword

If the ActiveX component supplies a type library, you can use the New keyword in a variable declaration or Set statement to create a new object and assign an object reference to an object variable.

If you declare an object variable with the New keyword, Visual Basic will automatically create a new object the first time you use the variable. For more information, see "Declaring an Object Variable."

You can also use the New keyword in a Set statement to assign a reference to a new object of the specified class. For example, the following statements assign a reference to a new DAO table object to the variable tdfOrders, setting the table's Name property to "Orders":

Dim tdfOrders As DAO.TableDef
Set tdfOrders = New DAO.TableDef
tdfOrders.Name = "Orders"

For More Information See "Dim Statement" or "Set Statement."

Assigning an Object Reference Using CreateObject

Regardless of whether or not an ActiveX component supplies a type library, you can use the CreateObject function in a Set statement to create a new object and assign an object reference to an object variable. You must specify the object's programmatic identifier as an argument to the function, and the object you want to access must be externally creatable.

To assign an object reference using CreateObject

  • Use the following syntax for CreateObject.

    Set objectvariable = CreateObject("progID", ["servername"])

The progID argument is usually the fully qualified class name of the object being created; for example, Word.Document. However, progID can be different from the class name. For example, the progID for a Microsoft Excel object is "Sheet" rather than "Worksheet." The optional servername argument can be specified to create an object on a remote machine across a network. It takes the Machine Name portion of a share name. For example, with a network share named \\MyServer\Public, the servername argument would be "MyServer."

The following code example starts Microsoft Excel (if Microsoft Excel is not already running) and establishes the variable xlApp to refer to an object of the Application class. The argument "Excel.Application" fully qualifies Application as a class defined by Microsoft Excel:

Dim xlApp As Excel.Application
Set xlApp = CreateObject("Excel.Application")

For More Information See "CreateObject Function."

Assigning an Object Reference Using GetObject

The GetObject function is most often used to assign a reference to an existing object, although you can also use it to assign a reference to a new object.

To assign a reference to an existing object, use the following syntax.

Set objectvariable = GetObject([pathname] [, progID])

The pathname argument can be the path to an existing file, an empty string, or omitted entirely. If it is omitted, then progID is required. Specifying the path to an existing file causes GetObject to create an object using the information stored in the file. Using an empty string for the first argument causes GetObject to act like CreateObject — it will create a new object of the class whose programmatic identifier is progID. The following table describes the results of using GetObject.

If the ActiveX component is running | Result
Set X = GetObject(, "MySrvr.Application")
 X references an existing Application object.
Set X = GetObject("", "MySrvr.Object") | X references a new, externally creatable object.

If the ActiveX component is not running | Result
Set X = GetObject(, "MySrvr.Object") | An error is returned.
Set X = GetObject("", "MySrvr.Object") | The ActiveX component (MySrvr) is started, and X references a new object.

For example, the variable wrdApp refers to a running Microsoft Word Application:

Dim wdApp As Word.Application
Set wdApp = GetObject("", "Word.Application")

Just as with CreateObject, the argument "Word.Application" is the programmatic identifier for the Application class defined by Microsoft Word. If multiple instances of Microsoft Word are running, you cannot predict to which instance wdApp will refer.

Important You can also use GetObject to assign a reference to an object in a compound document file. A compound document file contains references to multiple types of objects. For example, a compound document file could contain a spreadsheet, text, and bitmaps.

The following example starts the spreadsheet application, if it is not already running, and opens the file Revenue.xls:

Dim xlBook As Excel.Workbook
Set xlBook = GetObject("C:\Accounts\Revenue.xls")

For More Information See "GetObject Function."


Speeding Object References

You can make your Visual Basic applications run faster by optimizing the way Visual Basic resolves object references. The speed with which Visual Basic handles object references can be affected by:

  • Whether or not the ActiveX component has been implemented as an in-process server or an out-of-process server.

  • Whether an object reference is early-bound or late-bound.

In general, if a component has been implemented as part of an executable file (.exe file), it is an out-of-process server and runs in its own process. If it has been implemented as a dynamic-link library, it is an in-process server and runs in the same process as the client application.

Applications that use in-process servers usually run faster than those that use out-of-process servers because the application doesn't have to cross process boundaries to use an object's properties, methods, and events. For more information about in-process and out-of-process servers, see "In-Process and Out-of-Process Servers."

Object references are early-bound if they use object variables declared as variables of a specific class. Object references are late-bound if they use object variables declared as variables of the generic Object class. Object references that use early-bound variables usually run faster than those that use late-bound variables.

For example, you could assign a reference to an Excel object to either of the following variables:

Dim xlApp1 As Excel.Application
Set xlApp1 = New Excel.Application

Dim xlApp2 As Object
Set xlApp2 = CreateObject("Excel.Application")

Code that uses variable xlApp1 is early-bound and will execute faster than code that uses variable xlApp2, which is late-bound.

Late Binding

When you declare a variable As Object, Visual Basic cannot determine at compile time what sort of object reference the variable will contain. In this situation, Visual Basic must use late binding— that is, Visual Basic must determine at run time whether or not that object will actually have the properties and methods you used in your code.

For example, Visual Basic will compile the following code without generating errors, even though it refers to a method that doesn't exist, because it uses a late-bound object variable. It doesn't check for the existence of the method until run time, so it will produce a run-time error:

Dim xlApp As Object
Set xlApp = CreateObject("Excel.Application")
xlApp.TheImpossibleMethod   ' Method doesn't exist.

This code runs slower than code that uses an early-bound object variable because Visual Basic must include code in the compiled executable that will determine at run time whether or not the Microsoft Excel Application object has a TheImpossibleMethod method.

Although late binding is the slowest way to invoke the properties and methods of an object, there are times when it is necessary. For example, you may write a function that uses an object variable to act on any of several different classes of objects. Because you don't know in advance what class of object will be assigned to the variable, declare it as a late-bound variable using As Object.

Early Binding

If Visual Basic can detect at compile time what object a property or method belongs to, it can resolve the reference to the object at compile time. The compiled executable contains only the code to invoke the object's properties, methods, and events. This is called early binding.

When you declare an object variable using the class that defines the object, the variable can only contain a reference to an object of that class. Visual Basic can use early binding for any code that uses the variable.

Early binding dramatically reduces the time required to set or retrieve a property value, because the call overhead can be a significant part of the total time. For method calls, the improvement depends on the amount of work the method does. Short methods, where the call overhead is comparable to the time required to complete the task, will benefit the most.


Using an Object's Properties, Methods, and Events

After you assign an object reference to an object variable, you can use the variable to manipulate the object's properties and methods. You can also declare an object variable using the WithEvents keyword and use it to make your application respond to the object's events.

Using an Object's Properties and Methods

You can use the object.property syntax to set and return an object's property values or the object.method syntax to use methods on the object. For example, you could set the Caption property of the Application object as follows:

Dim xlApp As Excel.Application
Set xlApp = New Excel.Application
xlApp.Caption = "MyFirstObject"

Note The Excel.Application syntax for referring to the Microsoft Excel Application class is not supported in versions prior to Microsoft Excel 97. To refer to the Microsoft Excel Application class in Microsoft Excel 5.0 and Microsoft Excel 95, use the syntax [_ExcelApplication] instead. For example:

Set xlApp = New [_ExcelApplication]

You could call the Quit method of the Microsoft Excel Application object like this:

xlApp.Quit

In general, it is a good idea to be as specific as possible when referring to methods or properties of objects defined by other applications or projects. For example:

' Fully qualified property name sets
' the Microsoft Project window caption.
Dim pjWindow As Project.Window
' Get a reference to the first Window object.
Set pjWindow = ActiveProject.Windows(1)
pjWindow.Caption = "Project Caption"

' Unqualified name causes Visual Basic to use
' the first object it finds with a property
' named Caption - in this case, Form1.
Caption = "Microsoft Form1 Caption"

Note If you need to import binary data into your Visual Basic application and you plan to share the data between applications using ActiveX, use a Byte array to store the data. If you assign binary data to a string and then try to pass this data to an Automation object that takes a string, the data may not be converted correctly. "For more information on data types, see "Programming Fundamentals."

For More Information "For more information on working with an object's properties and methods, see "Programming with Objects."

Responding to an Object's Events

In addition to responding to events that occur to Visual Basic objects, your application can respond to events in an object provided by an ActiveX component. For example, your Visual Basic application can display a message box if an event occurs in a Microsoft Excel workbook.

You make your application respond to an object's events by adding code to an event procedure for the object. However, event procedures for objects provided by components are not automatically available in Visual Basic. You must first declare an object variable using the WithEvents keyword.

After you declare an object variable using WithEvents, the Visual Basic code window uses the variable to display event procedures for the object. You can then add code to these event procedures to respond to the object's events. When you assign an object reference to the variable, you establish a connection between the variable and the object at run time.

To create an event procedure for an object provided by a component

  1. Add a reference to the component's type library to your Visual Basic project. For more information on adding a reference to a type library, see "Creating a Reference to an Object."

  2. In the Declarations section of a form or class module, declare an object variable using the WithEvents keyword. For example:
    Dim WithEvents xlBook As Excel.Workbook

    Visual Basic adds the name of the object variable to the Object box in the code window. When you select the variable name, Visual Basic displays the object's event procedures in the Procedure list box.

  3. Select an event procedure, then add code to the procedure that you want your application to run when the event occurs.

    For example, suppose your Visual Basic application relies on data displayed in a Microsoft Excel workbook and that you've already declared a WithEvents variable xlBook for the workbook. When a user tries to close the workbook, you can display a message and keep the workbook from closing by adding the following code to the xlBook_BeforeClose event procedure in your application:

    Private Sub xlBook_BeforeClose(Cancel As Boolean)
' Hide the Microsoft Excel window so the message
' will be visible.
xlBook.Application.Visible = False
' Display the message.
MsgBox "This workbook must remain open."
' Unhide the Microsoft Excel window.
xlBook.Application.Visible=True
' Set the event procedure's Cancel argument
' to True, cancelling the event.
Cancel = True
End Sub
  4. Assign an object reference to the WithEvents object variable.

    For example, you could add the following to the Visual Basic form's Form_Load event procedure to assign the variable xlBook a reference to a Microsoft Excel workbook, Sales.xls:

    Private Sub Form_Load()
Set xlBook = GetObject("Sales.xls")
' Display Microsoft Excel and the Worksheet
' window.
xlBook.Application.Visible = True
xlBook.Windows(1).Visible = True
End Sub

For More Information See "Dim Statement."


Releasing an ActiveX Component

When you are finished using an object, clear any variables that reference the object so the object can be released from memory. To clear an object variable, set it to Nothing. For example:

Dim acApp As Access.Application
Set acApp = New Access.Application
MsgBox acApp.SysCmd(acSysCmdAccessVer)
Set acApp = Nothing

All object variables are automatically cleared when they go out of scope. If you want the variable to retain its value across procedures, use a public or form-level variable, or create procedures that return the object. The following code shows how you would use a public variable:

Public wdApp as Word.Application
.
.
.
' Create a Word object and start Microsoft Word.
Set wdApp = New Word.Application
.
.
.
' Microsoft Word will not close until the
' application ends or the reference is set to Nothing:
Set wdApp = Nothing

Also, be careful to set all object references to Nothing when finished, even for dependent objects. For example:

Dim xlApp As Excel.Application
Dim xlBook As Excel.Workbook
Set xlApp = New Excel.Application
Set xlBook = xlApp.Workbooks.Add
Set xlApp = Nothing   ' Careful! xlBook may still
                     ' contain an object reference.
Set xlBook = Nothing   ' Now all the references
                     ' are cleared.


Navigating Object Models

Once you understand how to use objects provided by components, you can use any object that is a component exposes to you. Components can range from a simple code component or ActiveX control to large components, such as Microsoft Excel and the Microsoft Data Access Object (DAO) programming interface, which expose many objects.

Each object exists somewhere in the component's object hierarchy, and you can access the objects in two ways:

  • Directly, if the object is externally creatable.

  • Indirectly, if the object is a dependent object. You can get a reference to it from another object higher in the component's hierarchy.

The best way to navigate an object hierarchy is to use the Object Browser (if the component provides an object library).

Navigating the Object Hierarchy

As you've seen, you navigate down an object hierarchy by setting references to dependent objects through externally creatable objects. You can also use a method on a collection object to return an individual object. For more information see "Working with Externally Creatable and Dependent Objects."

Figure 10.3 shows the object navigation path in a Microsoft Excel application.

Figure 10.3 Navigating down a Microsoft Excel object hierarchy using collections

To navigate back up, most applications use the Parent and Application, as shown in Figure 10.4.

Figure 10.4 Navigating back up a Microsoft Excel object hierarchy using the Parent and Application properties

Collection Objects

Collection objects are containers for groups of other objects. These objects provide an easy way to keep track of a set of objects that are of the same type. For example, a collection of all the Menu objects in an application can be accessed using the Menus collection object. You can use the following code to refer to all the workbooks that are currently loaded in Microsoft Excel:

Application.Workbooks

Notice that Workbooks is plural. The standard naming convention for collection objects is the plural of the type of object that makes up the collection. You can iterate through the objects in a collection by using the For Each statement, as follows:

Dim xlBook As Excel.Workbook
.
.
.
For Each xlBook In Application.Workbooks
   ' Display the name of each workbook.
   MsgBox xlBook.FullName
Next xlBook

Individual objects in many collections can also be referenced by name or by their index order in the collection. The following example shows how you would refer to Style objects named "Normal," "Example," and "Heading":

xlBook.Styles("Normal")
xlBook.Styles("Example")
xlBook.Styles("Heading")

Assuming these objects are the first three objects in the Styles, and that the collection is zero-based, you could also refer to them as follows:

xlBook.Styles(1)      ' Refers the Normal Style object.
xlBook.Styles(2)      ' Refers the Example Style
                     ' object.
xlBook.Styles(3)      ' Refers the Heading Style
                     ' object.

For More Information For more information on working with collection objects, see "Programming with Objects."


Working with Externally Creatable and Dependent Objects

How you create a reference to an object provided by a component depends on whether the object is an externally creatable or dependent object. You can directly create a reference to an externally creatable object; you create a reference to a dependent object indirectly by using a method of a higher-level object in the component's object hierarchy.

Externally Creatable Objects

Most large ActiveX-enabled applications and other ActiveX components provide a top-level externally creatable object in their object hierarchy that:

  • Provides access to other objects in the hierarchy.

  • Provides methods and properties that affect the entire application.

For example, the Microsoft Office applications each provide a top-level Application object. The following example shows how you can assign references to the Application objects of Microsoft Excel, Microsoft Word, and Microsoft Access:

Dim xlApp As Excel.Application
Dim wdApp As Word.Application
Dim acApp As Access.Application

Set xlApp = New Excel.Application
Set wdApp = New Word.Application
Set acApp = New Access.Application

You can then using these variables to access the dependent objects in each application and the properties and methods of these objects. For more information see "Creating a Reference to an Object."

Note The Excel.Application syntax for referring to the Microsoft Excel Application class is not supported in versions prior to Microsoft Excel 97. To refer to the Microsoft Excel Application class in Microsoft Excel 5.0 and Microsoft Excel 95, use the syntax [_ExcelApplication] instead. For example:

Set xlApp = New [_ExcelApplication]

In addition to these top-level externally creatable objects, ActiveX components can also provide externally creatable objects that are lower on the component's object hierarchy. You can access these objects either directly as an externally creatable object or indirectly as a dependent object of a higher-level externally creatable object. For example, you can create a reference to a DAO TableDef object either directly or indirectly:

   ' Create a reference to daoTable1 directly.
   Dim daoTable1 As DAO.TableDef
   Set daoTable1 = New DAO.TableDef
   daoTable1.Name = "Table1"

   ' Create a reference to daoTable2 indirectly,
   ' as a dependent object of the DAO DBEngine object.
   Dim daoDBE As DAO.DBEngine
   Dim daoWs As DAO.Workspace
   Dim daoDb As DAO.Database
   Dim daoTable2 As DAO.TableDef

   Set daoDBE = DAO.DBEngine
   Set daoWs = daoDBE.Workspaces(0)
   Set daoDb = daoWs.CreateDatabase("db1.mdb", _
   dbLangGeneral)
   Set daoTable2 = daoDb.CreateTableDef("Table2")

Some objects provide an Application object, but give it a different name. For example, the Microsoft Jet database engine in Microsoft Access calls its top-level object the DBEngine object.

Dependent Objects

You can get a reference to a dependent object in only one way — by using a property or method of an externally creatable object to return a reference to the dependent object. Dependent objects are lower in an object hierarchy, and they can be accessed only by using a method of an externally creatable object. For example, suppose you want a reference to a Button object from Microsoft Excel. You can't get a reference to this object using the following code (an error will result):

Dim xlButton As Excel.Button
Set xlButton = New Excel.Button

Instead, use the following code to get a reference to a Button object:

Dim xlApp As Excel.Application
Dim xlBook As Excel.Workbook
Dim xlSheet As Excel.Worksheet
Dim xlButton As Excel.Button

Set xlApp = New Excel.Application
Set xlBook = xlApp.Workbooks.Add
Set xlSheet = xlBook.Worksheets.Add
Set xlButton = xlSheet.Buttons.Add(44, 100, 100, 44)

' Now you can use a Button object property.
xlButton.Caption = "FirstButton"

Figure 10.5 illustrates how a Visual Basic application gets a reference to the Button object.

Figure 10.5 Accessing dependent objects


Handling Run-Time Errors

Handling Run-Time Errors in ActiveX Components

Error-handling code is especially important when you're working with ActiveX components, because code from the component is used from within your Visual Basic application. Where possible, you should include code to handle errors that the component may generate. For example, it is good practice to check for the error that occurs if a user unexpectedly closes a component application:

Function StartWord()
' Starts Microsoft Word.
On Error Goto ErrorTrap

   ' Declare a Microsoft Word Application variable
   ' and an integer variable for error trap.
   Dim wdApp As Word.Application
   Dim iTries As Integer

   ' Assign an object reference.
   Set wdApp = New Word.Application

   ' Release object variable.
   Set wdApp = Nothing

   Exit Function

ErrorTrap:
   ' Trap for the error that occurs if Microsoft Word
   ' can't be started.
   Select Case Err.Number
      Case 440               ' Automation error.
         iTries = iTries + 1
         ' Make up to 5 attempts to restart Word.
         If iTries < 5 Then
            Set wdApp = New Word.Application
            Resume
         Else
            Err.Raise Number:=VBObjectError + 28765, _
            Description:= "Couldn't restart Word"
         End If
      Case Else
         Err.Raise Number:= Err.Number
   End Select
End Function

If any error other than error 440 occurs in the preceding example, the procedure displays the error and raises an error. The application that provides the object might pass back its own error. In some cases, an application might use the same error code that Visual Basic uses for a different error. In these cases, you should use On Error Resume Next and check for errors immediately after each line that might cause an error. This type of error checking is called inline error-handling.

Testing for Object References

Before using an object variable in your code, you may want to make sure the variable holds a valid object reference. You can determine whether or not an object reference has been assigned to the variable by using Is Nothing. For example, the following code checks whether or not an object reference has been assigned to the variable wdDoc:

If wdDoc Is Nothing Then MsgBox "No object reference."

However, Is Nothing won't detect whether or not a valid object reference has become unavailable. For example, if you assign a Microsoft Word object reference to an object variable and Microsoft Word becomes unavailable, the variable will still hold a valid object reference. In this situation, use your error handler to trap the error that results when your code tries to use the variable.

For More Information For information about the errors that a particular application might pass back, see that application's documentation. For more information about trapping errors, see "Debugging Your Code and Handling Errors."


Handling Requests Pending to an ActiveX Component

It usually takes only a fraction of a second to set a property or call a method of an object. At times, however, your request may not be finished immediately.

  • If you call the Close method of a Workbook in Microsoft Excel while the user has a dialog box open, Microsoft Excel signals that it is busy and cannot execute your request. This can lead to a component busy condition.

  • If you call a method that performs a lengthy operation, such as a large amount of database work when the database is very active, you may try to perform another operation while the first operation is still pending. This can lead to a request pending condition.

  • If you have two or more programs making calls to a shared component, one call must be completed before another can begin. Components handle such conflicts by serializing the requests, that is, making them wait in line. This can also lead to a request pending condition.

Component busy is like getting a busy signal when you make a telephone call. You know you're not going to get through, so you may as well hang up and try again later.

Request pending is like having your call go through, and then having the person you're calling keep you talking much longer than you intended. If your request is serialized, then request pending is like having the other party pick up the telephone and say immediately, "Can you hold, please?"

The Component Busy Condition

A component may reject your request because it has a modal dialog box open, or because a user edit operation is in progress. For example, Microsoft Excel rejects requests from a client application while a spreadsheet cell is being edited.

Visual Basic assumes that the busy condition is temporary and keeps trying the request for a specified timeout interval. When that time is up, Visual Basic displays the Component Busy dialog box, as shown in Figure 10.6.

Figure 10.6 The Component Busy dialog box

The user can retry the request, cancel the request, or switch to the component and fix the problem (for example, by dismissing the dialog box). If the user chooses Cancel, the error &h80010001 (RPC_E_CALL_REJECTED) is raised in the procedure that made the request.

The Request Pending Condition

Once a component has accepted your application's request, your application must wait until the request has been completed. If the request takes a long time, the user may attempt to minimize your program or resize it to get it out of the way.

After a short timeout interval, Visual Basic responds to such attempts by displaying the Component Request Pending dialog.

The appearance of the Component Request Pending dialog box is slightly different from the Component Busy dialog. The Cancel button is disabled, as shown in Figure 10.7, because the request in progress cannot be canceled.

Figure 10.7 The Component Request Pending dialog box

Switching to the component is useful only if it has halted to display an error message as a result of your request. This should not be a common occurrence, because the proper behavior for a component is to return an error condition to the program that called it.

For More Information For more information, see "Changing the Component Busy or Request Pending Messages," "Controlling Timeout Intervals," and "Raising an Error on Component Busy Timeout" later in this chapter.


Changing the Component Busy or Request Pending Messages

The Component Busy and Component Request Pending dialog boxes are provided by Visual Basic as simple default messages. There are many situations where these dialog boxes may not meet your needs.

  • Your program may call a method of an object provided by a component that has no user interface. Components created using Visual Basic, Professional or Enterprise Editions, for example, may run in the background without any visible forms.

  • The component you call may have been created using the Remote Automation features of Visual Basic, Enterprise Edition, and may be running on another computer located at some distance from the user.

  • If your program has loaded a Microsoft Excel workbook using the GetObject function, the workbook will not be visible when the user switches to Microsoft Excel. In fact, Microsoft Excel itself may not be visible, in which case the Switch To button does nothing.

In these situations, the Switch To button is inappropriate and may confuse the user of your program. You can specify a substitute message for either or both of the timeouts. Your messages will be displayed in a simple message box, without a Switch To button.

For the request pending condition, the message box has only an OK button. For the component busy condition, an OK button and a Cancel button are provided. If the user presses Cancel, error -2147418111 (&h80010001) will be raised in the procedure in which you made the request.

The following properties of the App object determine whether the Component Busy or Component Request Pending dialog box will be replaced by a message box and allow you to specify the text and caption of the message box.

OLEServerBusyMsgText Property

Specifies the message text to be displayed when the component busy condition occurs. Setting this property causes the alternate message box to be used in place of the usual Component Busy dialog box.

OLEServerBusyMsgTitle Property

Specifies the caption to be used if an alternate message is supplied for the component busy condition. (Only setting this property will not cause the alternate message box to be used.)

OLERequestPendingMsgText Property

Specifies the message text to be displayed when the request pending condition occurs. Setting this property causes the alternate message box to be used in place of the usual Component Request Pending dialog box.

OLERequestPendingMsgTitle Property

Specifies the caption to be used if an alternate message is supplied for the request pending condition. (Only setting this property will not cause the alternate message box to be used.)

The following example sets titles and message texts for both the component busy and pending request conditions, completely overriding the Component Busy and Component Request Pending dialog boxes.

Public Const APP_TITLE = "Demo Application"

Private Sub cmdLongTransaction_Click()
   On Error Goto LongTransaction_Error
   ' You may wish to set the titles once, in Sub Main.
   App.OLEServerBusyMsgTitle = APP_TITLE

   App.OLERequestPendingMsgTitle = APP_TITLE
   ' Message texts specific to this request.
   App.OLEServerBusyMsgText = "The component for _
      the " & "Long Transaction has not responded. _
      If " & "you have been waiting more than five " _
      & "minutes, you may wish to cancel this " _
      & "request and try it later." & vbCrLf _
      & "Call Network Services to verify that the " _
      & "component is running, or to report problems."
   App.OLERequestPendingMsgText = "Your request " _
      & "is still executing. " & vbCrLf _
      & "Call Network Services to verify that the " _
      & " component is running, or to report _
      problems."
   ' Code to make a request and use results...
   ' ...
LongTransaction_Cleanup:
   ' Code to perform any necessary cleanup...
   ' ...
   Exit Sub

LongTransaction_Error:
   If Err.Number = &h80010001 Then
      MsgBox "Transaction cancelled"
   Else
      ' Code to handle other errors.
   End If
   Resume LongTransaction_Cleanup
End Sub

Important The length of your messages may be limited by the operating system. Messages more than a thousand characters in length can be used when the target operating system is Windows NT or Windows 95.


Controlling Timeout Intervals

You can set the timeout intervals that determine when Visual Basic displays the Component Busy and Component Request Pending dialog boxes, using two properties of the App object.

OLEServerBusyTimeout Property

Determines how long Visual Basic will go on retrying your Automation requests before displaying the Component Busy dialog. The default value is 10000 milliseconds (10 seconds).

OLERequestPendingTimeout Property

Determines how long Visual Basic waits before responding to mouse clicks, keypress events, and other events by displaying the Component Request Pending dialog. The default value is 5000 milliseconds (5 seconds).

The following example shows how the timeout values might be adjusted and reset for a call to the StockAnalysis method of a hypothetical BusinessRules object.

Public Sub SetTimeouts(ByVal lngComponentBusy As _
Long, ByVal lngRequestPending As Long)
   App.OLEServerBusyTimeout = lngComponentBusy
   App.OLERequestPendingTimeout = lngRequestPending
End Sub

Public Sub ResetTimeouts()
   App.OLEServerBusyTimeout = 10000

   App.OLERequestPendingTimeout = 5000
End Sub

Private Sub cmdFullAnalysis_Click()
   On Error Goto FullAnalysis_Error
   ' Set very short timeouts. After 2 seconds,
   ' the user will be notified and keypresses or
   ' clicks will display the Component Busy
   ' and Component Request Pending dialogs.
   SetTimeouts 2, 2
   Me.MousePointer = vbHourglass
   gobjBusinessRules.StockAnalysis txtNYSECode.Text, _
   ATYPE_FULL
FullAnalysis_Cleanup:
   Me.MousePointer = vbDefault
   ResetTimeouts
   Exit Sub

FullAnalysis_Error:
   If Err.Number = &h80010001 Then
      MsgBox "Analysis cancelled"
   Else
      ' Code to handle other errors...
   End If
   Resume FullAnalysis_Cleanup
End Sub

You can set either of these timeouts to very large values, because they are stored as Longs. For example, 86,400,000 milliseconds is a day, which is equivalent to an infinite timeout. When you do this, however, you risk having your program lock up until the component is no longer busy, or until a pending request has completed.

Important Because these timeout values are properties of the App object, they also affect documents you link or embed using the OLE container control or the Toolbox. If you are using linked or embedded documents and you change these properties for an Automation request, it is a good idea to reset the values afterward.


Raising an Error on Component Busy Timeout

For the component busy condition, you can bypass both the Component Busy dialog box and the replacement message by setting the Boolean OLEServerBusyRaiseError property of the App object to True. Visual Basic will retry your request for the length of time specified by the OLEServerBusyTimeout property, and then raise an error in the procedure that made the Automation request, just as if the user had pressed the Cancel button on the Component Busy dialog box.

The error returned is –2147418111 (&h80010001). In the error handler for the procedure, you can then take whatever action is most appropriate. For example, you could display a complex dialog box that offered the user several retry options or alternatives.

This property will be particularly useful for components designed to run on remote network computers, using the Remote Automation feature of Visual Basic, Enterprise Edition. Such a component may call on other components, and it must handle errors in those calls without displaying any forms.

There is no corresponding property for the request pending condition. Once an Automation request has been accepted by the component, the client program must wait until the request is complete.


Using a Component's Visual Interface

If a component supports object linking and embedding (OLE), you can link or embed an object into your application without writing any code by using the component's visual interface. You can use a component's visual interface in one of two ways:

  • By adding an OLE container control to your application, then inserting an object into the control.

  • By adding the object's class to the Toolbox, then adding an object of that class to your application just as you would add a control to a form.

Inserting an Object with the OLE Container Control

The OLE container control gives you the most flexibility in using an object's visual interface. With the OLE container control, you can:

  • Create a placeholder in your application for an object. You can create the object that appears within the OLE container control at run time, or you can change an object you have placed in the OLE container control at design time.

  • Create a linked object in your application.

  • Bind the OLE container control to a database.

  • Perform an action if the user moves, sizes, or updates the object in the OLE container control.

  • Create objects from data that was copied onto the Clipboard.

  • Display objects as icons.

An OLE container control can contain only one object at a time. There are several ways to create a linked or embedded object in the OLE container control — the one you choose depends on whether you are creating the linked or embedded object at design time or run time. Once you have an OLE container control drawn on your form, you can insert an object into the container control by:

  • Using the Insert Object or Paste Special dialog box. See "Inserting Objects at Design Time with the OLE Container Control" and "Creating Objects at Run Time with the OLE Container Control."

  • Setting the Class, SourceDoc, and SourceItem properties in the Properties window. See "Creating Objects at Run Time with the OLE Container Control."

  • Calling the CreateEmbed or CreateLink method. See "Creating Objects at Run Time with the OLE Container Control."

For More Information For more information on using the OLE container control, see "OLE Container Control" and "Containers for Controls" in "Using Visual Basic's Standard Controls."

Inserting an Object by Adding Its Class to the Toolbox

In the same way that you use the Toolbox to add one of Visual Basic's built-in controls to an application, you can use the Toolbox to add an object. First, add the object's class to the Toolbox, then add the object to a form.

To add an object's class to the Toolbox

  1. From the Project menu, choose Components.

  2. In the Components dialog box, click the Insertable Objects tab.

  3. Select the class you want to add to the Toolbox, then click OK. Visual Basic adds a button of that class to the toolbox.

    For example, to add a Microsoft Excel Worksheet button to the Toolbox, select Microsoft Excel Worksheet.

Once you've added the object's class to the Toolbox, you can draw it on a form to create an object of that class. For example, after you add a Microsoft Excel Worksheet button to the Toolbox, you can draw it on a form to create a worksheet object on the form.


Contrasting Linked and Embedded Objects

You use a component's visual interface to contain data from another application by linking or embedding that data into your Visual Basic application. The primary difference between a linked and embedded object is where their data is stored. For example, data associated with a linked object is managed by the application that created it and stored outside an OLE container control. Data associated with an embedded object is contained in an OLE container control and can be saved with your Visual Basic application.

When a linked or embedded object is created, it contains the name of the application that supplied the object, its data (or, in the case of a linked object, a reference to the data), and an image of the data.

Note To place an object in an OLE container control, the component that provides the object must be registered in your system registry. When you install an application that supplies the objects you want to use in your project, that application should register its object library on your system so that application's objects appear in the Insert Object dialog box. You can use Regedit.exe to search the system registry for an object, but take care not to alter the contents of the registry.

Linked Objects

When you link an object, you are inserting a placeholder (not the actual data itself) for the linked object into your application. For example, when you link a range of spreadsheet cells to a Visual Basic application, the data associated with the cells is stored in another file; only a link to the data and an image of the data are stored in the OLE container control. While working with your Visual Basic application, a user can activate the linked object (by double-clicking the object, for example), and the spreadsheet application will start automatically. The user can then edit those spreadsheet cells using the spreadsheet application. When editing a linked object, the editing is done in a separate window outside the OLE container control.

When an object is linked to a Visual Basic application, the object's current data can be viewed from any other applications that contain links to that data. The data exists in only one place — the ActiveX component — which is the source application that provides the object. For example, in Figure 10.8, Visual Basic contains a link to the Graph application. Microsoft Word also contains a link to the graph. If the graph's data is changed by either application, the modified graph will appear in both the Visual Basic application and the Microsoft Word document.

Figure 10.8 An object's data can be accessed from many different applications that contain links to that data

As you can see, linking makes it easy to track identical information that appears in more than one application. Linking is useful when you want to maintain one set of data that is accessed from several applications.

Embedded Objects

To create an embedded object, you can either use an OLE container control or add an object's class to the Toolbox. With an embedded object, all the data associated with the object is copied to and contained in the OLE container control. When you save the contents of the control to a file, the file contains the name of the application that produced the object, the object's data, and a metafile image of the object. For this reason, embedded objects can greatly increase file size.

Unlike linked objects, no other application has access to the data in an embedded object. Embedding is useful when you want your application to maintain data that is produced and edited in another application, as shown in Figure 10.9.

Figure 10.9 Your application maintains data for an embedded object

When the user activates the object (the graph), the ActiveX component that created the object (Microsoft Graph) is invoked by the container application (your Visual Basic application), and the object's data is opened for editing. In addition, the user interface and menu system of the object is displayed in the container application so the user can control the object in place. For more information on in-place activation, see "Activating an Object in the OLE Container Control" later in this chapter.


Inserting Objects at Design Time with the OLE Container Control

Each time you draw an OLE container control on a form, Visual Basic displays the Insert Object dialog box. You use this dialog box, shown in Figure 10.10, to insert linked or embedded objects at design time. The Insert Object dialog box presents a list of the available objects you can link or embed into your application.

Figure 10.10 The Insert Object dialog box

When you insert an object into the OLE container control at design time, the Class, SourceDoc, and SourceItem properties are automatically set for you. These properties identify the application that supplies the object, the source file name, and any specific data that is linked from within that file. For more information about these and other properties and methods that apply to the OLE container control, see "Inserting Objects at Run Time."

Inserting Linked Objects at Design Time

When you insert a linked object, the data displayed in the OLE container control exists in one place — the source file. The object's current data can be viewed from any other applications that contain links to that data. The OLE container control maintains the object's link information, such as the name of the application that supplied the object, the name of the linked file, and an image of the linked data.

To insert a linked object using the Insert Object dialog box

  1. Draw an OLE container control on a form.

    The Insert Object dialog box is displayed. You can also display this dialog box at any time by clicking the OLE container control with the right mouse button and then choosing the Insert Object command.

  2. Select the Create from File option button.

  3. Choose the Browse button.

    A Browse dialog box is displayed.

  4. Select the file you want to link.

  5. Click Insert to return to the Insert Object dialog box.

  6. Select the Link check box in the Insert Object dialog box, and choose OK to create the linked object.

When you use a linked object, every user who runs your application must have access (a valid path) to the linked file and a copy of the application that created the file. Otherwise, when your application is run, an image of the original data is displayed, but the user will not be able to modify the data, nor will the user see changes others may have made to the linked data. This may be a concern if your application is running on a network.

If your application contains a linked object, it is possible for that object's data to be changed by another application when your application is not running. The next time your application is run, changes to the source file do not automatically appear in the OLE container control. To display the current data in the OLE container control, use the control's Update method:

oleObj.Update

For More Information See "Update Method (OLE Container)."

If a user wants to save changes to a linked object, the user must save it from the ActiveX component's menu. The OLE container control's SaveToFile method applies only to embedded objects.

Creating Embedded Objects at Design Time

When you create an embedded object, you can either embed data from a file or create a new, empty object that can be filled with data later. When you embed data from a file, a copy of the specified file's data is displayed in the OLE container control. When you create a new object, the application that created the object is invoked and you can enter data into the object.

Typically, you create embedded objects that display existing data at design time. This allows you to view the object's data as it will appear to the user. You can then move and size the OLE container control and the other controls on the form to create your application's user interface.

To display existing data in an embedded object, create the object using an existing file as a template. The OLE container control then contains an image of the data in the file. An application that displays data using an embedded object will be larger than an application that displays the same data using a linked object, because the application with the embedded object actually contains the source file's data.

To create an embedded object using an existing file

  1. Draw an OLE container control on your form.

    The Insert Object dialog box is automatically displayed.

  2. Select the Create from File option button.

  3. Choose the Browse button.

    A Browse dialog box is displayed.

  4. Select the file you want to embed.

  5. Choose Insert to return to the Insert Object dialog box.

  6. In the Insert Object dialog box, choose OK to create the embedded object.

Unlike the data in a linked object, data in an embedded object is not persistent. In other words, if you want changes entered by the user to appear the next time your application is run, you must use the SaveToFile method to save the data. For more information on saving embedded data to a file, see "Saving and Retrieving Embedded Data" later in this chapter.

Creating Objects Using the Paste Special Dialog Box

Another way to create an object at design time is to use the Paste Special dialog box (shown in Figure 10.11). This dialog box is helpful if you only want to use a portion of a file — for instance, a range of cells from a spreadsheet, or a paragraph from a Word document.

Figure 10.11 The Paste Special dialog box

To create an object using the Paste Special dialog box

  1. Run the application containing the data you want to link or embed.

  2. Select the data you want to link or embed.

  3. From the ActiveX component's Edit menu, choose Copy.

    The data is copied onto the Clipboard.

  4. In Visual Basic, click the OLE container control with the right mouse button, and choose the Paste Special command from the pop-up menu.

  5. Select the Paste option button if you want to create an embedded object.

    -or-

    Select the Paste Link option button if you want to create a linked object.

    If there is already an object embedded or linked in the control, a message asks whether you'd like to delete that existing object and create a new one in its place.

  6. Choose OK to create the object.


Creating Objects at Run Time with the OLE Container Control

To create a linked or embedded object at run time, you use methods and properties in code. The OLE container control has a variety of properties and methods that you can use for manipulating linked or embedded objects. For a complete list of the properties and methods that apply to the OLE container control, see "OLE Container Control."

Using the Object Property

By using the OLE container control's Object property, you can also use the properties and methods of the linked or embedded object. The Object property is a run-time, read-only property that holds a reference to the object in an OLE container control. Use this property to perform Automation tasks with the OLE container control, including programmatically manipulating the properties and methods an object supports:

strObjName = oleObj1.Object.Name

To use this property, the OLE container control must contain an object that is programmable. For more information on programmable objects, see "Types of ActiveX Components."

Creating Linked Objects at Run Time

You can create a linked object from a file at run time with the OLE container control's CreateLink method. This method takes one argument, sourcedoc, which is the file from which the object is created, and an optional argument, sourceitem, which specifies the data you want to link from within the source file. The following code fragment creates a linked object at run time:

oleObj1.CreateLink "C:\Excel\Test.xls"

Note If you use CreateLink to create a linked object, you do not have to set the Class, SourceDoc, and SourceItem properties in the Properties window.

For More Information See "CreateLink Method."

Creating Embedded Objects at Run Time

To create an embedded object from a file at run time, you can use the CreateEmbed method. This method has two arguments, sourcedoc and class (which is optional if SourceDoc is specified). Sourcedoc determines the template for the object, and class determines the object type. When you use CreateEmbed, you do not need to set the SourceDoc and Class properties.

The following code fragment creates an embedded object using an existing file as a template for the object.

oleObj1.CreateEmbed "Q1profit.xls"

For More Information See "CreateEmbed Method."

When you create an empty embedded object, it is a good idea to activate the ActiveX component that will provide data for the object. You can do this with the DoVerb method. This allows the user to enter any data into the application at run time. The user can then show this newly entered data in the OLE container control by choosing the ActiveX component's Update command (this menu command should appear on the component's File menu).

To create an empty embedded object at run time

  1. Use the CreateEmbed method without specifying a source document to create an empty embedded object. For example, this code fragment inserts a file template for a Microsoft Excel Worksheet in the OLE container control:
    oleObj1.CreateEmbed "","Excel.Sheet"
  2. Use the DoVerb method. The default verb for the DoVerb method depends on the application. With Microsoft Excel, the default verb is Edit.

For example, the following code fragment creates an empty embedded object and then activates the application that created it using the default DoVerb action.

oleObj1.CreateEmbed "", "Excel.Sheet"
oleObj1.DoVerb -5  ' Activate

Providing empty embedded objects is useful when creating a document-centered application that uses a variety of information from different applications. For more information, see "Letting the User Specify Objects at Run Time."

Binding a Database to the OLE Container Control

You can bind the OLE container control to data stored in the Microsoft Jet database engine or Microsoft Access database. You may want to do this, for example, if you have a database with a table of employee pictures. If the pictures are stored as objects, you can bind them to the OLE container control and display them on a form as each record is accessed with the data control. To bind data to one of these databases, specify the source of data (recordset name) in the DataSource property and the field name from that data source in the DataField property of the OLE container control. When displaying an object from a database, the OLE container control allows the user to activate, edit, and update the object. As with any bound control, the updated object is automatically written back to the database when the record position is changed.

For More Information See the Data Access Guide.


Letting the User Specify Objects at Run Time

By displaying the Paste Special and Insert Object dialog boxes at run time, you can allow the user to create a variety of objects. You may do this when creating a document-centered application. In such an application, the user combines data from different applications to create a single document. For instance, this application might be a word processor in which the user might enter some text and then embed a spreadsheet and a chart using the Insert Object or Paste Special dialog box.

You use the OLE container control's InsertObjDlg method to display the Insert Object dialog box, or you can use the PasteSpecialDlg method to display the Paste Special dialog. These two dialogs let the user make decisions about what goes into the OLE container control.

  • The Insert Object dialog box presents a list of available objects and creates an object based on the user's selection.

  • The Paste Special dialog box allows the user to paste an object from the system Clipboard into an OLE container control.

You can display these dialog boxes at run time by calling the appropriate method on an event — for instance, a menu's Click event:

Private Sub cmdInsert_Click ()
   ' Display Insert Object dialog box.
   oleObj1.InsertObjDlg
   ' Check to make sure object was created with the
   ' OLEType property.
   If oleObj1.OLEType = vbOLENone Then
      MsgBox "Object Not Created."
   End If
End Sub

Private Sub oleObj1_Click ()
   ' Determine if the data contained on the Clipboard
   ' can be pasted into the OLE container control.
   If oleObj1.PasteOK Then
      ' Display Paste Special dialog box.
      oleObj1.PasteSpecialDlg
      ' Check to make sure object was created.
      If oleObj1.OLEType = vbOLENone Then
         MsgBox "Object Not Created."
      End If
   End If
End Sub

Once the dialog box is displayed, you do not need to write more code to create the object. The user makes choices in the dialog box and chooses OK to create an object. If the user cancels a dialog, an object is not created.

Note Before displaying the Insert Object or Paste Special dialog box, you may want to determine the value of the OLEType property to see if the OLE container control contains a linked object, embedded object, or no object, as demonstrated in the preceding code example.

The constant vbOLENone and other intrinsic constants are listed in the Visual Basic (VB) object library of the Object Browser.


Determining How an Object is Displayed

Determining How an Object is Displayed in the OLE Container Control

You can use the OLE container control's DisplayType property to indicate if the object will appear as an icon (set DisplayType = 1), or if the object's data will be displayed in the control (set DisplayType = 0). This property also determines the default setting of the Display As Icon check box when the Insert Object and Paste Special dialog boxes are displayed at both run time and design time.

Note Once the OLE container control contains an object, you cannot change its display type. You can, however, delete the linked or embedded object, set the DisplayType property, and then insert a new object.

You use the SizeMode property to determine how an object's icon or data image is displayed in the OLE container control when the control is not UI (user-interface) active. A setting of 0-Clip or 3-Zoom clips the image to fit the control, but it doesn't change the actual size of the image (you might not see all of the image when editing it). An object that is smaller than the control is edited in an area smaller than the control. An object larger than the control fills the entire container area and may be clipped if it is larger than the control area. Alternately, setting SizeMode to 2-AutoSize resizes the control to fit the image.


Activating an Object in the OLE Container Control

While the OLE container control's DoVerb method activates an object at run time, you can use the AppIsRunning property to determine whether the application supplying the object is activated and running. You can set AppIsRunning to True to start the ActiveX component, which causes objects to activate more quickly. You can also set this property to False to close the application or take another appropriate action when the object loses focus.

In-Place Activation

Some embedded objects can be edited (activated) from within the OLE container control. This is called in-place activation, because users can double-click an object in your application and interact with application supplying the object, without switching to a different application or window.

For objects that support in-place activation, you can set the AutoActivate property so that users can activate an object at any time. That is, when the OLE container control's AutoActivate property is set to Double-Click, users can double-click the control to activate it. It is important to note that activating an object launches that object's application if it is not already running.

Note If you want to display the ActiveX component's menus at run time when the user clicks the OLE container control, you must define at least one menu item for the form and set its Visible property to False. This can be an invisible menu if you don't want any menus displayed. See "Creating a User Interface," for more information on displaying an ActiveX component's menus and toolbars in a container application when an object is activated at run time.


Responding to Moving or Sizing the Container

The OLE container control has the ObjectMove event, which is triggered when the object associated with the OLE container control is moved or resized. The arguments to ObjectMove represent the coordinates of the object (excluding its border) within the object's container. If the object is moved off the form, the arguments have values representing the position relative to the upper-left corner of the form. These can be positive or negative. If the Width or Height of the ActiveX component is changed, the OLE container control is notified.

The ObjectMove event is the only way the OLE container control can determine if the object has been moved or resized. An ObjectMove event occurs when the user moves or resizes the object contained in the OLE container control. For example:

Private Sub oleObj1_ObjectMove(Left As Single, Top As _
   Single, Width As Single, Height As Single)
   ' This method resizes the OLE container control to
   ' the new object size.
   oleObj1.Move oleObj1.Left, oleObj1.Top, _
      Width, Height
   ' This method moves the OLE container control
   ' to the new object position.
   oleObj1.Move Left, Top, _
      oleObj1.Width, oleObj1.Height
' Repaints the form.
   Me.Refresh
End Sub


Saving and Retrieving Embedded Data

Data associated with an embedded object is not persistent; that is, when a form containing an OLE container control is closed, any changes to the data associated with that control are lost. To save updated data from an object to a file, you use the OLE container control's SaveToFile method. Once the data has been saved to a file, you can open the file and restore the object.

If the object is linked (OLEType = 0-Linked), then only the link information and an image of the data is saved to the specified file. The object's data is maintained by the application that created the object. If a user wants to save changes to a linked file, the user must choose the Save command from the ActiveX component's File menu because the SaveToFile method applies only to embedded objects.

If the object is embedded (OLEType = 1-Embedded), the object's data is maintained by the OLE container control and can be saved by your Visual Basic application.

Objects in the OLE container control can be saved only to open, binary files.

To save the data from an object to a file

  1. Open a file in binary mode.

  2. Use the SaveToFile method.

The cmdSaveObject_Click event procedure illustrates these steps:

Private Sub cmdSaveObject_Click ()
   Dim FileNum as Integer
   ' Get file number.
   FileNum = FreeFile
   ' Open file to be saved.
   Open "TEST.OLE" For Binary As #FileNum
   ' Save the file.
   oleObj1.SaveToFile FileNum
   ' Close the file.
   Close #FileNum
End Sub

Once an object has been saved to a file, it can be opened and displayed in an OLE container control.

Note When you use the SaveToFile or ReadFromFile methods, the file position is located immediately following the object. Therefore, if you save multiple objects to a file, you should read them in the same order you write them.

To read data from a file into an OLE container control

  1. Open the file in binary mode.

  2. Use the ReadFromFile method on the object.

The cmdOpenObject_Click event procedure illustrates these steps:

Private Sub cmdOpenObject_Click ()
   Dim FileNum as Integer
   ' Get file number.
   FileNum = FreeFile
   ' Open the file.
   Open "TEST.OLE" For Binary As #FileNum
   ' Read the file.
   oleObj1.ReadFromFile FileNum
   ' Close the binary file.
   Close #FileNum
End Sub

The Updated event is invoked each time the contents of an object is changed. This event is useful for determining if an object's data has been changed because it was last saved. To do this, set a global variable in the Updated event indicating the object needs to be saved. When you save the object, reset the variable.


Responding to Mouse and Keyboard Events

Your Visual Basic applications can respond to a variety of mouse events and keyboard events. For example, forms, picture boxes, and image controls can detect the position of the mouse pointer, can determine whether a left or right mouse button is being pressed, and can respond to different combinations of mouse buttons and SHIFT, CTRL, or ALT keys. Using the key events, you can program controls and forms to respond to various key actions or interpret and process ASCII characters.

In addition, Visual Basic applications can support both event-driven drag-and-drop and OLE drag-and-drop features. You can use the Drag method with certain properties and events to enable operations such as dragging and dropping controls. OLE drag and drop gives your applications all the power you need to exchange data throughout the Windows environment — and much of this technology is available to your application without writing code.

You can also use the mouse or keyboard to manage the processing of long background tasks, which allows your users to switch to other applications or interrupt background processing.

Other actions and events that involve the mouse or keyboard (the Click and DblClick events, the Focus events, and the Scroll event) are not covered in this chapter. For more information on the Click and DblClick events, see the topics "Clicking Buttons to Perform Actions" and "Understanding Focus" in "Forms, Controls, and Menus," and see "Click Event" and “DblClick Event" in the Language Reference. Also see "Scroll Event."

Topics

Responding to Mouse Events

How to use the MouseDown, MouseUp, and MouseMove events to control or enhance mouse action.
Detecting Mouse Buttons

How to detect various mouse button states using the button argument.
Detecting SHIFT, CTRL, and ALT States

How to detect various mouse button and SHIFT, CTRL, and ALT key states using the shift argument.
Dragging and Dropping

Enabling event-driven drag-and-drop within your Visual Basic application.
OLE Drag and Drop

How to use this powerful Windows technology to move or copy data from control to control within Visual Basic or from Visual Basic to other Windows applications, and vice versa.
Customizing the Mouse Pointer

Changing the mouse pointer to inform the user about the state and functionality of your application.
Responding to Keyboard Events

How to use the KeyDown, KeyUp, and KeyPress events to respond to various key actions or interpret and process ASCII characters.
Interrupting Background Processing

Managing intermittent and long-running background tasks.

Sample application

Mouse.vbp

Many of the code examples in this chapter are taken from the Mouse.vbp sample application. You'll find this application listed in the Samples directory.


Responding to Mouse Events

You can use the MouseDown, MouseUp, and MouseMove events to enable your applications to respond to both the location and the state of the mouse. (This list excludes drag events, which are introduced in "Dragging and Dropping" later in this chapter.) These mouse events are recognized by most controls.

Event | Description
MouseDown | Occurs when the user presses any mouse button.
MouseUp | Occurs when the user releases any mouse button.
MouseMove | Occurs each time the mouse pointer is moved to a new point on the screen.

A form can recognize a mouse event when the pointer is over a part of the form where there are no controls. A control can recognize a mouse event when the pointer is over the control.

When the user holds down a mouse button, the object continues to recognize all mouse events until the user releases the button. This is true even when the pointer is moved off the object.

The three mouse events use the following arguments.

Argument | Description
Button | A bit-field argument in which the three least-significant bits give the status of the mouse buttons.
Shift | A bit-field argument in which the three least-significant bits give the status of the SHIFT, CTRL, and ALT keys.
x, y | Location of the mouse pointer, using the coordinate system of the object that receives the mouse event.

A bit-field argument returns information in individual bits, each indicating whether a certain condition is on or off. Using binary notation, the three leftmost bits are referred to as most-significant and the three rightmost bits as least-significant. Techniques for programming with these arguments are described in "Detecting Mouse Buttons" and "Detecting SHIFT, CTRL, and ALT States" later in this chapter.


The MouseDown Event

MouseDown is the most frequently used of the three mouse events. It can be used to reposition controls on a form at run time or to create graphical effects, for instance. The MouseDown event is triggered when a mouse button is pressed.

Note The mouse events are used to recognize and respond to the various mouse states as separate events and should not be confused with the Click and DblClick events. The Click event recognizes when a mouse button has been pressed and released, but only as a single action — a click. The mouse events also differ from the Click and DblClick events in that they enable you to distinguish between the left, right, and middle mouse buttons and the SHIFT, CTRL, and ALT keys.

Using MouseDown with the Move Method

The MouseDown event is combined with the Move method to move a command button to a different location on a form. The new location is determined by the position of the mouse pointer: When the user clicks anywhere on the form (except on the control), the control moves to the cursor location.

A single procedure, Form_MouseDown, performs this action:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Command1.Move X, Y
End Sub

The Move method places the command button control's upper-left corner at the location of the mouse pointer, indicated by the x and y arguments. You can revise this procedure to place the center of the control at the mouse location:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Command1.Move (X - Command1.Width / 2), _
   (Y - Command1.Height / 2)
End Sub

Using MouseDown with the Line Method

The Click-A-Line sample application responds to a mouse click by drawing a line from the previous drawing location to the new position of the mouse pointer. This application uses the MouseDown event and the Line method. Using the following syntax, the Line method will draw a line from the last point drawn to the point (x2, y2):

Line – (x2, y2)

Click-A-Line uses a blank form with one procedure, Form_MouseDown:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Line -(X, Y)
End Sub

The first line starts at the upper-left corner, which is the default origin. Thereafter, whenever the mouse button is pressed, the application draws a straight line extending from the previous line to the present location of the mouse pointer. The result is a series of connected lines, as shown in Figure 11.1.

Figure 11.1 Connecting lines are drawn whenever MouseDown is invoked

For More Information See "MouseDown Event" in the Language Reference.


The MouseMove Event

The MouseMove event occurs when the mouse pointer is moved across the screen. Both forms and controls recognize the MouseMove event while the mouse pointer is within their borders.

Using MouseMove with the Line Method

Graphics methods can produce very different effects when used in a MouseMove procedure instead of in a MouseDown procedure. For example, in the topic "The MouseDown Event" earlier in this chapter, the Line method drew connected line segments. In the Scribble application described below, the same method is used in a Form_MouseMove procedure to produce a continuous curved line instead of connected segments.

In the Scribble application, the MouseMove event is recognized whenever the mouse pointer changes position. The following code draws a line between the current and previous location.

Private Sub Form_MouseMove (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Line -(X, Y)
End Sub

Like the MouseDown procedure, the line created by the MouseMove procedure starts at the upper-left corner, as shown in Figure 11.2.

Figure 11.2 The MouseMove event and the Line method create a simple sketch program

How MouseMove Works

How many times does the MouseMove event get called as the user moves the pointer across the screen? Or, to put it another way, when you move the pointer from the top of the screen to the bottom, how many locations are involved?

Visual Basic doesn't necessarily generate a MouseMove event for every pixel the mouse moves over. The operating environment generates a limited number of mouse messages per second. To see how often MouseMove events are actually recognized, you can enhance the Scribble application with the following code so that it draws a small circle at each location where a MouseMove event is recognized. The results are shown in Figure 11.3.

Private Sub Form_MouseMove (Button As Integer,_
      Shift As Integer, X As Single, Y As Single)
   Line -(X, Y)
   Circle (X, Y), 50
End Sub

Figure 11.3 A demonstration of where MouseMove events occur

Note that the faster the user moves the pointer, the fewer MouseMove events are recognized between any two points. Many circles close together indicate that the user moved the mouse slowly.

Your application can recognize many MouseMove events in quick succession. Therefore, a MouseMove event procedure shouldn't do anything that requires large amounts of computing time.

For More Information See "MouseMove Event" in the Language Reference.


The MouseUp Event

The MouseUp event occurs when the user releases the mouse button. MouseUp is a useful companion to the MouseDown and MouseMove events. The example below illustrates how all three events can be used together.

The Scribble application is more useful if it allows drawing only while the mouse button is held down and stops drawing when the button is released. To do this, the application would have to respond to three actions:

  • The user presses the mouse button (MouseDown).

  • The user moves the mouse pointer (MouseMove).

  • The user releases the mouse button (MouseUp).

MouseDown and MouseUp will tell the application to turn drawing on and off. You specify this by creating a form-level variable that represents the drawing state. Type the following statement in the Declarations section of the form code module:

Dim DrawNow As Boolean

DrawNow will represent two values: True will mean "draw a line," and False will mean "do not draw a line."

Because variables are initialized to 0 (False) by default, the application starts with drawing off. Then the first line in the MouseDown and MouseUp procedures turns drawing on or off by setting the value of the form-level variable DrawNow:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   DrawNow = True
   CurrentX = X
   CurrentY = Y
End Sub

Private Sub Form_MouseUp (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   DrawNow = False
End Sub

The MouseMove procedure draws a line only if DrawNow is True. Otherwise, it takes no action:

Private Sub Form_MouseMove (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If DrawNow Then Line -(X, Y)
End Sub

Each time the user presses a mouse button, the MouseDown event procedure is executed and turns drawing on. Then as the user holds the Mouse button down, the MouseMove event procedure is executed repeatedly as the pointer is dragged across the screen.

Note that the Line method omits the first endpoint, causing Visual Basic to start drawing at the mouse pointer's current coordinates. By default, the drawing coordinates correspond to the last point drawn; the form's CurrentX and CurrentY properties were reset in the Form_MouseDown procedure.

For More Information See "MouseUp Event" in the Language Reference.


Detecting Mouse Buttons

You can make your applications more powerful by writing code that responds differently to mouse events, depending on which mouse button is used or whether the SHIFT, CTRL, or ALT key is pressed. To provide these options, you use the arguments button and shift with the MouseDown, MouseUp, and MouseMove event procedures. Techniques for using the shift argument are described in "Detecting SHIFT, CTRL , and ALT States" later in this chapter.

The MouseDown, MouseUp, and MouseMove events use the button argument to determine which mouse button or buttons are pressed. The button argument is a bit-field argument — a value in which each bit represents a state or condition. These values are expressed as integers. The three least-significant (lowest) bits represent the left, right, and middle mouse buttons, as shown in Figure 11.4.

Figure 11.4 How bits represent the state of the mouse

The default value of each bit is 0 (False). If no buttons are pressed, the binary value of the three bits is 000. If you press the left button, the binary value, or pattern, changes to 001. The left-button bit-value changes from 0 (False) to 1 (True).

The button argument uses either a decimal value or an constant to represent these binary patterns. The following table lists the binary value of the bits, the decimal equivalent, and the Visual Basic constant:

Binary Value | Decimal Value | Constant | Meaning
001 | 1 | vbLeftButton | The left button is pressed.
010 | 2 | vbRightButton | The right button is pressed.
100 | 4 | vbMiddleButton | The middle button is pressed.

Note Visual Basic provides constants that represent the binary values of the button and shift arguments. These constants can be used interchangeably with their equivalent decimal values. Not all values have corresponding constants, however. The values for some button and/or shift combinations are derived by simply adding decimal values.

The middle button is assigned to decimal value 4. Pressing the left and right buttons simultaneously produces a single digit value of 3 (1+2). On a three-button mouse, pressing all three buttons simultaneously produces the decimal value of 7 (4+2+1). The following table lists the remaining button values derived from the possible button combinations:

Binary Value | Decimal Value | Constant | Meaning
000 | 0 |  | No buttons are pressed.
011 | 3 | vbLeftButton + vbRightButton | The left and right buttons are pressed.
101 | 5 | vbLeftButton + vbMiddleButton | The left and middle buttons are pressed.
110 | 6 | vbRightButton + vbMiddleButton | The right and middle buttons are pressed.
111 | 7 | vbRightButton + vbMiddleButton + vbLeftButton | All three buttons are pressed.


Using Button with MouseDown and MouseUp

You use the button argument with MouseDown to determine which button is being pressed and with MouseUp to determine which button has been released. Because only one bit is set for each event, you can't test for whether two or more buttons are being used at the same time. In other words, MouseDown and MouseUp only recognize one button press at a time.

Note In contrast, you can use the MouseMove event to test for whether two or more buttons are being pressed simultaneously. You can also use MouseMove to test for whether a particular button is being pressed, regardless of whether or not another button is being pressed at the same time. For more information, see "Using Button with MouseMove" later in this chapter.

You can specify which button causes a MouseDown or MouseUp event with simple code. The following procedure tests whether button equals 1, 2, or 4:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = 1 Then Print "You pressed _
      the left button."
   If Button = 2 Then Print "You pressed _
      the right button."
   If Button = 4 Then Print "You pressed _
      the middle button."
End Sub

If the user presses more than one button, Visual Basic interprets that action as two or more separate MouseDown events. It sets the bit for the first button pressed, prints the message for that button, and then does the same for the next button. Similarly, Visual Basic interprets the release of two or more buttons as separate MouseUp events.

The following procedure prints a message when a pressed button is released:

Private Sub Form_MouseUp(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = 1 Then Print "You released _
      the left button."
   If Button = 2 Then Print "You released _
      the right button."
   If Button = 4 Then Print "You released _
      the middle button."
End Sub


Using Button with MouseMove

For the MouseMove event, button indicates the complete state of the mouse buttons — not just which button caused the event, as with MouseDown and MouseUp. This additional information is provided because all, some, or none of the bits might be set. This compares with just one bit per event in the MouseDown and MouseUp procedures.

Testing for a Single Button

If you test MouseMove for equality to 001 (decimal 1), you're testing to see if only the left mouse button is being held down while the mouse is moved. If another button is held down with the left button, the following code doesn't print anything:

Private Sub Form_MouseMove (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = 1 Then Print "You're pressing _
      only the left button."
End Sub

To test for whether a particular button is down, use the And operator. The following code prints the message for each button pressed, regardless of whether another button is pressed:

Private Sub Form_MouseMove (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button And 1 Then Print "You're pressing _
      the left button."
   If Button And 2 Then Print "You're pressing _
      the right button."
End Sub

Pressing both buttons simultaneously prints both messages to the form. The MouseMove event recognizes multiple button states.

Testing for Multiple Buttons

In most cases, to isolate which button or buttons are being pressed, you use the MouseMove event.

Building on the previous examples, you can use the If…Then…Else statement to determine whether the left, right, or both buttons are being pressed. The following example tests for the three button states (left button pressed, right button pressed, and both buttons pressed) and prints the corresponding message.

Add the following code to the form's MouseMove event:

Private Sub Form_MouseMove(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = 1 Then
      Print "You're pressing the left button."
   ElseIf Button = 2 Then
      Print "You're pressing the right button."
   ElseIf Button = 3 Then
      Print "You're pressing both buttons."
   End If
End Sub

You could also use the And operator with the Select Case statement to determine button and shift states. The And operator combined with the Select Case statement isolates the possible button states of a three-button mouse and then prints the corresponding message.

Create a variable called ButtonTest in the Declarations section of the form:

Dim ButtonTest as Integer

Add the following code to the form's MouseMove event:

Private Sub Form_MouseMove(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
ButtonTest = Button And 7
   Select Case ButtonTest
      Case 1 ' or vbLeftButton
         Print "You're pressing the left button."
      Case 2 ' or vbRightButton
         Print "You're pressing the right button."
      Case 4 ' or vbMiddleButton
         Print "You're pressing the middle button."
      Case 7
         Print "You're pressing all three buttons."
   End Select
End Sub


Using Button to Enhance Graphical Mouse Applications

You can use the button argument to enhance the Scribble application described in "The MouseMove Event" earlier in this chapter. In addition to drawing a continuous line when the left mouse button is pressed and stopping when the button is released, the application can draw a straight line from the last point drawn when the user presses the right button.

When writing code, it is often helpful to note each relevant event and the desired response. The three relevant events here are the mouse events:

  • Form_MouseDown: This event takes a different action depending on the state of the mouse buttons: If the left button is down, set DrawNow to True and reset drawing coordinates; If the right button is down, draw a line.

  • Form_MouseUp: If the left button is up, set DrawNow to False.

  • Form_MouseMove: If DrawNow is True, draw a line.

The variable DrawNow is declared in the Declarations section of the form:

Dim DrawNow As Boolean

The MouseDown procedure has to take a different action, depending on whether the left or right mouse button caused the event:

Private Sub Form_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = vbLeftButton Then
      DrawNow = True
      CurrentX = X
      CurrentY = Y
   ElseIf Button = vbRightButton Then
      Line -(X, Y)
   End If
End Sub

The following MouseUp procedure turns off drawing only when the left button is released:

Private Sub Form_MouseUp (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If Button = vbLeftButton Then DrawNow = False
End Sub

Note that within the MouseUp procedure, a bit set to 1 (vbLeftButton) indicates that the corresponding mouse button is released and drawing is turned off.

The following MouseMove procedure is identical to the one in the version of the Scribble application found in "The MouseMove Event" earlier in this chapter.

Private Sub Form_MouseMove (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   If DrawNow Then Line -(X, Y)
End Sub


Detecting SHIFT, CTRL, and ALT States

The mouse and keyboard events use the shift argument to determine whether the SHIFT, CTRL, and ALT keys are pressed and in what, if any, combination. If the SHIFT key is pressed, shift is 1; if the CTRL key is pressed, shift is 2; and if the ALT key is pressed, shift is 4. To determine combinations of these keys, use the total of their values. For example, if SHIFT and ALT are pressed, shift equals 5 (1 + 4).

The three least-significant bits in shift correspond to the state of the SHIFT, CTRL, and ALT keys, as shown in Figure 11.5.

Figure 11.5 How bits represent the state of the SHIFT, CTRL, and ALT keys

Any or all of the bits in shift can be set, depending on the state of the SHIFT, CTRL, and ALT keys. These values and constants are listed in the following table:

Binary Value | Decimal Value | Constant | Meaning
001 | 1 | vbShiftMask | The SHIFT key is pressed.
010 | 2 | vbCtrlMask | The CTRL key is pressed.
100 | 4 | vbAltMask | The ALT key is pressed.
011 | 3 | vbShiftMask + vbCtrlMask | The SHIFT and CTRL keys are pressed.
101 | 5 | vbShiftMask + vbAltMask | The SHIFT and ALT keys are pressed.
110 | 6 | vbCtrlMask + vbAltMask | The CTRL and ALT keys are pressed.
111 | 7 | vbCtrlMask + vbAltMask + vbShiftMask | The SHIFT, CTRL, and ALT keys are pressed.

As with the mouse events' button argument, you can use the If…Then…Else statement or the And operator combined with the Select Case statement to determine whether the SHIFT, CTRL, or ALT keys are being pressed and in what, if any, combination.

Open a new project and add the variable ShiftTest to the Declarations section of the form:

Dim ShiftTest as Integer

Add the following code to the form's MouseDown event:

Private Sub Form_MouseDown(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   ShiftTest = Shift And 7
   Select Case ShiftTest
      Case 1 ' or vbShiftMask
         Print "You pressed the SHIFT key."
      Case 2 ' or vbCtrlMask
         Print "You pressed the CTRL key."
      Case 4 ' or vbAltMask
         Print "You pressed the ALT key."
      Case 3
         Print "You pressed both SHIFT and CTRL."
      Case 5
         Print "You pressed both SHIFT and ALT."
      Case 6
         Print "You pressed both CTRL and ALT."
      Case 7
         Print "You pressed SHIFT, CTRL, and ALT."
      End Select
End Sub


Dragging and Dropping

When you design Visual Basic applications, you often drag controls around on the form. The drag-and-drop features in Visual Basic allow you to extend this ability to the user at run time. The action of holding a mouse button down and moving a control is called dragging, and the action of releasing the button is called dropping.

Note Dragging a control at run time doesn't automatically change its location — you must program the relocation yourself, as described in "Changing the Position of a Control." Often, dragging is used only to indicate that some action should be performed; the control retains its original position after the user releases the mouse button.

Using the following drag-and-drop properties, events, and method, you can specify both the meaning of a drag operation and how dragging can be initiated (if at all) for a given control.

Category | Item | Description
Properties | DragMode | Enables automatic or manual dragging of a control.
 | DragIcon | Specifies what icon is displayed when the control is dragged.
Events | DragDrop | Recognizes when a control is dropped onto the object.
 | DragOver | Recognizes when a control is dragged over the object.
Methods | Drag | Starts or stops manual dragging.

All controls except menus, timers, lines, and shapes support the DragMode and DragIcon properties and the Drag method. Forms recognize the DragDrop and DragOver events, but they don't support the DragMode and DragIcon properties or the Drag method.

Note Controls can only be dragged when they do not have the focus. To prevent a control from getting the focus, set its TabStop property to False.


Enabling Automatic Drag Mode

To allow the user to drag a control, set its DragMode property to 1-Automatic.

When you set dragging to Automatic, dragging is always "on." For more control over dragging operations, use the 0-Manual setting described in "Controlling When Dragging Starts or Stops" later in this chapter.

Note While an automatic drag operation is taking place, the control being dragged doesn't recognize other mouse events.


Selecting the Drag Icon

Changing the Drag Icon

When dragging a control, Visual Basic uses a gray outline of the control as the default drag icon. You can substitute other images for the outline by setting the DragIcon property. This property contains a Picture object that corresponds to a graphic image.

The easiest way to set the DragIcon property is to use the Properties window. Select the DragIcon property, and then click the Properties button to select a file containing a graphic image from the Load Icon dialog box.

You can assign icons to the DragIcon property from the Icon Library included with Visual Basic. (The icons are located in the \Program files\Microsoft Visual Basic\Icons directory.) You can also create your own drag icons with a graphics program.

At run time, you can select a drag icon image by assigning the DragIcon property of one control to the same property of another:

Set Image1.DragIcon = Image2.DragIcon

You can also set the DragIcon property at run time by assigning the Picture property of one control to the DragIcon property of another:

Set Image1.DragIcon = Image3.Picture

Or, you can use the LoadPicture function:

Set Image1.DragIcon = LoadPicture("c:\Program _
files\Microsoft Visual Basic\Icons _
\Computer\Disk04.ico")

For More Information For information on the Picture property and the LoadPicture function, see "Working with Text and Graphics.". Also see "Picture Property" and "LoadPicture Function" in the Language Reference.


Responding When the User Drops the Object

When the user releases the mouse button after dragging a control, Visual Basic generates a DragDrop event. You can respond to this event in many ways. Remember that the control doesn't automatically move to the new location, but you can write code to relocate the control to the new location (indicated by the last position of the gray outline). See "Changing the Position of a Control" for more information.

Two terms are important when discussing drag-and-drop operations: source and target.

Term | Meaning
Source | The control being dragged. This control can be any object except a menu, timer, line, or shape.
Target | The object onto which the user drops the control. This object, which can be a form or control, recognizes the DragDrop event.

A control becomes the target if the mouse position is within its borders when the button is released. A form is the target if the pointer is in a blank portion of the form.

The DragDrop event provides three arguments: source, x, and y. The source argument is a reference to the control that was dropped onto the target.

Because source is declared As Control, you use it just as you would a control — you can refer to its properties or call one of its methods.

The following example illustrates how the source and target interact. The source is an Image control with its Picture property set to load a sample icon file representing a few file folders. Its DragMode property has been set to 1-Automatic and its DragIcon property to a sample drag-and-drop icon file. The target, also an image control, contains a picture of an open file cabinet.

Add the following procedure to the second image control's DragDrop event:

Private Sub Image2_DragDrop(Source As Control, _
      X As Single, Y As Single)
   Source.Visible = False
   Image2.Picture = LoadPicture("c:\Program _
      Files\Microsoft Visual _
      Basic\Icons\Office\Files03a.ico")
End Sub

Dragging and dropping Image1 onto Image2 causes Image1 to vanish and Image2 to change its picture to that of a closed file cabinet. Using the source argument, the Visible property of Image1 was changed to False.

Note You should use the source argument carefully. Although you know that it always refers to a control, you don't necessarily know which type of control. For example, if the control is a text box and you attempt to refer to Source.Value, the result is a run-time error because text boxes have no Value property.

You can use the If...Then...Else statement with the TypeOf keyword to determine what kind of control was dropped.

For More Information See "If…Then…Else" in the Language Reference and see "Programming with Objects."


Controlling When Dragging Starts or Stops

Visual Basic has a Manual setting for the DragMode property that gives you more control than the Automatic setting. The Manual setting allows you to specify when a control can and cannot be dragged. (When DragMode is set to Automatic, you can always drag the control as long as the setting isn't changed.)

For instance, you may want to enable dragging in response to MouseDown and MouseUp events, or in response to a keyboard or menu command. The Manual setting also allows you to recognize a MouseDown event before dragging starts, so that you can record the mouse position.

To enable dragging from code, leave DragMode in its default setting (0-Manual). Then use the Drag method whenever you want to begin or stop dragging an object. Use the following Visual Basic constants to specify the action of the Drag argument.

Constant | Value | Meaning
vbCancel | 0 | Cancel drag operation
vbBeginDrag | 1 | Begin drag operation
vbEndDrag | 2 | End drag operation

The syntax for the Drag method is as follows:

[object.]Drag action

If action is set to vbBeginDrag, the Drag method initiates dragging of the control. If action is set to vbEndDrag, the control is dropped, causing a DragDrop event. If action is set to vbCancel, the drag is canceled. The effect is similar to giving the value vbEndDrag, except that no DragDrop event occurs.

Building on the example given in "Responding When the User Drops the Object" earlier in this chapter, you can add a MouseDown event for Image1 that illustrates the Drag method. Set the Image1 DragMode property to 0-Manual, then add the following procedure:

Private Sub Image1_MouseDown(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Image1.Drag vbBeginDrag
   Set Image1.DragIcon = LoadPicture("c:\Program _
      files\ Microsoft Visual _
      Basic\Icons\Dragdrop\Dragfldr.ico")
End Sub

Adding a DragOver event procedure to Image2 allows you to terminate dragging when the source enters the target. This example closes the file cabinet when Image1 is passed over Image2.

Private Sub Image2_DragOver(Source As Control, _
      X As Single, Y As Single, State As Integer)
   Source.Drag vbEndDrag
   Source.Visible = False
   Image2.Picture = LoadPicture("c:\Program _
      files\Microsoft Visual _
      Basic\Icons\Office\Files03a.ico")
End Sub

Adding a third Image control to the form demonstrates canceling a drag operation. In this example the Image3 Picture property contains an icon of a trash can. Using the DragOver event and the source argument, dragging the files over Image3 cancels the drag operation.

Private Sub Image3_DragOver(Source As Control, _
      X As Single, Y As Single, State As Integer)
   Source.Drag vbCancel
End Sub

For More Information See "Drag-and-Drop Constants" in the Language Reference.


Causing Control Movement

Changing the Position of a Control

You may want the source control to change position after the user releases the mouse button. To move a control to the new mouse location, use the Move method with any control that has been drag-enabled.

You can reposition a control when it is dragged and dropped to any location on the form not occupied by another control. To illustrate this, start a new Visual Basic project, add an Image control to the form and assign it any icon or bitmap by setting the Picture property, and then change the Image control's DragMode property to 1-Automatic.

Add the following procedure to the form's DragDrop event:

Private Sub Form_DragDrop (Source As Control, _
      X As Single, Y As Single)
   Source.Move X, Y
End Sub

This code may not produce precisely the effects you want, because the upper-left corner of the control is positioned at the mouse location. This code positions the center of the control at the mouse location:

Private Sub Form_DragDrop (Source As Control, _
      X As Single, Y As Single)
   Source.Move (X - Source.Width / 2), _
      (Y - Source.Height / 2)
End Sub

The code works best when the DragIcon property is set to a value other than the default (the gray rectangle). When the gray rectangle is being used, the user usually wants the control to move precisely into the final position of the gray rectangle. To do this, record the initial mouse position within the source control. Then use this position as an offset when the control is moved.

To record the initial mouse position

  1. Specify manual dragging of the control.

  2. Declare two form-level variables, DragX and DragY.

  3. Turn on dragging when a MouseDown event occurs.

  4. Store the value of x and y in the form-level variables in this event.

The following example illustrates how to cause drag movement for an image control named Image1. The control's DragMode property should be set to 0-Manual at design time. The Declarations section contains the form-level variables DragX and DragY, which record the initial mouse position within the Image control:

Dim DragX As Single, DragY As Single

The MouseDown and MouseUp procedures for the control turn dragging on and drop the control, respectively. In addition, the MouseDown procedure records the mouse position inside the control at the time dragging begins:

Private Sub Image1_MouseDown (Button As Integer, _
      Shift As Integer, X As Single, Y As Single)
   Image1.Drag 1
   DragX = X
   DragY = Y
End Sub

The Form_DragDrop procedure actually moves the control. To simplify this example, assume that Image1 is the only control on the form. The target can therefore only be the form itself. The Form_DragDrop procedure repositions the control, using DragX and DragY as offsets:

Private Sub Form_DragDrop (Source As Control, _
      X As Single, Y As Single)
   Source.Move (X - DragX), (Y - DragY)
End Sub

Note that this example assumes that Image1 and the form use the same units in their respective coordinate systems. If they don't, then you'll have to convert between units.

For More Information For information on coordinate systems, see "Working with Text and Graphics" and "ScaleMode Property" in the Language Reference.


OLE Drag and Drop

One of the most powerful and useful features you can add to your Visual Basic applications is the ability to drag text or graphics from one control to another, or from a control to another Windows application, and vice versa. OLE drag-and-drop allows you to add this functionality to your applications.

With OLE drag and drop, you’re not dragging one control to another control to invoke some code (as with the drag and drop discussed earlier in this chapter); you’re moving data from one control or application to another control or application. For example, you can select and drag a range of cells in Excel and then drop the range of cells into a DataGrid control in your application.

Nearly all Visual Basic controls support OLE drag-and-drop to some degree. In addition, some standard and ActiveX controls (those provided in the Professional and Enterprise editions of Visual Basic) provide automatic support for OLE drag-and-drop, which means that the control supports automatic settings in both their OLEDragMode and OLEDropMode properties, and that no code needs to be written to either drag from or drop to the control. This is opposed to manual dragging and dropping, in which the behavior of the drag or drop must be programmed by you.

Some of the controls that support both automatic OLEDragMode and OLEDropMode include the PictureBox, Label, and TextBox controls, among others. To enable automatic OLE dragging and dropping for these controls, set both the OLEDragMode and OLEDropMode properties to Automatic.

Some controls support automatic OLE dragging but only manual dropping, and some support automatic OLE dropping but only manual dragging. For instance, the ComboBox control supports both manual and automatic dragging, but doesn’t support automatic dropping. This is because if, for example, you drag an item into a ComboBox, Visual Basic has no way of knowing exactly where to place the new item. However, manual dropping is available so that you can programmatically place items wherever you like in the ComboBox. To enable automatic dragging from these controls, set the OLEDragMode property to Automatic.

Some controls support only the manual OLE drag-and-drop events, meaning that you can program them to act either as the source or target of the OLE drag-and-drop operations.

Note To determine if other ActiveX controls support OLE drag and drop, load the control into Visual Basic and check for the existence of the OLEDragMode and OLEDropMode properties, or for the OLEDrag method. (A control that does not have automatic support for OLE drag will not have the OLEDragMode property, but it will have an OLEDrag method if it supports OLE drag through code.)

Note Forms, MDI forms, Document Objects, User Controls, and Property Pages contain the OLEDropMode property and provide support for manual dragging and dropping only.

Using the following OLE drag-and-drop properties, events, and method, you can specify how a given control responds to dragging and dropping.

Category | Item | Description
Properties | OLEDragMode | Enables automatic or manual dragging of a control (if the control supports manual but not automatic OLE drag, it will not have this property but it will support the OLEDrag method and the OLE drag-and-drop events).
 | OLEDropMode | Specifies how the control will respond to a drop.
Events | OLEDragDrop | Recognizes when a source object is dropped onto a control.
 | OLEDragOver | Recognizes when a source object is dragged over a control.
 | OLEGiveFeedback | Provides customized drag icon feedback to the user, based on the source object.
 | OLEStartDrag | Specifies which data formats and drop effects (copy, move, or refuse data) the source supports when dragging is initiated.
 | OLESetData | Provides data when the source object is dropped.
 | OLECompleteDrag | Informs the source of the action that was performed when the object was dropped into the target.
Method | OLEDrag | Starts manual dragging.

Automatic vs. Manual Dragging and Dropping

It is helpful to think of OLE drag-and-drop implementation as either automatic or manual.

Automatic dragging and dropping means that, for example, you can drag text from one text box control to another by simply setting the OLEDragMode and OLEDropMode properties of these controls to Automatic: You don’t need to write any code to respond to any of the OLE drag-and-drop events. When you drag a range of cells from Excel into a Word document, you’ve performed an automatic drag-and-drop operation. Depending upon how a given control or application supports OLE drag and drop and what type of data is being dragged, automatically dragging and dropping data may be the best and simplest method.

Manual dragging and dropping means that you have chosen (or have been forced to) manually handle one or more of the OLE drag-and-drop events. Manual implementation of OLE drag and drop may be the better method when you want to gain greater control over each step in the process, to provide the user with customized visual feedback, to create your own data format. Manual implementation is the only option when a control does not support automatic dragging and dropping.

It is also helpful to define the overall model of the OLE drag-and-drop operation. In a drag and drop operation, the object from which data is dragged is referred to as the source. The object into which the data is dropped is referred to as the target. Visual Basic provides the properties, events, and method to control and respond to actions affecting both the source and the target. It is also helpful to recognize that the source and the target may be in different applications, in the same application, or even in the same control. Depending upon the scenario, you may need to write code for either the source or target, or both.


Enabling Automatic OLE Drag and Drop

If your controls support automatic dragging and dropping, you can drag data from and/or drop data into a Visual Basic control by setting the control’s OLEDragMode and/or OLEDropMode properties to Automatic. For instance, you may want to drag text from a text box control into a Word for Windows document, or allow the text box control to accept data dragged from the Word for Windows document.

To allow dragging from the text box control, set the OLEDragMode property to Automatic. At run time, you can select text typed into the text box control and drag it into the open Word for Windows document.

When you drag text from the text box control into a Word for Windows document, it is, by default, moved rather than copied into the document. If you hold the CTRL key down while dropping text, it will be copied rather than moved. This is the default behavior for all objects or applications that support OLE drag-and-drop. To restrict this operation by allowing data to only be moved or only be copied, you need to modify the automatic behavior by using the manual dragging and dropping techniques. For more information, see "Using the Mouse and Keyboard to Modify Drop Effects and User Feedback."

To allow the text box control to automatically retrieve data in a OLE drag-and-drop operation, set its OLEDropMode property to Automatic. At run time, data dragged from an OLE-enabled application into the text box control will be moved rather than copied unless you hold down the CTRL key during the drop, or alter the default behavior through code.

Automatic support for dragging and dropping data has its limitations; some of these limitations are derived from the functionality of the controls themselves. For instance, if you move text from a Word for Windows document into a text box control, all the rich text formatting in the Word document will be stripped out because the text box control doesn’t support this formatting. Similar limitations exist for most controls. Another limitation of automatic operations is that you don't have complete control over what kind of data is dragged and/or dropped.

Note When dragging data, you may notice that the mouse pointer indicates if the object that it is passing over supports OLE drag and drop for the type of data that you are dragging. If the object supports OLE drag and drop for the type of data, the “drop” pointer is displayed. If the object does not, a "no drop" pointer is displayed.


The OLE Drag and Drop DataObject Object

OLE drag-and-drop uses the same source and target model as the simple event-driven drag-and-drop techniques discussed in “Dragging and Dropping.” In this case, however, you’re not dragging one control to another control to invoke some code; you’re moving data from one control or application to another control or application. For example, the user selects and drags a range of cells in Excel (source) then drops the range of cells into the DataGrid control (target) in your application.

In Visual Basic, the vehicle, or repository, of this data is the DataObject object — it is the means by which data is moved from the source to the target. It does this by providing the methods needed to store, retrieve, and analyze the data. The following table lists the property and methods used by the DataObject object:

Category | Item | Description
Property | Files | Holds the names of files dragged to or from the Windows Explorer.
Methods | Clear | Clears the content of the DataObject object.
 | GetData | Retrieves data from the DataObject object.
 | GetFormat | Determines if a specified data format is available in the DataObject object.
 | SetData | Places data into the DataObject object, or indicates that a specified format is available upon request.

Used with the OLE drag-and-drop events, these methods allow you to manage data in the DataObject object on both the source and target sides (if both are within your Visual Basic application). For instance, you can place data into the DataObject object on the source side using the SetData method, and then use the GetData method to accept the data on the target side.

The Clear method is used to clear the content of the DataObject object on the source side when the OLEStartDrag event is triggered. When data from a control is dragged in an automatic drag operation, its data formats are placed into the DataObject object before the OLEStartDrag event is triggered. If you don’t want to use the default formats, you use the Clear method. If you want to add to the default data formats, you do not use the Clear method.

The Files property allows you to store the names of a range of files that can be then dragged into a drop target. See “Dragging Files from the Windows Explorer” for more information on this property.

You can also specify the format of the data being transferred. The SetData and GetData methods use the following arguments to place or retrieve data in the DataObject object:

Argument | Description
Data | Allows you to specify the type of data that is placed into the DataObject object (optional argument if the format argument has been set; otherwise, it's required).
Format | Allows you to set several different formats that the source can support, without having to load the data for each (optional argument if the data argument has been set or if Visual Basic understands the format; otherwise, it's required).

Note When data is dropped onto the target and no format has been specified, Visual Basic is able to detect if it is a bitmap, metafile, enhanced metafile, or text. All other formats must be specified explicitly or an error will be generated.

The format argument uses the following constants or values to specify the format of the data:

Constant | Value | Meaning
vbCFText | 1 | Text
vbCFBitmap | 2 | Bitmap (.bmp)
vbCFMetafile | 3 | Metafile (.wmf)
vbCFEMetafile | 14 | Enhanced metafile (.emf)
vbCFDIB | 8 | Device-independent bitmap (.dib or .bmp)
vbCFPalette | 9 | Color palette
vbCFFiles | 15 | List of files
vbCFRTF | -16639 | Rich text format (.rtf)

The SetData, GetData, and GetFormat methods use the data and format arguments to return either the type of data in the DataObject object or to retrieve the data itself if the format is compatible with the target. For example:

Private Sub txtSource_OLEStartDrag(Data As _
      VB.DataObject, AllowedEffects As Long)
   Data.SetData txtSource.SelText, vbCFText
End Sub

In this example, data is the text selected in a textbox and format has been specified as text (vbCFText).

Note You should use the vbCFDIB data format instead of vbCFBitmap and vbCFPalette, in most cases. The vbCFDIB format contains both the bitmap and palette and is therefore the preferred method of transferring a bitmap image. You can, however, also specify the vbCFBitmap and vbCFPalette for completeness. If you chose not to use the vbCFDIB format, you must specify both the vbCFBitmap and vbCFPalette formats so that the bitmap and the palette are correctly placed into the DataObject object.

For More Information See "Creating a Custom Data Format" for information on defining your own data format.


How OLE Drag and Drop Works

When an OLE drag-and-drop operation is performed, certain events are generated on the source and target sides. The events associated with the source object are always generated, whether the drag-and-drop operation is automatic or manual. The target-side events, however, are only generated in a manual drop operation. The following illustration shows which events occur and can be responded to on the drag source, and which occur and can be responded to on the drop target.

Figure 11.6 Source-side and target-side events

Which events you’ll need to respond to depends upon how you’ve chosen to implement the drag-and-drop functionality. For example, you may have created an application with a text box that you want to allow to automatically accept dragged data from another application. In this case, you simply set the control’s OLEDropMode property to Automatic. If you want to allow data to be automatically dragged from the text box control as well, you set its OLEDragMode property to Automatic.

If, however, you want to change the default mouse cursors or enhance the functionality for button states and shift keys, you need to manually respond to the source- and target-side events. Likewise, if you want to analyze the data before it is dropped into a control (to verify that the data is compatible, for instance), or delay when the data is loaded into the DataObject object (so that multiple formats don't need to be loaded at the beginning), you'll need to use manual OLE drag-and-drop operations.

Because you can drag and drop data into numerous Visual Basic controls and Windows applications — with varying limitations and requirements — implementing OLE drag and drop can range from straightforward to fairly complex. The simplest implementation, of course, would be dragging and dropping between two automatic objects, whether the object is a Word document, an Excel spreadsheet, or a control in your application that has been set to Automatic. Specifying multiple data formats that would be acceptable to your drop target would be more complicated.

Starting the Drag

What happens in a basic manual OLE drag-and-drop operation within your Visual Basic application? When the user drags data from an OLE drag source (a text box control, for example) by selecting and then holding down the left mouse button, the OLEStartDrag event is triggered and you can then either store the data or simply specify the formats that the source supports. You also need to specify whether copying or moving the data, or both, is allowed by the source.

For More Information See "Starting the OLE Drag Operation" for more information on the OLEDrag method, the OLEstartDrag event, using the SetData method to specify the supported data formats, and placing data into the DataObject.

Dragging Over the Target

As the user drags over the target, the target’s OLEDragOver event is triggered, indicating that the source is within its boundaries. You then specify what the target would do if the data were dropped there — either copy, move, or refuse the data. By convention, the default is usually move, but it may be copy.

When the target specifies which drop effect will be performed if the source is dropped there, the OLEGiveFeedback event is triggered. The OLEGiveFeedback event is used to provide visual feedback to the user on what action will be taken when the selection is dropped — i.e., the mouse pointer will be changed to indicate a copy, move, or "no drop" action.

As the source is moved around within the boundaries of the target — or if the user presses the SHIFT, CTRL, or ALT keys while holding down the mouse button — the drop effect may be changed. For example, instead of allowing a copy or a move, the data may be refused.

If the user passes beyond the target or presses the ESC key, for example, then the drag operation may be canceled or modified (the mouse pointer may be changed to indicate that the object it is currently passing over will not accept the data).

For More Information See "Dragging the OLE Drag Source over the OLE Drop Target" for more information on the OLEDragOver and OLEGiveFeedback events.

Completing the Drag

When the user drops the source onto the target, the target’s OLEDragDrop event is triggered. The target queries the source for the format of the data it contains (or supports, if the data wasn’t placed into the source when the drag was started) and then either retrieves or rejects the data.

If the data was stored when the drag started, the target retrieves the data by using the GetData method. If the data wasn’t stored when the drag started, the data is retrieved by triggering the source’s OLESetData event and then using the SetData method.

When the data is accepted or rejected, the OLECompleteDrag event is triggered and the source can then take the appropriate action: if the data is accepted and a move is specified, the source deletes the data, for example.

For More Information See “Dropping the OLE Drag Source onto the OLE Drop Target” for more information on the OLEDragDrop event, the OLECompleteDrag event, and using the GetFormat and GetData methods to retrieve data from the DataObject object.


Starting the OLE Drag Operation

If you want to be able to specify which data formats or drop effects (copy, move, or no drop) are supported, or if the control you want to drag from doesn't support automatic dragging, you need to make your OLE drag operation manual.

The first phase of a manual drag-and-drop operation is calling the OLEDrag method, setting the allowed drop effects, specifying the supported data formats, and, optionally, placing data into the DataObject object.

You use the OLEDrag method to manually start the drag operation and the OLEStartDrag event to specify the allowed drop-action effects and the supported data formats.

The OLEDrag Method

Generally, the OLEDrag method is called from an object’s MouseMove event when data has been selected, the left mouse button is pressed and held, and the mouse is moved.

The OLEDrag method does not provide any arguments. Its primary purpose is to initiate a manual drag and then allow the OLEStartDrag event to set the conditions of the drag operation (for example, specifying what will happen when the data is dragged into another control).

If the source control supports the OLEDragMode property, to have manual control over the drag operation you must set the property to Manual and then use the OLEDrag method on the control. If the control supports manual but not automatic OLE drag, it will not have the OLEDragMode property, but it will support the OLEDrag method and the OLE drag-and-drop events.

Note The OLEDrag method will also work if the source control’s OLEDragMode property is set to Automatic.

Specifying Drop Effects and Data Formats

In a manual OLE drag operation, when the user begins dragging the source and the OLEDrag method is called, the control's OLEStartDrag event fires. Use this event to specify what drop effects and data formats the source supports.

The OLEStartDrag event uses two arguments to specify supported data formats and whether the data can be copied or moved when the data is dropped (drop effects).

Note If no drop effects or data formats are specified in the OLEStartDrag event, the manual drag will not be started.

The AllowedEffects Argument

The allowedeffects argument specifies which drop effects the drag source supports. For example:

Private Sub txtSource_OLEStartDrag(Data As _
      VB.DataObject, AllowedEffects As Long)
   AllowedEffects = vbDropEffectMove Or _
      vbDropEffectCopy
End Sub

The target can then query the drag source for this information and respond accordingly.

The allowedeffects argument uses the following values to specify drop effects:

Constant | Value | Description
vbDropEffectNone | 0 | Drop target cannot accept the data.
vbDropEffectCopy | 1 | Drop results in a copy. The original data is untouched by the drag source.
vbDropEffectMove | 2 | Drag source removes the data.

The Format Argument

You specify which data formats the object supports by setting the format argument of the OLEStartDrag event. To do this, you use the SetData method. For example, in a scenario using a rich text box control as a source and a text box control as a target, you might specify the following supported formats:

Private Sub rtbSource_OLEStartDrag(Data As _
      VB.DataObject, AllowedEffects As Long)
   AllowedEffects = vbDropEffectMove Or _
      vbDropEffectCopy

   Data.SetData , vbCFText
   Data.SetData , vbCFRTF
End Sub

The target can query the source to determine which data formats are supported and then respond accordingly — e.g., if the format of the dropped data is not supported by the target, reject the dropped data. In this case, the only data formats that are supported by the source are the text and rich-text formats.

For More Information See "The OLE Drag and Drop DataObject Object" for more information on format values for the SetData method.

Placing Data into the DataObject object

In many cases, especially if the source supports more than one format, or if it is time-consuming to create the data, you may want to place data into the DataObject object only when it is requested by the target. You can, however, place the data into the DataObject object when you begin a drag operation by using the SetData method in the OLEStartDrag event. For example:

Private Sub txtSource_OLEStartDrag(Data As _
      VB.DataObject, AllowedEffects As Long)
   Data.Clear
   Data.SetData txtSource.SelText, vbCFText
End Sub

This example clears the default data formats from the DataObject object using the Clear method, specifies the data format (text) of the selected data, and then places the data into the DataObject object with the SetData method.


Dragging the OLE Drag Source over the OLE Drop Target

With a manual target, you can determine and respond to the position of the source data within the target and respond to the state of the mouse buttons and the SHIFT, CTRL, and ALT keys. Where both the source and the target are manual, you can modify the default visual behavior of the mouse.

To . . . | Use the . . .
Determine and respond to the position of the source object | state argument of the OLEDragOver event
Respond to the state of the mouse buttons | button argument of the OLEDragDrop and OLEDragOver events
Respond to the state of the SHIFT, CTRL, and ALT keys | shift arguments of the OLEDragDrop and OLEDragOver events
Modify the default visual behavior of the mouse | effect argument of the OLEDragOver event and the effect argument of the OLEGiveFeedback

For More Information For more information about changing the mouse cursor, see "Dragging the OLE Drag Source over the OLE Drop Target." For more information about using the button and shift arguments, see "Using the Mouse and Keyboard to Modify Drop Effects and User Feedback."

The OLEDragOver Event State Argument

Depending upon its position, the effect argument may be changed to indicate the currently acceptable drop effect.

The state argument of the OLEDragOver event allows you to respond to the source data entering, passing over, and leaving the target control. For example, when the source data enters the target control, the state argument is set to vbEnter.

When the drag source is moved around within the boundaries of the drop target, the state argument is set to vbOver. Depending upon the position (the x and y arguments) of the mouse pointer, you may want to change the drag effect. Notice that the OLEDragOver event is generated several times a second, even when the mouse is stationary.

The state argument of the OLEDragOver event specifies when the data enters, passes over, and leaves the target control by using the following constants:

Constant | Value | Meaning
vbEnter | 0 | Data has been dragged within the range of a target.
vbLeave | 1 | Data has been dragged out of the range of a target.
vbOver | 2 | Data is still within the range of a target, and either the mouse has moved, a mouse or keyboard button has changed, or a certain system-determined amount of time has elapsed.

Providing the User with Customized Visual Feedback

If you want to modify the default visual behavior of the mouse in an OLE drag-and-drop operation, you can manipulate the OLEDragOver event on the target side and the OLEGiveFeedback event on the source side.

OLE drag and drop provides automatic visual feedback during a drag-and-drop operation. For example, when you start a drag, the mouse pointer is changed to indicate that a drag has been initiated. When you pass over objects that do not support OLE drop, the mouse pointer is changed to the "no drop" cursor.

Modifying the mouse pointer to indicate how a control will respond if the data is dropped onto it involves two steps: determining what type of data is in the DataObject object using the GetFormat method, and then setting the effect argument of the OLEDragOver event to inform the source what drop effects are allowed for this control.

The OLEDragOver Event

When a target control’s OLEDropMode property is set to Manual, the OLEDragOver event is triggered whenever dragged data passes over the control.

The effect argument of the OLEDragOver event is used to specify what action would be taken if the object were dropped. When this value is set, the source’s OLEGiveFeedback event is triggered. The OLEGiveFeedback event contains its own effect argument, which is used to provide visual feedback to the user on what action will be taken when the selection is dragged — i.e., the mouse pointer is changed to indicate a copy, move, or "no drop" action.

The effect argument of the OLEDragOver event uses the following constants to indicate the drop action:

Constant | Value | Description
vbDropEffectNone | 0 | Drop target cannot accept the data.
vbDropEffectCopy | 1 | Drop results in a copy. The original data is untouched by the drag source.
vbDropEffectMove | 2 | Drag source removes the data.

Note The effect argument of the OLEDragOver and OLEGiveFeedback events express the same drop effects (copy, move, no drop) as the allowedeffects argument of the OLEStartDrag event. They differ only in that the OLEStartDrag event specifies which effects are allowed, and the OLEDragOver and OLEGiveFeedback use the effect argument to indicate to the source which of these actions will be taken.

The following code example queries the DataObject object for a compatible data format for the target control. If the data is compatible, the effect argument informs the source that a move will be performed if the data is dropped. If the data is not compatible, the source will be informed and a "no drop" mouse pointer will be displayed.

Private Sub txtTarget_OLEDragOver(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single, State As Integer)
   If Data.GetFormat(vbCFText) Then
      Effect = vbDropEffectMove And Effect
   Else
      Effect = vbDropEffectNone
   End If
End Sub

When the source data is dragged over the target, and the OLEDragOver event is triggered, the source tells the target which effects it allows (move, copy, no drop). You must then chose which single effect will occur if the data is dropped. The effect argument of the OLEDragOver event informs the source which drop action it supports, and the source then informs the user by using the OLEGiveFeedback event to modify the mouse pointer.

The OLEGiveFeedback Event

To change the default behavior of the mouse pointer based on the effect argument of the OLEDragOver event, you need to manually specify new mouse pointer values using the OLEGiveFeedback event. The source’s OLEGiveFeedback event is triggered automatically when the effect argument of the OLEDragOver event is set.

The OLEGiveFeedback event contains two arguments (effect and defaultcursors) that allow you to modify the default mouse pointers in an OLE drag-and-drop operation.

The effect argument, like the other OLE drag-and-drop events, specifies whether data is to be copied, moved, or rejected. The purpose of this argument in the OLEGiveFeedback event, however, is to allow you to provide customized visual feedback to the user by changing the mouse pointer to indicate these actions.

Constant | Value | Description
vbDropEffectNone | 0 | Drop target cannot accept the data.
vbDropEffectCopy | 1 | Drop results in a copy. The original data is untouched by the drag source.
vbDropEffectMove | 2 | Drag source removes the data.
vbDropEffectScroll | &H80000000& | Scrolling is about to start or is currently occurring in the target. The value is used in addition to the other values.

Note The vbDropEffectScroll value can be used by some applications or controls to indicate that the user is causing scrolling by moving the mouse pointer near the edge of an application’s window. Scrolling is automatically supported by some but not all of the Visual Basic standard controls. You may need to program for the scroll effect if you drag data into a program that contains scroll bars — Word for Windows, for example.

The defaultcursors argument specifies whether the default OLE cursor set is used. Setting this argument to False allows you to specify your own cursors using the Screen.MousePointer property of the Screen object.

In most cases, specifying custom mouse pointers is unnecessary because the default behavior of the mouse is handled by OLE. If you decide to specify custom mouse pointers using the OLEGiveFeedback event, you need to account for every possible effect, including scrolling. It is also a good idea to program for effects that may be added later by creating an option that gives the control of the mouse pointer back to OLE if an unknown effect is encountered.

The following code example sets the effect and defaultcursors arguments and specifies custom cursors (.ico or .cur files) for the copy, move, and scroll effects by setting the MousePointer and MouseIcon properties of the Screen object. It also returns control of the mouse pointer back to OLE if an unknown effect is encountered.

Private Sub TxtSource_OLEGiveFeedback(Effect As Long, _
      DefaultCursors As Boolean)
   DefaultCursors = False
   If Effect = vbDropEffectNone Then
      Screen.MousePointer = vbNoDrop
   ElseIf Effect = vbDropEffectCopy Then
      Screen.MousePointer = vbCustom
      Screen.MouseIcon = LoadPicture("c:\copy.ico")
   ElseIf Effect = (vbDropEffectCopy Or _
         vbDropEffectScroll) Then
      Screen.MousePointer = vbCustom
      Screen.MouseIcon = _
         LoadPicture("c:\copyscrl.ico")
   ElseIf Effect = vbDropEffectMove Then
      Screen.MousePointer = vbCustom
      Screen.MouseIcon = LoadPicture("c:\move.ico")
   ElseIf Effect = (vbDropEffectMove Or _
         vbDropEffectScroll) Then
      Screen.MousePointer = vbCustom
      Screen.MouseIcon = _
         LoadPicture("c:\movescrl.ico")
   Else
      ' If some new format is added that we do not
      '   understand, allow OLE to handle it with
      '   correct defaults.
      DefaultCursors = True
   End If
End Sub

Note You should always reset the mouse pointer in the OLECompleteDrag event if you specify a custom mouse pointer in the OLEGiveFeedback event. For more information about informing the source when data is dropped, see "Dropping the OLE Drag Source onto the OLE Drop Target."

For More Information See "Customizing the Mouse Pointer" for information on setting the MousePointer and MouseIcon properties.


Dropping the OLE Drag Source onto the OLE Drop Target

If your target supports manual OLE drag-and-drop operations, you can control what happens when the cursor is moved within the target and can specify what kind of data the target will accept. When the user drops the source object onto the target control, the OLEDragDrop event is used to query the DataObject object for a compatible data format, and then retrieve the data.

The OLEDragDrop event also informs the source of the drop action, allowing it to delete the original data if a move has been specified, for example.

Retrieving the Data

The OLEDragDrop event is triggered when the user drops the source onto the target. If data was placed into the DataObject object when the drag operation was initiated, it can be retrieved when the OLEDragDrop event is triggered, by using the GetData method. If, however, only the supported source formats were declared when the drag operation was initiated, then the GetData method will automatically trigger the OLESetData event on the source to place the data into, and then retrieve the data from, the DataObject object.

The following example retrieves data that was placed into the DataObject object when the drag operation was initiated. The drag operation may have been initiated manually (using the OLEDrag method on the source) or automatically (by setting the OLEDragMode property of the source to Automatic). The dragged data is retrieved using the DataObject object’s GetData method. The GetData method provides you with constants that represent the data types that the DataObject object supports. In this case, we are retrieving the data as text.

Private Sub txtTarget_OLEDragDrop(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single)
   txtTarget.Text = Data.GetData(vbCFText)
End Sub

For More Information For a complete list of GetData format constants, see "The OLE Drag and Drop DataObject Object" earlier in this chapter.

Querying the DataObject Object

You may need to query the DataObject object for the types of data that are being dropped onto the target. You use the GetFormat method in an If…Then statement to specify which types of data the target control can accept. If the data within the DataObject object is compatible, the drop action will be completed.

Private Sub txtTarget_OLEDragDrop(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single)
   If Data.GetFormat(vbCFText) Then
      txtTarget.Text = Data.GetData(vbCFText)
   End If
End Sub

Placing Data into the DataObject Object

When the target uses the GetData method to retrieve data from the source, the OLESetData event is only triggered if the data was not placed into the source when the drag operation was initiated.

In many cases, especially if the source supports more than one format, or if it is time-consuming to create the data, you may want to place data into the DataObject object only when it is requested by the target. The OLESetData event allows the source to respond to only one request for a given format of data.

For example, if the supported data formats were specified using the OLEStartDrag event when the drag operation was initiated, but data was not placed into the DataObject object, the OLESetData event is used to place a specific format of data into the DataObject object.

Private Sub txtSource_OLESetData(Data As _
      VB.DataObject, DataFormat As Integer)
   If DataFormat = vbCFText Then
      Data.SetData txtSource.SelText, vbCfText
   End If
End Sub

Informing the Source When Data is Dropped

The effect argument of the OLEDragDrop event specifies how the data was incorporated into the target when the data was dropped. When this argument is set, the OLECompleteDrag event is triggered on the source with its effect argument set to this value. The source can then take the appropriate action: If a move is specified, the source deletes the data, for example.

The effect argument of the OLEDragDrop event uses the same constants as the effect argument of the OLEDragOver event to indicate the drop action. The following table lists these constants:

Constant | Value | Description
vbDropEffectNone | 0 | Drop target cannot accept the data.
vbDropEffectCopy | 1 | Drop results in a copy. The original data is untouched by the drag source.
vbDropEffectMove | 2 | Drag source removes the data.

The following example sets the effect argument to indicate the drop action.

Private Sub txtTarget_OLEDragDrop(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single)
   If Data.GetFormat(vbCFText) Then
      txtTarget.Text = Data.GetData(vbCFText)
   End If
   Effect = vbDropEffectMove
End Sub

On the source side, the OLECompleteDrag event is triggered when the source is dropped onto the target, or when the OLE drag-and-drop operation is canceled. OLECompleteDrag is the last event in the drag-and-drop operation.

The OLECompleteDrag event contains only one argument (effect), which is used to inform the source of the action that was taken when the data is dropped onto the target.

The effect argument returns the same values that are used by the effect argument of the other OLE drag-and-drop events: vbDropEffectNone, vbDropEffectCopy, and vbDropEffectMove.

By setting this argument after a move has been specified by the target and the source has been dropped into the target, for example, the source will delete the original data in the control. You should also use the OLECompleteDrag event to reset the mouse pointer if you specified a custom mouse pointer in the OLEGiveFeedback event. For example:

Private Sub txtSource_OLECompleteDrag(Effect As Long)
   If Effect = vbDropEffectMove Then
      txtSource.SelText = ""
   End If
   Screen.MousePointer = vbDefault
End Sub


Using the Mouse and Keyboard to Modify Drop Effects and User Feedback

You can enhance the OLEDragDrop and OLEDragOver events by using the button and shift arguments to respond to the state of the mouse buttons and the SHIFT, CTRL, and ALT keys. For instance, when dragging data into a control, you can allow the user to perform a copy operation by pressing the CTRL key, or a move operation by pressing the SHIFT key.

In the following example, the shift argument of the OLEDragDrop event is used to determine if the SHIFT key is pressed when the data is dropped. If it is, a move is performed. If it is not, a copy is performed.

Private Sub txtTarget_OLEDragDrop(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single)
   If Shift And vbCtrlMask Then
      txtTarget.Text = Data.GetData(vbCFText)
      Effect = vbDropEffectCopy
   Else
      txtTarget.Text = Data.GetData(vbCFText)
      Effect = vbDropEffectMove
   End If
End Sub

The button argument can be used to isolate and respond to the various mouse button states. For instance, you may want to allow the user to move the data by pressing both the right and left mouse buttons simultaneously.

To indicate to the user what action will be taken when the source object is dragged over the target when a mouse button or the SHIFT, CTRL, and ALT keys are pressed, you can set the shift and button arguments of the OLEDragOver event. For example, to inform the user what action will be taken when the SHIFT button is pressed during a drag operation, you can add the following code to the OLEDragOver event:

Private Sub txtTarget_OLEDragOver(Data As _
      VB.DataObject, Effect As Long, Button As _
      Integer, Shift As Integer, X As Single, _
      Y As Single, State As Integer)
   If Shift And vbCtrlMask Then
      Effect = vbDropEffectCopy
   Else
      Effect = vbDropEffectMove
   End If
End Sub

For More Information See "Detecting Mouse Buttons" and "Detecting SHIFT, CTRL, and ALT States" for more information on responding to mouse and keyboard states.


Creating a Custom Data Format

If the formats supplied in Visual Basic are insufficient for some specific purpose, you can create a custom data format for use in an OLE drag-and-drop operation. For example, a custom data format is useful if your application defines a unique data format that you need to drag between two instances of your application, or just within the application itself.

To create a custom data format, you have to call the Windows API RegisterClipboardFormat function. For example:

Private Declare Function RegisterClipboardFormat Lib _
      "user32.dll" Alias "RegisterClipboardFormatA" _
      (ByVal lpszFormat$) As Integer
Dim MyFormat As Integer

Once defined, you can use your custom format as you would any other DataObject object data format. For example:

Dim a() As Byte
a = Data.GetData(MyFormat)

To use this functionality, you have to place data into and retrieve data from the DataObject object as a Byte array. You can then assign your custom data format to a string variable because it is automatically converted.

Caution Retrieving your custom data format with the GetData method may yield unpredictable results.

Because Visual Basic doesn’t understand your custom data format (because you defined it), it doesn’t have a way to determine the size of the data. Visual Basic can determine the memory size of the Byte array because it has been allocated by Windows, but the operating system usually assigns more memory than is needed.

Therefore, when you retrieve a custom data format, you get back a Byte array containing at least, and possibly more than, the number of bytes that the source actually placed into the DataObject object. You must then correctly interpret your custom data format when it is retrieved from the DataObject object. For example, in a simple string, you have to search for the NULL character and then truncate the string to that length.


Dragging Files From the Windows Explorer

You can use OLE drag-and-drop to drag files from the Windows Explorer into an appropriate Visual Basic control, or vice versa. For example, you can select a range of text files in the Windows Explorer and then open them all in a single text box control by dragging and dropping them onto the control.

To illustrate this, the following procedure uses a text box control and the OLEDragOver and OLEDragDrop events to open a range of text files using the Files property and the vbCFFiles data format of the DataObject object.

To drag text files into a text box control from the Windows Explorer

  1. Start a new project in Visual Basic.

  2. Add a text box control to the form. Set its OLEDropMode property to Manual. Set its MultiLine property to True and clear the Text property.

  3. Add a function to select and index a range of files. For example:
    Sub DropFile(ByVal txt As TextBox, ByVal strFN$)
Dim iFile As Integer
iFile = FreeFile

Open strFN For Input Access Read Lock Read _
Write As #iFile
Dim Str$, strLine$
While Not EOF(iFile) And Len(Str) <= 32000
Line Input #iFile, strLine$
If Str <> "" Then Str = Str & vbCrLf
Str = Str & strLine
Wend
Close #iFile

txt.SelStart = Len(txt)
txt.SelLength = 0
txt.SelText = Str

End Sub
  4. Add the following procedure to the OLEDragOver event. The GetFormat method is used to test for a compatible data format (vbCFFiles).
    Private Sub Text1_OLEDragOver(Data As _
VB.DataObject, Effect As Long, Button As Integer, _
Shift As Integer, X As Single, Y As Single, State _
As Integer)
If Data.GetFormat(vbCFFiles) Then
'If the data is in the proper format, _
inform the source of the action to be taken
Effect = vbDropEffectCopy And Effect
Exit Sub
End If
'If the data is not desired format, no drop
Effect = vbDropEffectNone

End Sub
  5. Finally, add the following procedure to the OLEDragDrop event.
    Private Sub Text1_OLEDragDrop(Data As _
VB.DataObject, Effect As Long, Button As Integer, _
Shift As Integer, X As Single, Y As Single)
If Data.GetFormat(vbCFFiles) Then
Dim vFN

For Each vFN In Data.Files
DropFile Text1, vFN
Next vFN
End If
End Sub
  6. Run the application, open the Windows Explorer, highlight several text files, and drag them into the text box control. Each of the text files will be opened in the text box.


Customizing the Mouse Pointer

You can use the MousePointer and MouseIcon properties to display a custom icon, cursor, or any one of a variety of predefined mouse pointers. Changing the mouse pointer gives you a way to inform the user that long background tasks are processing, that a control or window can be resized, or that a given control doesn't support drag-and-drop, for instance. Using custom icons or mouse pointers, you can express an endless range of visual information about the state and functionality of your application.

With the MousePointer property you can select any one of sixteen predefined pointers. These pointers represent various system events and procedures. The following table describes several of these pointers and their possible uses in your application.

Mouse pointer | Constant | Description
 | vbHourglass | Alerts the user to changes in the state of the program. For example, displaying an hourglass tells the user to wait.
 | vbSizePointer | Notifies the user of changes in function. For example, the double arrow sizing pointers tell users they can resize a window.
 | vbNoDrop | Warns the user an action can't be performed. For example, the no drop pointer tells users they can't drop a file at this location.

Each pointer option is represented by an integer value setting. The default setting is 0-Default and is usually displayed as the standard Windows arrow pointer. However, this setting is controlled by the operating system and can change if the system mouse settings have been changed by the user. To control the mouse pointer in your application, you set the MousePointer property to an appropriate value.

A complete list of mouse pointers is available by selecting the MousePointer property of a control or form and scanning the pull-down settings list or by using the Object Browser and searching for MousePointerConstants.

When you set the MousePointer property for a control, the pointer appears when the mouse is over the corresponding control. When you set the MousePointer property for a form, the selected pointer appears both when the mouse is over blank areas of the form and when the mouse is over controls with the MousePointer property set to 0-Default.

At run time you can set the value of the mouse pointer either by using the integer values or the Visual Basic mouse pointer constants. For example:

Form1.MousePointer = 11 'or vbHourglass

For More Information For a complete list of mouse pointer constants, see "MousePointer Constants" in the Language Reference.

Icons and Cursors

You can set the mouse pointer to display a custom icon or cursor. Using custom icons or cursors allows you to further modify the look or functionality of your application. Icons are simply .ico files, like those shipped with Visual Basic. Cursors are .cur files and, like icons, are essentially bitmaps. Cursors, however, are created specifically to show the user where actions initiated by the mouse will take place — they can represent the state of the mouse and the current input location.

Cursors also contain hot spot information. The hot spot is a pixel which tracks the location of the cursor — the x and y coordinates. Typically, the hot spot is located at the center of the cursor. Icons, when loaded into Visual Basic through the MouseIcon property, are converted to the cursor format and the hot spot is set to the center pixel. The two differ in that the hot spot location of a .cur file can be changed, whereas that of an .ico file cannot. Cursor files can be edited in Image Editor, which is available in the Windows SDK.

To use a custom icon or cursor, you set both the MousePointer and MouseIcon properties.

To use an .ico file as a mouse pointer

  1. Select a form or control and set the MousePointer property to 99-Custom.

  2. Load an .ico file into the MouseIcon property. For example, for a form:
    Form1.MouseIcon = LoadPicture("c:\Program _
Files\Microsoft Visual _
Basic\Icons\Computer\Disk04.ico")

Both properties must be set appropriately for an icon to appear as a mouse pointer. If no icon is loaded into MouseIcon when the MousePointer property is set to 99-Custom, the default mouse pointer is used. Likewise, if the MousePointer property is not set to 99-Custom, the setting of MouseIcon is ignored.

Note Visual Basic does not support animated cursor (.ani) files.

For More Information See "MouseIcon Property" and "MousePointer Property" in the Language Reference.


Responding to Keyboard Events

Keyboard events, along with mouse events, are the primary elements of a user's interaction with your program. Clicks and key presses trigger events and provide the means of data input and the basic forms of window and menu navigation.

Although the operating system provides the seamless back-end for all these actions, it's sometimes useful or necessary to modify or enhance them. The KeyPress, KeyUp, and KeyDown events allow you to make these modifications and enhancements.

Programming your application to respond to key events is referred to as writing a keyboard handler. A keyboard handler can work on two levels: at the control level and at the form level. The control level (low-level ) handler allows you to program a specific control. For instance, you might want to convert all the typed text in a Textbox control to uppercase. A form-level handler allows the form to react to the key events first. The focus can then be shifted to a control or controls on the form, and the events can either be repeated or initiated.

With these key events you can write code to handle most of the keys on a standard keyboard. For information on dealing with international character sets and keyboards, see "International Issues."


Writing Low-Level Keyboard Handlers

Visual Basic provides three events that are recognized by forms and by any control that accepts keyboard input. They are described in the following table.

Keyboard event | Occurs
KeyPress | When a key corresponding to an ASCII character is pressed
KeyDown | As any key on the keyboard is pressed
KeyUp | As any key on the keyboard is released

Only the object that has the focus can receive a keyboard event. For keyboard events, a form has the focus only if it is active and no control on that form has the focus. This happens only on blank forms and forms on which all controls have been disabled. However, if you set the KeyPreview property on a form to True, the form receives all keyboard events for every control on the form before the control recognizes them. This is extremely useful when you want to perform the same action whenever a certain key is pressed, regardless of which control has the focus at the time.

The KeyDown and KeyUp events provide the lowest level of keyboard response. Use these events to detect a condition that the KeyPress event is unable to detect, for instance:

  • Special combinations of SHIFT, CTRL, and ALT keys.

  • Arrow keys. Note that some controls (command buttons, option buttons, and check boxes) do not receive arrow-key events: Instead, arrow keys cause movement to another control.

  • PAGEUP and PAGEDOWN.

  • Distinguishing the numeric keypad from numbers on the typewriter keys.

  • Responding to a key being released as well as pressed (KeyPress responds only to a key being pressed).

  • Function keys not attached to menu commands.

The keyboard events are not mutually exclusive. When the user presses a key, both the KeyDown and KeyPress events are generated, followed by a KeyUp event when the user releases the key. When the user presses one of the keys that KeyPress does not detect, only a KeyDown event occurs, followed by a KeyUp event.

Before using the KeyUp and KeyDown events, make sure that the KeyPress event isn't sufficient. This event detects keys that correspond to all the standard ASCII characters: letters, digits, and punctuation on a standard keyboard, as well as the ENTER, TAB, and BACKSPACE keys. It's generally easier to write code for the KeyPress event.

You also should consider using shortcut and access keys, which are described in "Menu Basics" in "Forms, Controls, and Menus." Shortcut keys must be attached to menu commands, but they can include function keys (including some function-key – shift-key combinations). You can assign shortcut keys without writing additional code.

Note The Windows ANSI (American National Standards Institute) character set corresponds to the 256 characters that include the standard Latin alphabet, publishing marks (such as copyright symbol, em dash, ellipsis), as well as many alternate and accented letters. These characters are represented by a unique 1-byte numeric value (0-255). ASCII (American Standard Code for Information Interchange) is essentially a subset (0-127) of the ANSI character set and represents the standard letters, digits, and punctuation on a standard keyboard. The two character sets are often referred to interchangeably.


The KeyPress Event

The KeyPress event occurs when any key that corresponds to an ASCII character is pressed. The ASCII character set represents not only the letters, digits, and punctuation on a standard keyboard but also most of the control keys. The KeyPress event only recognizes the ENTER, TAB, and BACKSPACE keys, however. The other function, editing, and navigation keys can be detected by the KeyDown and KeyUp events.

Use the KeyPress event whenever you want to process the standard ASCII characters. For example, if you want to force all the characters in a text box to be uppercase, you can use this event to change the case of the keys as they are typed:

Private Sub Text1_KeyPress (KeyAscii As Integer)
   KeyAscii = Asc(UCase(Chr(KeyAscii)))
End Sub

The keyascii argument returns an integer value corresponding to an ASCII character code. The procedure above uses Chr to convert the ASCII character code into the corresponding character, UCase to make the character uppercase, and Asc to turn the result back into a character code.

Using the same ASCII character codes, you can test whether a key recognized by the KeyPress event is pressed. For instance, the following event procedure uses KeyPress to detect if the user is pressing the BACKSPACE key:

Private Sub Text1_KeyPress (KeyAscii As Integer)
   If KeyAscii = 8 Then MsgBox "You pressed the _
      BACKSPACE key."
End Sub

You can also use the Visual Basic key-code constants in place of the character codes. The BACKSPACE key in the example above has an ASCII value of 8. The constant value for the BACKSPACE key is vbKeyBack.

For More Information For a complete list of character codes, see "Character Set (0–127)" and "Character Set (128–255)" in the Language Reference. A complete list of key code constants with corresponding ASCII values is available in "Key Code Constants" or by using the Object Browser and searching for KeyCodeConstants.

You can also use the KeyPress event to alter the default behavior of certain keys. For example, pressing ENTER when there is no Default button on the form causes a beep. You can avoid this beep by intercepting the ENTER key (character code 13) in the KeyPress event.

Private Sub Text1_KeyPress (KeyAscii As Integer)
   If KeyAscii = 13 Then KeyAscii = 0
End Sub


The KeyDown and KeyUp Events

The KeyUp and KeyDown events report the exact physical state of the keyboard itself: A key is pressed down (KeyDown) and a key is released (KeyUp). In contrast, the KeyPress event does not report the state of the keyboard directly — it doesn't recognize the up or down state of the key, it simply supplies the character that the key represents.

A further example helps to illustrate the difference. When the user types uppercase "A," the KeyDown event gets the ASCII code for "A." The KeyDown event gets the same code when the user types lowercase "a." To determine whether the character pressed is uppercase or lowercase, these events use the shift argument. In contrast, the KeyPress event treats the uppercase and lowercase forms of a letter as two separate ASCII characters.

The KeyDown and KeyUp events return information on the character typed by providing the following two arguments.

Argument | Description
Keycode | Indicates the physical key pressed. In this case, "A" and "a" are returned as the same key. They have the identical keycode value. But note that "1" on the typewriter keys and "1" on the numeric keypad are returned as different keys, even though they generate the same character.
Shift | Indicates the state of the SHIFT, CTRL, and ALT keys. Only by examining this argument can you determine whether an uppercase or lowercase letter was typed.

The Keycode Argument

The keycode argument identifies a key by the ASCII value or by the key-code constant. Key codes for letter keys are the same as the ASCII codes of the uppercase character of the letter. So the keycode for both "A" and "a" is the value returned by Asc("A"). The following example uses the KeyDown event to determine if the "A" key has been pressed:

Private Sub Text1_KeyDown(KeyCode As Integer, _
      Shift As Integer)
   If KeyCode = vbKeyA Then MsgBox "You pressed _
      the A key."
End Sub

Pressing SHIFT + "A" or "A" without the SHIFT key displays the message box — that is, the argument is true in each case. To determine if the uppercase or lowercase form of the letter has been pressed you need to use the shift argument. See the topic, "The Shift Argument" later in this chapter.

Key codes for the number and punctuation keys are the same as the ASCII code of the number on the key. So the keycode for both "1" and "!" is the value returned by Asc("1"). Again, to test for the "!" character you need to use the shift argument.

The KeyDown and KeyUp events can recognize most of the control keys on a standard keyboard. This includes the function keys (F1-F16), the editing keys (HOME, PAGE UP, DELETE, etc.), the navigation keys (RIGHT, LEFT, UP, and DOWN ARROW), and the keypad. These keys can be tested for by using either the key-code constant or the equivalent ASCII value. For example:

Private Sub Text1_KeyDown(KeyCode As Integer, _
      Shift As Integer)
   If KeyCode = vbKeyHome Then MsgBox "You _
      pressed the HOME key."
End Sub

For More Information For a complete list of character codes, see "Character Set (0–127)" and "Character Set (128–255)" in the Language Reference. A complete list of key code constants with corresponding ASCII values is available in "Key Code Constants" or by using the Object Browser and searching for KeyCodeConstants.

The Shift Argument

The key events use the shift argument in the same way that the mouse events do — as integer and constant values that represent the SHIFT, CTRL, and ALT keys. You can use the shift argument with KeyDown and KeyUp events to distinguish between uppercase and lowercase characters, or to test for the various mouse states.

Building on the previous example, you can use the shift argument to determine whether the uppercase form of a letter is pressed.

Private Sub Text1_KeyDown(KeyCode As Integer, _
      Shift As Integer)
   If KeyCode = vbKeyA And Shift = 1 _
   Then MsgBox "You pressed the uppercase A key."
End Sub

Like the mouse events, the KeyUp and KeyDown events can detect the SHIFT, CTRL, and ALT individually or as combinations. The following example tests for specific shift-key states.

Open a new project and add the variable ShiftKey to the Declarations section of the form:

Dim ShiftKey as Integer

Add a Textbox control to the form and this procedure in the KeyDown event:

Private Sub Text1_KeyDown(KeyCode As Integer, _
      Shift As Integer)
   ShiftKey = Shift And 7
   Select Case ShiftKey
      Case 1 ' or vbShiftMask
         Print "You pressed the SHIFT key."
      Case 2 ' or vbCtrlMask
         Print "You pressed the CTRL key."
      Case 4 ' or vbAltMask
         Print "You pressed the ALT key."
      Case 3
         Print "You pressed both SHIFT and CTRL."
      Case 5
         Print "You pressed both SHIFT and ALT."
      Case 6
         Print "You pressed both CTRL and ALT."
      Case 7
         Print "You pressed SHIFT, CTRL, and ALT."
      End Select
End Sub

As long as the Textbox control has the focus, each key or combination of keys prints a corresponding message to the form when pressed.

For More Information See "Detecting SHIFT, CTRL, and ALT States" earlier in this chapter.


Writing Form-Level Keyboard Handlers

Each KeyDown and KeyUp event is attached to a specific object. To write a keyboard handler that applies to all objects on the form, set the KeyPreview property of the form to True. When the KeyPreview property is set to True, the form recognizes the KeyPress, KeyUp, and KeyDown events for all controls on the form before the controls themselves recognize the events. This makes it very easy to provide a common response to a particular keystroke.

You can set the KeyPreview property of the form to True in the Properties window or through code in the Form_Load procedure:

Private Sub Form_Load
   Form1.KeyPreview = True
End Sub

You can test for the various key states on a form by declaring a ShiftKey variable and using the Select Case statement. The following procedure will print the message to the form regardless of which control has the focus.

Open a new project and add the variable ShiftKey to the Declarations section of the form:

Dim ShiftKey as Integer

Add a Textbox and a CommandButton control to the form. Add the following procedure to the form's KeyDown event:

Private Sub Form_KeyDown(KeyCode As Integer, _
      Shift As Integer)
   ShiftKey = Shift And 7
   Select Case ShiftKey
      Case 1 ' or vbShiftMask
         Print "You pressed the SHIFT key."
      Case 2 ' or vbCtrlMask
         Print "You pressed the CTRL key."
      Case 4 ' or vbAltMask
         Print "You pressed the ALT key."
   End Select
End Sub

If you have defined a shortcut key for a menu control, the Click event for that menu control occurs automatically when the user types that key, and no key event occurs.

Similarly, if there is a command button on the form with the Default property set to True, the ENTER key causes the Click event for that command button to occur instead of a key event. If there is a command button with the Cancel property set to True, the ESC key causes the Click event for that command button to occur instead of a key event.

For example, if you add a Click event procedure to the CommandButton and then set either the Default or Cancel properties to True, pressing the RETURN or ESC keys will override the KeyDown event. This procedure closes the application:

Private Sub Command1_Click()
   End
End Sub

Notice that the TAB key moves the focus from control to control and does not cause a key event unless every control on the form is disabled or has TabStop set to False.

When the KeyPreview property of the form is set to True, the form recognizes the keyboard events before the controls, but the events still occur for the controls. To prevent this, you can set the keyascii or keycode arguments in the form key-event procedures to 0. For example, if there is no default button on the form, you can use the ENTER key to move the focus from control to control:

Private Sub Form_KeyPress (KeyAscii As Integer)
Dim NextTabIndex As Integer, i As Integer
   If KeyAscii = 13 Then
      If Screen.ActiveControl.TabIndex = _
      Count - 1 Then
         NextTabIndex = 0
      Else
         NextTabIndex = Screen.ActiveControl._
         TabIndex + 1
      End If
      For i = 0 To Count - 1
         If Me.Controls(i).TabIndex = _
         NextTabIndex Then
            Me.Controls(i).SetFocus
            Exit For
         End If
      Next i
      KeyAscii = 0
   End If
End Sub

Because this code sets keyascii to 0 when it is 13, the controls never recognize the ENTER key being pressed, and their key-event procedures are never called.


Interrupting Background Processing

Your application may utilize long background processing to accomplish certain tasks. If this is the case, it is helpful to provide the user with a way to either switch to another application or interrupt or cancel the background task. The Windows operating environment gives users the first option: switching to another application by using the ALT+TAB key combination, for instance. You can provide the other options by writing code that responds when a user either clicks a cancel button or presses the ESC key.

In considering how to implement this in your application, it's important to understand how tasks from various applications are handled by the operating system. Windows is a preemptively multitasking operating system, which means that idle processor time is efficiently shared among background tasks. These background tasks can originate from the application the user is working with, from another application, or perhaps from some system-controlled events. Priority is always given to the application that the user is working with, however. This ensures that the mouse and keyboard always respond immediately.

Background processing can be placed into two categories: constant and intermittent. An example of a constant task would be copying a file from a server. Periodically updating a value would be an example of an intermittent task. Both types of tasks can be interrupted or canceled by the user. However, because background processing is usually a complex matter, it is important to consider how these tasks are initiated in the first place. The topic "Allowing Users to Interrupt Tasks" later in this chapter describes these considerations and techniques.


Allowing Users to Interrupt Tasks

During long background tasks, your application cannot respond to user input. Therefore, you should provide the user with a way to interrupt or cancel the background processing by writing code for either the mouse or keyboard events. For example, when a long background task is running, you can display a dialog box that contains a Cancel button that the user can initiate by clicking the ENTER key (if the focus is on the Cancel button) or by clicking on it with the mouse.

Note You may also want to give the user a visual cue when a long task is processing. For example, you might show the user how the task is progressing (using a Label or Gauge control, for instance), or by changing the mouse pointer to an hourglass.

There are several techniques, but no one way, to write code to handle background processing. One way to allow users to interrupt a task is to display a Cancel button and allow its Click event to be processed. You can do this by placing the code for your background task in a timer event, using the following guidelines.

  • Use static variables for information that must persist between occurrences of the Timer event procedure.

  • When the Timer event gets control, allow it to run slightly longer than the time you specified for the Interval property. This ensures that your background task will use every bit of processor time the system can give it. The next Timer event will simply wait in the message queue until the last one is done.

  • Use a fairly large value — five to ten seconds — for the timer's Interval property, as this makes for more efficient processing. Preemptive multitasking prevents other applications from being blocked, and users are generally tolerant of a slight delay in canceling a long task.

  • Use the Enabled property of the Timer as a flag to prevent the background task from being initiated when it is already running.

For More Information See "Using the Timer Control" in "Using Visual Basic's Standard Controls."


Using DoEvents

Although Timer events are the best tool for background processing, particularly for very long tasks, the DoEvents function provides a convenient way to allow a task to be canceled. For example, the following code shows a "Process" button that changes to a "Cancel" button when it is clicked. Clicking it again interrupts the task it is performing.

' The original caption for this button is "Process".
Private Sub Command1_Click()
   ' Static variables are shared by all instances
   ' of a procedure.
   Static blnProcessing As Boolean
   Dim lngCt As Long
   Dim intYieldCt As Integer
   Dim dblDummy As Double
   ' When the button is clicked, test whether it's
    'already processing.
   If blnProcessing Then
      ' If processing is in progress, cancel it.
      blnProcessing = False
   Else
      Command1.Caption = "Cancel"
      blnProcessing = True
      lngCt = 0
      ' Perform a million floating-point
      ' multiplications. After every
      ' thousand, check for cancellation.
      Do While blnProcessing And (lngCt < 1000000)
         For intYieldCt = 1 To 1000
            lngCt = lngCt + 1
            dblDummy = lngCt * 3.14159
         Next intYieldCt
         ' The DoEvents statement allows other
         ' events to occur, including pressing this
         ' button a second time.
         DoEvents
      Loop
      blnProcessing = False
      Command1.Caption = "Process"
      MsgBox lngCt & " multiplications were performed"
   End If
End Sub

DoEvents switches control to the operating-environment kernel. Control returns to your application as soon as all other applications in the environment have had a chance to respond to pending events. This doesn't cause the current application to give up the focus, but it does enable background events to be processed.

The results of this yielding may not always be what you expect. For example, the following Click-event code waits until ten seconds after the button was clicked and then displays a message. If the button is clicked while it is already waiting, the clicks will be finished in reverse order.

Private Sub Command2_Click()
   Static intClick As Integer
   Dim intClickNumber As Integer
   Dim dblEndTime As Double
      ' Each time the button is clicked,
      ' give it a unique number.
   intClick = intClick + 1
   intClickNumber = intClick
      ' Wait for ten seconds.
   dblEndTime = Timer + 10#
   Do While dblEndTime > Timer
      ' Do nothing but allow other
      ' applications to process
      ' their events.
      DoEvents
   Loop
   MsgBox "Click " & intClickNumber & " is finished"
End Sub

You may want to prevent an event procedure that gives up control with DoEvents from being called again before DoEvents returns. Otherwise, the procedure might end up being called endlessly, until system resources are exhausted. You can prevent this from happening either by temporarily disabling the control or by setting a static "flag" variable, as in the earlier example.

Avoiding DoEvents When Using Global Data

It may be perfectly safe for a function to be called again while it has yielded control with DoEvents. For example, this procedure tests for prime numbers and uses DoEvents to periodically enable other applications to process events:

Function PrimeStatus (TestVal As Long) As Integer
   Dim Lim As Integer
   PrimeStatus = True
   Lim = Sqr(TestVal)
   For I = 2 To Lim
      If TestVal Mod I = 0 Then
         PrimeStatus = False
         Exit For
      End If
      If I Mod 200 = 0 Then DoEvents
   Next I
End Function

This code calls the DoEvents statement once every 200 iterations. This allows the PrimeStatus procedure to continue calculations as long as needed while the rest of the environment responds to events.

Consider what happens during a DoEvents call. Execution of application code is suspended while other forms and applications process events. One of these events might be a button click that launches the PrimeStatus procedure again.

This causes PrimeStatus to be re-entered, but since each occurrence of the function has space on the stack for its parameters and local variables, there is no conflict. Of course, if PrimeStatus gets called too many times, an Out of Stack Space error could occur.

The situation would be very different if PrimeStatus used or changed module-level variables or global data. In that case, executing another instance of PrimeStatus before DoEvents could return might result in the values of the module data or global data being different than they were before DoEvents was called. The results of PrimeStatus would then be unpredictable.

For More Information See "DoEvents Function" and "Refresh Method" in the Language Reference.


Working with Text and Graphics

Visual Basic includes sophisticated text and graphics capabilities for use in your applications. If you think of text as a visual element, you can see that size, shape and color can be used to enhance the information presented. Just as a newspaper uses headlines, columns and bullets to break the words into bite-sized chunks, text properties can help you emphasize important concepts and interesting details.

Visual Basic also provides graphics capabilities allowing you great flexibility in design, including the addition of animation by displaying a sequence of images.

This chapter describes ways of placing and manipulating text and graphics. Details on formatting, fonts, color palettes, and printing are included. By combining these capabilities with good design concepts, you can optimize the attractiveness and ease of use of your applications.

Topics

Working with Fonts

Explains how to choose fonts for your application and set font characteristics.
Displaying Text on Forms and Picture Boxes

Describes using the Print method to print messages to a form, picture box, or printer.
Formatting Numbers, Dates, and Times

How to display numbers, dates, and times in different formats.
Working with Selected Text

Selecting and manipulating text in a text box or combo box.
Transferring Text and Graphics with the Clipboard Object

How to cut, copy, and paste items to the Clipboard.
Understanding the Coordinate System

How to define the location of controls and forms.
Using Graphical Controls

Covers the properties that apply to all the graphical controls.
Using Graphics Methods

How to use the graphics methods to create lines and shapes.
Working with Color

Everything from the basics of color to managing multiple color palettes.
Using the Picture Object

How to manipulate graphics with the Picture object or an array of Picture objects.
Printing

Covers considerations for printing text and graphics from your application.

Sample applications

Blanker.vbp, Palettes.vbp

Some of the code examples in this chapter are taken from the Blanker (Blanker.vbp)and Palettes (Palettes.vbp) samples. You'll find these applications listed in the Samples directory.


Fonts

Working with Fonts

Text is displayed using a font — a set of characters of the same typeface, available in a particular size, style, and weight.

The Windows 95 and Windows NT operating systems provide you and your users with a complete set of standard fonts. TrueType® fonts are scaleable, which means they can reproduce a character at any size. When you select a TrueType font, it is rendered into the selected point size and displayed as a bitmap on the screen.

When printing, the selected TrueType font or fonts are rendered into the appropriate size and then sent to the printer. Therefore, there is no need for separate screen and printer fonts. Printer fonts will be substituted for TrueType fonts, however, if an equivalent font is available, which increases print speed.

Choosing Fonts for Your Application

Remember that a user of your application may not have the fonts you used to create the application. If you select a TrueType font that a user doesn’t have, Windows selects the closest matching font on the user’s system. Depending on the design of your application, this may cause problems for the user. For example, the font Windows selects may enlarge text so that labels overlap on the screen.

One way to avoid font problems is to distribute the necessary fonts with your application. (You will probably need to obtain permission from the copyright holder of the font to distribute it with your application.)

You can also program your application to check among the fonts available in the operating system for the fonts you use. If the font doesn’t reside in the operating system, you can program the application to choose a different font from the list.

Another way to avoid font problems is to use fonts users are most likely to have on their systems. If you use fonts from a specific version of Windows, you may have to specify that version as a system requirement of your application.

Checking Available Fonts

Your program can easily determine whether matching fonts are available on both the user’s system and printer. The Fonts property applies to the Printer and Screen objects. An array returned by the Fonts property is a list of all of the fonts available to a printer or screen. You can iterate through the property array, and then search for matching name strings. This code example determines whether the system has a printer font that matches the font of the selected form:

Private Sub Form_Click ()
Dim I As Integer, Flag As Boolean
   For I = 0 To Printer.FontCount - 1
      Flag = StrComp (Font.Name,Printer.Fonts(I), 1)
      If Flag = True Then
         Debug.Print "There is a matching font."
         Exit For
      End If
   Next I
End Sub

For More Information For information about setting font properties, see "Setting Font Characteristics" later in this chapter. For information about fonts in East Asian systems, see "Font, Display, and Print Considerations in a DBCS Environment" in "International Issues."

Creating Your Own Font Types

If you set a reference to Standard OLE Types using the References dialog box, you can use the StdFont class to create your own font types. If you view the Object Browser, you will notice that there are StdFont and Font classes. The Font class is derived from the StdFont base class and is supported by all controls.

You can use the following syntax:

Dim MyFont As Font

But, you cannot use:

Dim MyFont As New Font

Instead, to create your own font or picture types, use code like the following:

Dim MyFont As New StdFont
With MyFont
   .Bold = True
   .Name = "Arial"
End With
Set Text1.Font = MyFont


Setting Font Characteristics

Forms, controls that display text (as text or captions), and the Printer object support a Font property, which determines the visual characteristics of text, including:

  • Font name (typeface)

  • Font size (in points)

  • Special characteristics (bold, italic, underline, or strikethrough)

For details on the Printer object, see "Printing from an Application" later in this chapter.

Setting Font Properties

You can set any of the font properties at design time by double-clicking Font in the Properties window and setting the properties in the Font dialog box.

At run time, you set font characteristics by setting the Font object’s properties for each form and control. The following table describes the properties for the Font object.

Property | Type | Description
Name | String | Specifies name of font, such as Arial or Courier.
Size | Single | Specifies font size in points (72 points to an inch when printed).
Bold | Boolean | If True, the text is bold.
Italic | Boolean | If True, the text is italic.
StrikeThrough | Boolean | If True, Visual Basic strikes through the text.
Underline | Boolean | If True, the text is underlined.
Weight | Integer | Returns or sets the weight of the font. Above a certain weight, the Bold property is forced to True.

For example, the following statements set various font properties for a label named lblYearToDate:

With lblYearToDate.Font
   .Name = "Arial"         ' Change the font to Arial.
   .Bold = True            ' Make the font bold.
End With

The order in which you select font properties is important, because not all fonts support all font variations. Set the Name property first. Then you can set any of the Boolean properties, such as Bold and Italic, to True or False.

You can also store a set of font properties in a Font object. You can declare a Font object just as you would any other object, using the StdFont class:

Dim MyFont As New StdFont
With MyFont
   .Name = "Arial"
   .Size = 10
   .Bold = True
End With

Note Before you can create a new Font object, you must use the References dialog box (available from the Project menu) to create a reference to Standard OLE Types.

You can then easily switch from one set of font properties to another, by setting the form or control’s Font object to the new object:

Set lblYearToDate.Font = MyFont

For More Information See "Font Object" in the Language Reference.

Working with Small Fonts

Some fonts do not support the sizes smaller than 8 points. When you set the Size property for one of these fonts to a size smaller than 8 points, either the Name property or the Size property will automatically change to a different font or a different size. To avoid unpredictable results, each time you set the Size property to a font size smaller than 8 points, examine the values of the Name property and the Size property again after setting it.

Applying Font Properties to Specific Objects

The effect of setting font properties varies depending on the technique used to display text. If the text is specified by a property (such as Text or Caption), then changing a font property applies to all the text in that control. Labels, text boxes, frames, buttons, check boxes, and all the file-system controls use a property to specify text.

If the application shows text with the Print method, then changing a font property affects all uses of Print after the property change. Text printed before the property change is not affected. Only forms, picture boxes, and the Debug and Printer objects support the Print method.

Because changes in font properties apply to all the text in text boxes and labels, you cannot mix fonts in these controls. If you need to mix fonts (for example, making some words bold but leaving others in normal font), then create a picture box and use the Print method to display text. "Displaying Text on Forms and Picture Boxes" explains how to use the Print method.

The FontTransparent Property

Forms and picture boxes have an additional font property, FontTransparent. When FontTransparent is True, the background shows through any text displayed on the form or picture box. Figure 12.1 shows the effects of the FontTransparent property.

Figure 12.1 The effects of the FontTransparent property


Displaying Text on Forms and Picture Boxes

To display text on a form or picture box, use the Print method, preceded by the name of the form or picture box. To send output text to a printer, use the Print method on the Printer object.

Using the Print Method

The Print method syntax is:

[object.]Print [outputlist] [{ ; | , }]

The object argument is optional; if omitted, the Print method applies to the current form.

For example, the following statements print messages to:

  • A form named MyForm:
    MyForm.Print "This is a form."
  • A picture box named picMiniMsg:
    picMiniMsg.Print "This is a picture box."
  • The current form:
    Print "This is the current form."
  • The Printer object:
    Printer.Print "This text is going to the printer."

The outputlist argument is the text that appears on the form or picture box. Multiple items in the outputlist argument must be separated by commas or semicolons or both, as explained in "Displaying Different Items on a Single Line" later in this chapter.

Truncated Text

If the form or picture box is too small to display all the text, the text is cut off. Where the form or picture box cuts off the text depends on the coordinates of the location at which you began printing the text. You cannot scroll through a form or picture box.

Layering

When you print text to a form, the text appears in a layer behind any controls that have been placed on the form. So printing to a form usually works best on a form specifically created to hold the text. For more information about how text and graphics appear in layers on a form, see "Layering Graphics with AutoRedraw and ClipControls" later in this chapter.


Displaying Different Items on a Single Line

The items you display or print can include property values, constants, and variables (either string or numeric). The Print method, discussed in "Displaying Text on Forms and Picture Boxes," prints the value of numeric items. Positive number values have a leading and a trailing space. Negative numeric values display their sign instead of a leading space.

Use a semicolon (;) or a comma (,) to separate one item from the next. If you use a semicolon, Visual Basic prints one item after another, without intervening spaces. If you use a comma, Visual Basic skips to the next tab column.

For example, the following statement prints to the current form:

Print "The value of X is "; X; "and the value of Y _
   is "; Y

If X contains the value 2 and Y contains the value 7, the statement produces this output:

The value of X is 2 and the value of Y is 7

By default, each Print method prints the text and moves to the next line. If there are no items, Print simply skips a line. A series of Print statements (in the following example, for a picture box named picLineCount) automatically uses separate lines:

picLineCount.Print "This is line 1."
picLineCount.Print "This is line 2."

By placing a semicolon (or comma) at the end of the first statement, however, you cause the output of the next Print statement to appear on the same line:

picLineCount.Print "This all appears ";
picLineCount.Print "on the same line."


Displaying Print Output at a Specific Location

You can control placement of Print output by specifying the drawing coordinates, using either or both of these techniques:

  • Use the Cls (clear) method to erase a form or picture box and reset the drawing coordinates to the origin (0,0).

  • Set drawing coordinates with the CurrentX and CurrentY properties.

The Cls Method

All the text and graphics on the object that were created with Print and graphics methods can be deleted with the Cls method. The Cls method also resets the drawing coordinates to the origin (0,0), which is the upper-left corner by default. For example, these statements clear:

  • A picture box named Picture1:
    Picture1.Cls
  • The current form:
    Cls

Setting Drawing Coordinates

You can set the drawing coordinates of forms and picture boxes directly with the CurrentX and CurrentY properties. For example, these statements reset the drawing coordinates to the upper-left corner for Picture1 and for the current form:

  • A picture box named Picture1:
    Picture1.CurrentX = 0
Picture1.CurrentY = 0
  • The current form:
    CurrentX = 0
CurrentY = 0

Any new text you print appears on top of any text and graphics already at that location. To erase text selectively, draw a box with the Line method and fill it with the background color. Keep in mind that the drawing coordinates specified by CurrentX and CurrentY usually change location when you use a graphics method.

By default, forms and picture boxes use a coordinate system where each unit corresponds to a twip (1,440 twips equal an inch, and approximately 567 twips equal a centimeter). You may want to change the ScaleMode property of the form, picture box, or Printer object from twips to points, because text height is measured in points. Using the same unit of measure for the text and for the object where you will print the text makes it easier to calculate the position of the text.

For More Information For more information about twips and drawing coordinates, see "Understanding the Coordinate System" later in this chapter.

The TextHeight and TextWidth Methods

Before using the Print method, you can use the TextHeight and TextWidth methods to determine where to position the CurrentX and CurrentY properties. TextHeight returns the height of a line of text, taking into account the object’s font size and style. The syntax is:

[object.]TextHeight(string)

If the string argument contains embedded carriage-return characters (Chr(13)), then the text corresponds to multiple lines, and TextHeight returns the height of the number of lines of text contained in the string. If there are no embedded carriage returns, TextHeight always returns the height of one line of text.

One way to use the TextHeight method is to set the CurrentY property to a particular line. For example, the following statements set the drawing coordinates to the beginning of the fifth line:

CurrentY = TextHeight("sample") * 4
CurrentX = 0

Assuming there are no carriage returns in the sample text, you would use this syntax to set CurrentY to the nth line:

CurrentY = [object.]TextHeight(string) * (n – 1)

If object is omitted, the method applies to the current form. The object argument can be a form, a picture box, or the Printer object.

The TextWidth method returns the width of a string, taking into account the object’s font size and style. This method is useful because many fonts have proportional-width characters. The TextWidth method helps you determine whether the width of the string is larger than the width of the form, picture box, or Printer object.

For example, the following statements use TextWidth and TextHeight to center the text in a box by positioning CurrentX and CurrentY. The name of the box in this example is MealCard.

CurrentX = (BoxWidth - TextWidth("MealCard")) / 2
CurrentY = (Boxheight - TextHeight("MealCard")) / 2

For More Information See "TextHeight Method" and "TextWidth Method" in the Language Reference.


Formatting Numbers, Dates, and Times

Visual Basic provides great flexibility in displaying number formats, as well as date and time formats. You can easily display international formats for numbers, dates, and times.

The Format function converts the numeric value to a text string and gives you control over the string’s appearance. For example, you can specify the number of decimal places, leading or trailing zeros, and currency formats. The syntax is:

Format(expression[, format[, firstdayofweek[, firstweekofyear]]])

The expression argument specifies a number to convert, and the format argument is a string made up of symbols that shows how to format the number. The most commonly used symbols are listed in the table below.

Symbol | Description
0 | Digit placeholder; prints a trailing or a leading zero in this position, if appropriate.
# | Digit placeholder; never prints trailing or leading zeros.
. | Decimal placeholder.
, | Thousands separator.
– + $ ( ) space | Literal character; characters are displayed exactly as typed into the format string.

The firstdayofweek argument is a constant that specifies the first day of the week; the firstweekofyear argument is a constant that specifies the first week of the year. Both arguments are optional. For more information about these constants, see "Format Function" in the Language Reference.

Named Formats

Visual Basic provides several standard formats to use with the Format function. Instead of designating symbols in the format argument, you specify these formats by name in the format argument of the Format function. Always enclose the format name in double quotation marks ("").

The following table lists the format names you can use.

Named format | Description
General Number | Displays number with no thousand separator.
Currency | Displays number with thousand separator, if appropriate; display two digits to the right of the decimal separator. Output is based on user's system settings.
Fixed | Displays at least one digit to the left and two digits to the right of the decimal separator.
Standard | Displays number with thousand separator, at least one digit to the left and two digits to the righseparator.
Percent | Multiplies the value by 100 with a percent sign at the end.
Scientific | Uses standard scientific notation.
General Date | Shows date and time if expression contains both. If expression is only a date or a time, the missing information is not displayed. Date display is determined by user's system settings.
Long Date | Uses the Long Date format specified by user's system settings.
Medium Date | Uses the dd-mmm-yy format (for example, 03-Apr-93). Date display is determined by user's system settings.
Short Date | Uses the Short Date format specified by user's system settings.
Long Time | Displays a time using user's system's long-time format; includes hours, minutes, seconds.
Medium Time | Shows the hour, minute, and "AM" or "PM" using the "hh:mm AM/PM" format.
Short Time | Shows the hour and minute using the hh:mm format.
Yes/No | Any nonzero numeric value (usually –1) is Yes. Zero is No.
True/False | Any nonzero numeric value (usually –1) is True. Zero is False.
On/Off | Any nonzero numeric value (usually –1) is On. Zero is Off.

The Format function supports many other special characters, such as the percentage placeholder and exponents.

For More Information See "Format Function" in the Language Reference.

Number Formats

The following number conversions assume that the country in the Windows Control Panel is set to "English (United States)."

Format syntax | Result
Format(8315.4, "00000.00") | 08315.40
Format(8315.4, "#####.##") | 8315.4
Format(8315.4, "##,##0.00") | 8,315.40
Format(315.4,"$##0.00") | $315.40

The symbol for the decimal separator is a period (.), and the symbol for the thousands separator is a comma (,). However, the separator character that is actually displayed depends on the country specified in the Windows Control Panel.

Printing Formatted Dates and Times

To print formatted dates and times, use the Format function with symbols representing date and time. These examples use the Now and Format functions to identify and format the current date and time. The following examples assume that the Regional Settings dialog box of the Windows Control Panel is set to "English(United States)".

Format syntax | Result
Format(Now, "m/d/yy") | 1/27/93
Format(Now, "dddd, mmmm dd, yyyy") | Wednesday, January 27, 1993
Format(Now, "d-mmm") | 27-Jan
Format(Now, "mmmm-yy") | January-93
Format(Now, "hh:mm AM/PM") | 07:18 AM
Format(Now, "h:mm:ss a/p") | 7:18:00 a
Format(Now, "d-mmmm h:mm") | 3-January 7:18

By using the Now function with the format "ddddd" and "ttttt, " you can print the current date and time in a format appropriate for the selection in the Regional Settings dialog box of the Windows Control Panel.

Country | Format syntax | Result
Sweden | Format(Now, "ddddd ttttt") | 1992-12-31 18.22.38
United Kingdom | Format(Now, "ddddd ttttt") | 31/12/92 18:22:38
Canada (French) | Format(Now, "ddddd ttttt") | 92-12-31 18:22:38
United States | Format(Now, "ddddd ttttt") | 12/31/92 6:22:38 PM

For More Information For more information about international considerations when using the Format function, see "Locale-Aware Functions" in "International Issues. " For more information about dates based on system locale, see "Writing International Code in Visual Basic" in "International Issues. "


Working with Selected Text

Text boxes and combo boxes have a series of properties for selected text that are especially useful when working with the Clipboard. These properties, which refer to the block of text selected (highlighted) inside the control, allow you to create cut-and-paste functions for the user. The following properties can all be changed at run time.

Property | Description
SelStart | A Long integer that specifies the starting position of the selected block of text. If no text is selected, this property specifies the position of the insertion point. A setting of 0 indicates the position just before the first character in the text box or combo box. A setting equal to the length of the text in the text box or combo box indicates the position just after the last character in the control.
SelLength | A Long integer that specifies the number of characters selected.
SelText | The String containing the selected characters (or an empty string, if no characters are selected).

You can control what text is selected by setting the SelStart and SelLength properties. For example, these statements highlight all the text in a text box:

Text1.SetFocus
' Start highlight before first character.
Text1.SelStart = 0
' Highlight to end of text.
Text1.SelLength = Len(Text1.Text)

If you assign a new string to SelText, that string replaces the selected text, and the insertion point is placed just after the end of the newly inserted text. For example, the following statement replaces the selected text with the string "I’ve just been inserted!":

Text1.SelText = "I’ve just been inserted!"

If no text was selected, the string is simply pasted into the text box at the insertion point.

For More Information See "SelStart Property," "SelLength Property," and "SelText Property" in the Language Reference.


Transferring Text and Graphics with the Clipboard Object

The Clipboard object has no properties or events, but it has several methods that allow you to transfer data to and from the environment’s Clipboard. The Clipboard methods fall into three categories. The GetText and SetText methods are used to transfer text. The GetData and SetData methods transfer graphics. The GetFormat and Clear methods work with both text and graphic formats.

To learn more about the Clipboard, see the following topics:

  • Cutting, Copying, and Pasting Text with the Clipboard Discusses the GetText and SetText methods.

  • Working with Multiple Formats on the Clipboard Covers the methods that work with data formats other than text.

  • Checking the Data Formats on the Clipboard Describes using the GetFormat method to determine the format of data on the Clipboard.

For More Information For information about transferring data within your application or between applications, see "OLE Drag and Drop" in "Responding to Mouse and Keyboard Events."


Cutting, Copying, and Pasting Text with the Clipboard

Two of the most useful Clipboard methods are SetText and GetText. These two methods transfer string data to and from the Clipboard, as shown in Figure 12.2.

Figure 12.2 Moving data to and from the Clipboard with SetText and GetText

SetText copies text onto the Clipboard, replacing whatever text was stored there before. You use SetText like a statement. Its syntax is:

Clipboard.SetText data[, format]

GetText returns text stored on the Clipboard. You use it like a function:

destination = Clipboard.GetText()

By combining the SetText and GetText methods with the selection properties introduced in "Working with Selected Text," you can easily write Copy, Cut, and Paste commands for a text box. The following event procedures implement these commands for controls named mnuCopy, mnuCut, and mnuPaste:

Private Sub mnuCopy_Click ()
   Clipboard.Clear
   Clipboard.SetText Text1.SelText
End Sub

Private Sub mnuCut_Click ()
   Clipboard.Clear
   Clipboard.SetText Text1.SelText
   Text1.SelText = ""
End Sub

Private Sub mnuPaste_Click ()
   Text1.SelText = Clipboard.GetText()
End Sub

Note The example works best if these are menu controls, because you can use menus while Text1 has the focus.

Notice that both the Copy and Cut procedures first empty the Clipboard with the Clear method. (The Clipboard is not cleared automatically because you may want to place data on the Clipboard in several different formats, as described in "Working with Multiple Formats on the Clipboard" later in this chapter.) Both the Copy and Cut procedures then copy the selected text in Text1 onto the Clipboard with the following statement:

Clipboard.SetText Text1.SelText

In the Paste command, the GetText method returns the string of text currently on the Clipboard. An assignment statement then copies this string into the selected portion of the text box (Text1.SelText). If no text is currently selected, Visual Basic places this text at the insertion point in the text box:

Text1.SelText = Clipboard.GetText()

This code assumes that all text is transferred to and from the text box Text1, but the user can copy, cut, and paste between Text1 and controls on other forms.

Because the Clipboard is shared by the entire environment, the user can also transfer text between Text1 and any application using the Clipboard.

Working with the ActiveControl Property

If you want the Copy, Cut, and Paste commands to work with any text box that has the focus, use the ActiveControl property of the Screen object. The following code provides a reference to whichever control has the focus:

Screen.ActiveControl

You can use this fragment just like any other reference to a control. If you know that the control is a text box, you can refer to any of the properties supported for text boxes, including Text, SelText, and SelLength. The following code assumes that the active control is a text box, and uses the SelText property:

Private Sub mnuCopy_Click ()
   Clipboard.Clear
   Clipboard.SetText Screen.ActiveControl.SelText
End Sub

Private Sub mnuCut_Click ()
   Clipboard.Clear
   Clipboard.SetText Screen.ActiveControl.SelText
   Screen.ActiveControl.SelText = ""
End Sub

Private Sub mnuPaste_Click ()
   Screen.ActiveControl.SelText = Clipboard.GetText()
End Sub


Working with Multiple Formats on the Clipboard

You can actually place several pieces of data on the Clipboard at the same time, as long as each piece is in a different format. This is useful because you don’t know what application will be pasting the data, so supplying the data in several different formats enhances the chance that you will provide it in a format that the other application can use. The other Clipboard methods — GetData, SetData, and GetFormat — allow you to deal with data formats other than text by supplying a number that specifies the format. These formats are described in the following table, along with the corresponding number.

Constant | Description
vbCFLink | Dynamic data exchange link.
vbCFText | Text. Examples earlier in this chapter all use this format.
vbCFBitmap | Bitmap.
vbCFMetafile | Metafile.
vbCFDIB | Device-independent bitmap.
vbCFPalette | Color palette.

You can use the last four formats when cutting and pasting data from picture box controls. The following code provides generalized Cut, Copy, and Paste commands that work with any of the standard controls.

Private Sub mnuCopy_Click ()
   Clipboard.Clear
   If TypeOf Screen.ActiveControl Is TextBox Then
      Clipboard.SetText Screen.ActiveControl.SelText
   ElseIf TypeOf Screen.ActiveControl Is ComboBox Then
      Clipboard.SetText Screen.ActiveControl.Text
   ElseIf TypeOf Screen.ActiveControl Is PictureBox _
         Then
      Clipboard.SetData Screen.ActiveControl.Picture
   ElseIf TypeOf Screen.ActiveControl Is ListBox Then
      Clipboard.SetText Screen.ActiveControl.Text
   Else
      ' No action makes sense for the other controls.
   End If
End Sub

Private Sub mnuCut_Click ()
   ' First do the same as a copy.
   mnuCopy_Click
   ' Now clear contents of active control.
   If TypeOf Screen.ActiveControl Is TextBox Then
      Screen.ActiveControl.SelText = ""
   ElseIf TypeOf Screen.ActiveControl Is ComboBox Then
      Screen.ActiveControl.Text = ""
   ElseIf TypeOf Screen.ActiveControl Is PictureBox _
         Then
      Screen.ActiveControl.Picture = LoadPicture()
   ElseIf TypeOf Screen.ActiveControl Is ListBox Then
      Screen.ActiveControl.RemoveItem Screen.ActiveControl.ListIndex
   Else
      ' No action makes sense for the other controls.
   End If
End Sub

Private Sub mnuPaste_Click ()
   If TypeOf Screen.ActiveControl Is TextBox Then
      Screen.ActiveControl.SelText = Clipboard.GetText()
   ElseIf TypeOf Screen.ActiveControl Is ComboBox Then
      Screen.ActiveControl.Text = Clipboard.GetText()
   ElseIf TypeOf Screen.ActiveControl Is PictureBox _
         Then
      Screen.ActiveControl.Picture = _
         Clipboard.GetData()
   ElseIf TypeOf Screen.ActiveControl Is ListBox Then
      Screen.ActiveControl.AddItem Clipboard.GetText()
   Else
      ' No action makes sense for the other controls.
   End If
End Sub


Checking the Data Formats on the Clipboard

You can use the GetFormat method to determine whether the data on the Clipboard is in a particular format. For example, you can disable the Paste command depending on whether the data on the Clipboard is compatible with the currently active control.

Private Sub mnuEdit_Click ()
' Click event for the Edit menu.
   mnuCut.Enabled = True
   mnuCopy.Enabled = True
   mnuPaste.Enabled = False
   If TypeOf Screen.ActiveControl Is TextBox Then
      If Clipboard.GetFormat(vbCFText) Then mnuPaste.Enabled = True
   ElseIf TypeOf Screen.ActiveControl Is ComboBox Then
      If Clipboard.GetFormat(vbCFText) Then mnuPaste.Enabled = True
   ElseIf TypeOf Screen.ActiveControl Is ListBox Then
      If Clipboard.GetFormat(vbCFText) Then mnuPaste.Enabled = True
   ElseIf TypeOf Screen.ActiveControl Is PictureBox _
         Then
      If Clipboard.GetFormat(vbCFBitmap) Then mnuPaste.Enabled = True
   Else
      ' Can't cut or copy from the other types
      '   of controls.
      mnuCut.Enabled = False
      mnuCopy.Enabled = False
   End If
End Sub

Note You might also want to check for other data formats with the constants vbCFPalette, vbCFDIB, and vbCFMetafile. If you want to replace a picture’s palette using Clipboard operations, you should request vbCFBitmap rather than vbCFDIB from the Clipboard. See "Working with 256 Colors" later in this chapter for more information on working with the color palette.

For More Information See "Clipboard Object" in the Language Reference.


Using the Coordinate System

Understanding the Coordinate System

Every graphical operation described in this chapter (including resizing, moving, and drawing) uses the coordinate system of the drawing area or container. Although you can use the coordinate system to achieve graphical effects, it is also important to know how to use the coordinate system to define the location of forms and controls in your application.

The coordinate system is a two-dimensional grid that defines locations on the screen, in a form, or other container (such as a picture box or Printer object). You define locations on this grid using coordinates in the form:

(x, y)

The value of x is the location of the point along the x-axis, with the default location of 0 at the extreme left. The value of y is the location of the point along the y-axis, with the default location of 0 at the extreme top. This coordinate system is illustrated in Figure 12.3.

Figure 12.3 The coordinate system of a form

The following rules apply to the Visual Basic coordinate system:

  • When you move or resize a control, you use the coordinate system of the control’s container. If you draw the object directly on the form, the form is the container. If you draw the control inside a frame or picture box, the frame or the control is the container.

  • All graphics and Print methods use the coordinate system of the container. For example, statements that draw inside a picture box use the coordinate system of that control.

  • Statements that resize or move a form always express the form’s position and size in twips.

    When you create code to resize or move a form, you should first check the Height and Width properties of the Screen object to make sure the form will fit on the screen.

  • The upper-left corner of the screen is always (0, 0). The default coordinate system for any container starts with the (0, 0) coordinate in the upper-left corner of the container.

The units of measure used to define locations along these axes are collectively called the scale. In Visual Basic, each axis in the coordinate system can have its own scale.

You can change the direction of the axis, the starting point, and the scale of the coordinate system, but use the default system for now. "Changing an Object’s Coordinate System" later in this chapter discusses how to make these changes.

Twips Explained

By default, all Visual Basic movement, sizing, and graphical-drawing statements use a unit of one twip. A twip is 1/20 of a printer’s point (1,440 twips equal one inch, and 567 twips equal one centimeter). These measurements designate the size an object will be when printed. Actual physical distances on the screen vary according to the monitor size. "Changing an Object’s Coordinate System" describes how to select units other than twips.


Changing an Object's Coordinate System

You set the coordinate system for a particular object (form or control) using the object’s scale properties and the Scale method. You can use the coordinate system in one of three different ways:

  • Use the default scale.

  • Select one of several standard scales.

  • Create a custom scale.

Changing the scale of the coordinate system can make it easier to size and position graphics on a form. For example, an application that creates bar charts in a picture box can change the coordinate system to divide the control into four columns, each representing a bar in the chart. The following sections explain how to set default, standard, and custom scales to change the coordinate system.

Using the Default Scale

Every form and picture box has several scale properties (ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight, and ScaleMode) and one method (Scale) you can use to define the coordinate system. The default scale for objects in Visual Basic places the coordinate (0,0) at the upper-left corner of the object. The default scale uses twips.

If you want to return to the default scale, use the Scale method with no arguments.

Selecting a Standard Scale

Instead of defining units directly, you can define them in terms of a standard scale by setting the ScaleMode property to one of the settings shown in the following table.

ScaleMode setting | Description
0 | User-defined. If you set ScaleWidth, ScaleHeight, ScaleTop, or ScaleLeft directly, the ScaleMode property is automatically set to 0.
1 | Twips. This is the default scale. There are 1,440 twips to one inch.
2 | Points. There are 72 points to one inch.
3 | Pixels. A pixel is the smallest unit of resolution on the monitor or printer. The number of pixels per inch depends on the resolution of the device.
4 | Characters. When printed, a character is 1/6 of an inch high and 1/12 of an inch wide.
5 | Inches.
6 | Millimeters.
7 | Centimeters.

All of the modes in the table, except for 0 and 3, refer to printed lengths. For example, an item that is two units long when ScaleMode is set to 7 is two centimeters long when printed.

' Set scale to inches for this form.
ScaleMode = 5
' Set scale to pixels for picPicture1.
picPicture1.ScaleMode = 3

Setting a value for ScaleMode causes Visual Basic to redefine ScaleWidth and ScaleHeight so that they are consistent with the new scale. ScaleTop and ScaleLeft are then set to 0. Directly setting ScaleWidth, ScaleHeight, ScaleTop, or ScaleLeft automatically sets ScaleMode to 0.

Creating a Custom Scale

You can use an object’s ScaleLeft, ScaleTop, ScaleWidth, and ScaleHeight properties to create a custom scale. Unlike the Scale method, these properties can be used either to set the scale or to get information about the current scale of the coordinate system.

Using ScaleLeft and ScaleTop

The ScaleLeft and ScaleTop properties assign numeric values to the upper-left corner of an object. For example, these statements set the value of the upper-left corner for the current form and upper-left corner for a picture box named picArena.

ScaleLeft = 100
ScaleTop = 100
picArena.ScaleLeft = 100
picArena.ScaleTop = 100

These scale values are shown in Figure 12.4.

Figure 12.4 The ScaleLeft and ScaleTop properties for a form and a control

These statements define the upper-left corner as (100, 100). Although the statements don’t directly change the size or position of these objects, they alter the effect of subsequent statements. For example, a subsequent statement that sets a control’s Top property to 100 places the object at the very top of its container.

Using ScaleWidth and ScaleHeight

The ScaleWidth and ScaleHeight properties define units in terms of the current width and height of the drawing area. For example:

ScaleWidth = 1000
ScaleHeight = 500

These statements define a horizontal unit as 1/1,000 of the current internal width of the form and a vertical unit as 1/500 of the current internal height of the form. If the form is later resized, the units remain the same.

Note ScaleWidth and ScaleHeight define units in terms of the internal dimensions of the object; these dimensions do not include the border thickness or the height of the menu or caption. Thus, ScaleWidth and ScaleHeight always refer to the amount of room available inside the object. The distinction between internal and external dimensions (specified by Width and Height) is particularly important with forms, which can have a thick border. The units can also differ: Width and Height are always expressed in terms of the container’s coordinate system; ScaleWidth and ScaleHeight determine the coordinate system of the object itself.

Setting Properties to Change the Coordinate System

All four of these scale properties can include fractions and they can also be negative numbers. Negative settings for the ScaleWidth and ScaleHeight properties change the orientation of the coordinate system.

The scale shown in Figure 12.5 has ScaleLeft, ScaleTop, ScaleWidth, and Scale Height all set to 100.

Figure 12.5 Scale running from (100, 100) to (200, 200)

Using the Scale Method to Change the Coordinate System

A more efficient way to change the coordinate system, other than setting individual properties, is to use the Scale method. You specify a custom scale using this syntax:

[object.]Scale (x1, y1) – (x2, y2)

The values of x1 and y1 determine the settings of the ScaleLeft and ScaleTop properties. The differences between the two x-coordinates and the two y-coordinates determine the settings of ScaleWidth and ScaleHeight, respectively. For example, suppose you set the coordinate system for a form by setting end points (100, 100) and (200, 200):

Scale (100, 100)-(200, 200)

This statement defines the form as 100 units wide and 100 units high. With this scale in place, the following statement moves a shape control one-fifth of the way across the form:

shpMover.Left = shpMover.Left + 20

Specifying a value of x1 > x2 or y1 > y2 has the same effect as setting ScaleWidth or ScaleHeight to a negative value.


Converting Scales

Use the ScaleX and ScaleY methods to convert from one scale mode to another scale mode. Those methods have the following syntax:

[object.]ScaleX (value [, fromScale [, toScale]]

[object.]ScaleY (value [, fromScale[,toScale]]

The destination object is a form, picture box, or Printer object. The value is expressed in the coordinate system specified by the scale mode fromScale. The value returned is expressed in the scale mode specified by toScale, or the scale mode of object if toScale is omitted. If fromScale is omitted, the scale mode for value is HIMETRIC.

HIMETRIC is the scale mode that specifies physical sizes. For example, the number of HIMETRIC units in a line of 10 centimeters is 10,000. The resulting line drawn on the screen is ten centimeters long, regardless of the size of the video display area. For information on the HIMETRIC scale mode and physical sizes, see the Microsoft Windows SDK.

The following statement stretches the content of the picture box control MyPic to twice its width. MyPic.Picture.Width returns the width of the picture contained in the picture control, which is a HIMETRIC value that needs to be converted into the scale mode of Form1.

Form1.PaintPicture MyPic.Picture, X, Y, _
   Form1.ScaleX(MyPic.Picture.Width) * 2

The following example illustrates two equivalent ways to specify a form’s Width to np pixels wide.

' The ScaleMode of the form is set to pixels.
ScaleMode = vbPixels

' Option 1:
' Temporarily set the form’s ScaleMode to twips.
ScaleMode = vbTwips
' ScaleX() returns the value in twips.
Width = Width - ScaleWidth + ScaleX(np, vbPixels)
' Set back the ScaleMode of the form to pixels.
ScaleMode = vbPixels
' Option 2:
' Conversion from pixels to twips without changing
'    the ScaleMode of the form.
Width = Width + ScaleX(np - ScaleWidth, vbPixels, _
   vbTwips)

For More Information See "ScaleX Property" or "ScaleY Property" in the Language Reference.


Using Graphical Controls

Visual Basic provides three controls designed to create graphical effects in an application:

  • The image control

  • The line control

  • The shape control

Advantages of Graphical Controls

The image, line, and shape controls are very useful for creating graphics at design time. One advantage of graphical controls is that they require fewer system resources than other Visual Basic controls, which improves the performance of your Visual Basic application.

Another advantage of graphical controls is that you can create graphics with less code than with graphics methods. For example, you can use either the Circle method or the shape control to place a circle on a form. The Circle method requires that you create the circle with code at run time, while you can simply draw the shape control on the form and set the appropriate properties at design time.

Limitations of Graphical Controls

While graphical controls are designed to maximize performance with minimal demands on the application, they accomplish this goal by limiting other features common to controls in Visual Basic. Graphical controls:

  • Cannot appear on top of other controls, unless they are inside a container that can appear on top of other controls (such as a picture box).

  • Cannot receive focus at run time.

  • Cannot serve as containers for other controls.

  • Do not have an hWnd property.

For More Information For information about the graphics methods, see "Using the Graphics Methods" later in this chapter. For information about the graphical controls, see "Using the Image Control," "Using the Line Control," and "Using the Shape Control" in "Using Visual Basic's Standard Controls." For information about the effect of graphics on your application's performance, see especially "Cutting Back on Graphics" in "Designing for Performance and Compatibility."


Adding Pictures to Your Application

Pictures can be displayed in three places in Visual Basic applications:

  • On a form

  • In a picture box

  • In an image control

Pictures can come from paint programs, such as those that ship with the various versions of Microsoft Windows, other graphics applications, or clip-art libraries. Visual Basic provides a large collection of icons you can use as graphics in applications. Visual Basic allows you to add .jpg and .gif files, as well as .bmp, .dib, .ico, .cur, .wmf, and .emf files to your applications. For more information about the graphics formats supported by Visual Basic, see "Using the Image Control" and "Using the Picture Box Control" in "Using Visual Basic's Standard Controls."

You use different techniques to add a picture to a form, a picture box, or an image control depending on whether you add the picture at design time or run time.

Adding a Picture at Design Time

There are two ways to add a picture at design time:

  • Load a picture onto a form, or into a picture box or image control from a picture file:

    In the Properties window, select Picture from the Properties list and click the Properties button. Visual Basic displays a dialog box, from which you select a picture file.

    If you set the Picture property for a form, the picture you select is displayed on the form, behind any controls you’ve placed on it. Likewise, if you set the Picture property for a picture box, the picture is displayed in the box, behind any controls you’ve placed on it.

  • Paste a picture onto a form or into a picture box or image control:

    Copy a picture from another application (such as Microsoft Paint) onto the Clipboard. Return to Visual Basic, select the form, picture box, or image control, and from the Edit menu, choose Paste.

Once you’ve set the Picture property for a form, picture box, or image control — either by loading or pasting a picture — the word displayed in the Settings box is " (Bitmap), " " (Icon), " or " (Metafile). " To change the setting, load or paste another picture. To set the Picture property to " (None) " again, double-click the word displayed in the Settings box and press the DEL key.

Adding a Picture at Run Time

There are four ways to add a picture at run time:

  • Use the LoadPicture function to specify a file name and assign the picture to the Picture property.

    The following statement loads the file Cars.bmp into a picture box named picDisplay (you name a control by setting its Name property):

    picDisplay.Picture = LoadPicture("C:\Picts\Cars.bmp")

    You can load a new picture file onto a form or into a picture box or image control whenever you want. Loading a new picture completely replaces the existing picture, although the source files of the pictures are never affected.

  • Use the LoadResPicture function to assign a picture from the project’s .res file into the Picture property.

    The following statement loads the bitmap resource ID, 10, from the resource file into a picture box named picResource:

    Set picResource.Picture = LoadResPicture(10, _
vbResBitmap)
  • Copy a picture from one object to another.

    Once a picture is loaded or pasted onto a form or into a picture box or image control, you can assign it to other forms, picture boxes, or image controls at run time. For example, this statement copies a picture from a picture box named picDisplay to an image control named imgDisplay:

    Set imgDisplay.Picture = picDisplay.Picture
  • Copy a picture from the Clipboard object.

For More Information For more information about copying a picture from the Clipboard, see "Working with Multiple Formats on the Clipboard."

For information on resource files, see "Working with Resource Files" in "More About Programming."

Note If you load or paste pictures from files at design time, the pictures are saved and loaded with the form, and the application copies pictures from one object to another. Then, when you create an .exe file, you don’t need to give your users copies of the picture files; the .exe file itself contains the images. Also, consider supplying a .res file and using LoadResPicture. The .res file gets built into the .exe, and the bitmaps are saved in a standard format that any resource editor can read. If you load pictures at run time with the LoadPicture function, you must supply the picture files to your users along with your application.

Removing a Picture at Run Time

You can also use the LoadPicture function to remove a picture at run time without replacing it with another picture. The following statement removes a picture from an image control named imgDisplay:

Set imgDisplay.Picture = LoadPicture("")

Moving and Sizing Pictures

If a form, picture box, or image control is moved (at design time or run time), its picture automatically moves with it. If a form, picture box, or image control is resized so that it is too small to display a picture, the picture gets clipped at the right and bottom. A picture also gets clipped if you load or copy it onto a form or into a picture box or image control that is too small to display all of it.

AutoSize Property

If you want a picture box to automatically expand to accommodate a new picture, set the AutoSize property for the picture box to True. Then when a picture is loaded or copied into the picture box at run time, Visual Basic automatically expands the control down and to the right enough to display all of the picture. If the image you load is larger than the edges of the form, it appears clipped because the form size doesn’t change.

You can also use the AutoSize property to automatically shrink a picture box to reflect the size of a new picture.

Note Image controls do not have an AutoSize property, but automatically size themselves to fit the picture loaded into them. Forms don’t have an AutoSize property, and they do not automatically enlarge to display all of a picture.

Stretch Property of Image Controls

If you want a picture in an image control to automatically expand to fit a particular size, use the Stretch property. When the Stretch property is False, the image control automatically adjusts its size to fit the picture loaded into it. To resize the picture to fit the image control, set the Stretch property for the image control to True.

Selecting Art for the Picture Control

Where do you get picture files? If you want icons, you can use the Icon Library included with Visual Basic. You can find the icon files within the subdirectories of the main Visual Basic directory (\Vb\Graphics\Icons). You can create .bmp files with Microsoft Paint, or you can buy a clip-art collection that includes bitmap or icon files, or metafiles. You can also create a resource (.res) file containing pictures.

For More Information See "Working with Resource Files" in "More About Programming, for more information on creating a resource file.


Introduction to Graphics Properties for Forms and Controls

Forms and various controls have graphics properties. The following table lists these properties.

Category | Properties
Display processing | AutoRedraw, ClipControls
Current drawing location | CurrentX, CurrentY
Drawing techniques | DrawMode, DrawStyle, DrawWidth, BorderStyle, BorderWidth
Filling techniques | FillColor, FillStyle
Colors | BackColor, ForeColor, BorderColor, FillColor

Forms and picture boxes have additional properties:

  • Scale properties, as described in "Changing an Object’s Coordinate System" earlier in this chapter.

  • Font properties, as described in "Setting Font Characteristics" earlier in this chapter.

There are two properties of forms and picture boxes you’ll probably want to use right away: BackColor and ForeColor. BackColor paints the background of the drawing area. If BackColor is light blue, then the entire area is light blue when you clear it. ForeColor (foreground) determines the color of text and graphics drawn on an object, although some graphics methods give you the option of using a different color. For more information about color, see "Working with Color" later in this chapter.


Creating Persistent Graphics with AutoRedraw

Each form and picture box has an AutoRedraw property. AutoRedraw is a Boolean property that, when set to True, causes graphics output to be saved in memory. You can use the AutoRedraw property to create persistent graphics.

Persistent Graphics

Microsoft Windows manipulates the screen image to create an illusion of overlapping windows. When one window is moved over another, temporarily hiding it, and is then moved away again, the window and its contents need to be redisplayed. Windows takes care of redisplaying the window and controls. But your Visual Basic application must handle redisplaying graphics in a form or picture box.

If you create graphics on the form using graphics methods, you usually want them to reappear exactly as you placed them (persistent graphics). You can use the AutoRedraw property to create persistent graphics.

AutoRedraw and Forms

The default setting of AutoRedraw is False. When AutoRedraw is set to False, any graphics created by graphics methods that appear on the form are lost if another window temporarily hides them. Also, graphics that extend beyond the edges of the form are lost if you enlarge the form. The effects of setting AutoRedraw to False are shown in Figure 12.6.

Figure 12.6 The effects of setting AutoRedraw to False

When the AutoRedraw property of a form is set to True, Visual Basic applies graphics methods to a "canvas" in memory. The application copies the contents of this memory canvas to redisplay graphics temporarily hidden by another window. In most cases, the size of this canvas for forms is the size of the screen. If the form’s MaxButton property is False and the border of the form is not sizable, the size of the canvas is the size of the form.

This canvas also lets the application save graphics that extend beyond the edges of the form when the form is resizable. The effects of setting AutoRedraw to True are shown in Figure 12.7.

Figure 12.7 The effects of setting AutoRedraw to True

AutoRedraw and Picture Boxes

When the AutoRedraw property of a picture box is set to True, Visual Basic saves only the visible contents of the picture box in memory. This is because the memory canvas used to save the contents of the picture box is the same size as the picture box. Graphics that extend outside the picture box are cropped and never appear later, even if the size of the picture box changes.

Using Nonpersistent Graphics

You can leave AutoRedraw set to False for the form and all its picture boxes to conserve memory. But then the graphics are not automatically persistent: You have to manage redrawing all graphics in code as needed.

You can include code in the Paint event for a form or picture box that redraws all lines, circles, and points as appropriate. This approach usually works best when you have a limited amount of graphics that you can reconstruct easily.

A Paint event procedure is called whenever part of a form or picture box needs to be redrawn — for example, when a window that covered the object moves away, or when resizing causes graphics to come back into view. If AutoRedraw is set to True, the object’s Paint procedure is never called unless your application calls it explicitly. The visible contents of the object are stored in the memory canvas, so the Paint event isn’t needed.

Keep in mind that the decision to use nonpersistent graphics can affect the way graphics paint on the form or container. "Clipping Regions with ClipControls" and "Layering Graphics with AutoRedraw and ClipControls" discuss other factors that may determine whether or not you should use nonpersistent graphics.

Changing AutoRedraw at Run Time

You can change the setting of AutoRedraw at run time. If AutoRedraw is False, graphics and output from the Print method are written only to the screen, not to memory. If you clear the object with the Cls method, any output written when AutoRedraw was set to True does not get cleared. This output is retained in memory, and you must set AutoRedraw to True again and then use the Cls method to clear it.

For More Information To learn about the performance implications of AutoRedraw, see "Optimizing Display Speed" in "Designing for Performance and Compatibility."


Clipping Regions with ClipControls

Each form, picture box, and frame control has a ClipControls property. ClipControls is a Boolean property that, when set to True, causes the container to define a clipping region when painting the container around all controls except:

  • The shape control

  • The line control

  • The image control

  • Labels

  • Any ActiveX graphical controls

By setting the ClipControls property to False, you can improve the speed with which a form paints to the screen. The speed improvement is greatest on forms with many controls that do not overlap, like dialog boxes.

Clipping Regions

Clipping is the process of determining which parts of a form or container are painted when the form or container is displayed. The outline used to determine what parts of the form or container are painted or "clipped" defines the clipping region for that form or container. Clipping regions are useful when a Windows – based application needs to save one part of the display and simultaneously repaint the rest.

Clipping Forms and Containers

The default setting of ClipControls is True. When the ClipControls property is True, Windows defines a clipping region for the background of the form or container before a Paint event. This clipping region surrounds all nongraphical controls. When using ClipControls, labels act like graphical controls.

During a Paint event, Windows repaints only the background inside the clipping region, avoiding the nongraphical controls. Figure 12.8 shows a form with four controls, a box painted with the Line method, and the clipping region created for that form by setting ClipControls to True. Notice that the clipping region did not clip around the label or shape controls on the form. The box drawn in the background with the Line method paints only in the clipping region.

Figure 12.8 The clipping region created when ClipControls is True

When ClipControls is False, Windows does not define a clipping region for the background of the form or container before a Paint event. Also, output from graphics methods within the Paint event appears only in the parts of the form or container that need to be repainted. Since calculating and managing a clipping region takes time, setting ClipControls to False may cause forms with many nonoverlapping controls (such as complex dialog boxes) to display faster.

Note Avoid nesting controls with ClipControls set to True inside controls with ClipControls set to False. Doing so may result in the nested controls not repainting correctly. To fix this, set ClipControls to True for both the containers and the controls.

For More Information See "Optimizing Display Speed" in "Designing for Performance and Compatibility."


Layering Graphics with AutoRedraw and ClipControls

Different combinations of AutoRedraw and ClipControls have different effects on the way graphical controls and graphics methods paint to the screen.

As you create graphics, keep in mind that graphical controls and labels, nongraphical controls, and graphics methods appear on different layers in a container. The behavior of these layers depends on three factors:

  • The AutoRedraw setting.

  • The ClipControls setting.

  • Whether graphics methods appear inside or outside the Paint event.

Normal Layering

Usually, the layers of a form or other container are, from front to back, as follows:

Layer | Contents
Front | Nongraphical controls like command buttons, check boxes, and file controls.
Middle | Graphical controls and labels.
Back | Drawing space for the form or container. This is where the results of graphics methods appear.

Anything in one layer covers anything in the layer behind, so graphics you create with the graphical controls appear behind the other controls on the form, and all graphics you create with the graphics methods appear below all graphical and nongraphical controls. The normal arrangement of layers is shown in Figure 12.9.

Figure 12.9 Normal layering of graphics on a form

Effects on Layering

You can produce normal layering using any of several approaches. Combining settings for AutoRedraw and ClipControls and placing graphics methods inside or outside the Paint event affects layering and the performance of the application.

The following table lists the effects created by different combinations of AutoRedraw and ClipControls and placement of graphics methods.

AutoRedraw | ClipControls | Graphics methods in/out of Paint event | Layering behavior
True | True (default) | Paint event ignored | Normal layering.
True | False | Paint event ignored | Normal layering. Forms with many controls that do not overlap may paint faster because no clipping region is calculated or created.
False (default) | True (default) | In | Normal layering.
False | True | Out | Nongraphical controls in front. Graphics methods and graphical controls appear mixed in the middle and back layers. Not recommended.
False | False | In | Normal layering, affecting only pixels that were previously covered or that appear when resizing a form.
False | False | Out | Graphics methods and all controls appear mixed in the three layers. Not recommended.

The Effects of AutoRedraw

Setting AutoRedraw to True always produces normal layering. While using AutoRedraw is the easiest way to layer graphics, applications with large forms may suffer from reduced performance due to the memory demands of AutoRedraw.

The Effects of ClipControls

When AutoRedraw is True, the setting of ClipControls has no effect on how graphics layer on a form or in a container. But ClipControls can affect how fast the form displays. When ClipControls is False, the application doesn’t create a clipping region. Not having to calculate or paint to avoid holes in a clipping region may cause the form to display faster.

Also, when AutoRedraw and ClipControls are both False, the application repaints only the pixels of a form or container that are exposed by:

  • Covering the form or container with another window and then moving the window away.

  • Resizing the form or container.

The Effects of the Paint Event

When AutoRedraw is False, the best place to use graphics methods is within the Paint event of the form or container. Confining graphics methods to the Paint event causes those methods to paint in a predictable sequence.

Using graphics methods outside a Paint event when AutoRedraw is False can produce unstable graphics. Each time the output of a graphics method appears on the form or container, it may cover any controls or graphics methods already there (if ClipControls is False). When an application uses more than a few graphics methods to create visual effects, managing the resulting output can be extremely difficult unless the methods are all confined to the Paint event.


Moving Controls Dynamically

With Visual Basic, one of the easiest effects to achieve is moving a control at run time. You can either directly change the properties that define the position of a control or use the Move method.

Using the Left and Top Properties

The Left property is the distance between the upper-left corner of the control and the left side of the form. The Top property is the distance between the upper-left corner of the control and the top of the form. Figure 12.10 shows the Left and Top properties of a control.

Figure 12.10 The Left and Top properties

You can move a control by changing the settings of its Left and Top properties with statements such as these:

txtField1.Left = txtField1.Left + 200
txtField1.Top = txtField1.Top – 300

Moving a Line Control

As mentioned previously, line controls don’t have Left or Top properties. Instead, you use special properties to control the position of line controls on a form. The following table lists these properties and how they determine the position of a line control.

Property | Description
X1 | The x-coordinate of the start of the line. The coordinate is given in current scale units. The start of the line is the end created when you start drawing.
Y1 | The y-coordinate of the start of the line.
X2 | The x-coordinate of the end of the line. The end of the line is the end created when you stop drawing.
Y2 | The y-coordinate of the end of the line.

The Jumpy Line demo of the Blanker application randomly changes the position of a line control on the DemoForm using these statements:

' Set random X position for 1st line end.
linLineCtl.X1 = Int(DemoForm.Width * Rnd)
' Set random Y position for 1st line end.
linLineCtl.Y1 = Int(DemoForm.Height * Rnd)
' Set random X position for 2nd line end.
linLineCtl.X2 = Int(DemoForm.Width * Rnd)
' Set random Y position for 2nd line end.
linLineCtl.Y2 = Int(DemoForm.Height * Rnd)
' Clear stray pixels from moving line.
Cls
' Pause display briefly before next move.
Delay

Using the Move Method

Changing the Left and Top or X and Y properties produces a jerky effect as the control first moves horizontally and then vertically. The Move method produces a smoother diagonal movement.

The syntax for the Move method is:

[object.]Move left [, top[, width[, height] ] ]

The object is the form or control to be moved. If object is omitted, the current form moves. The left and top arguments are the new settings for the Left and Top properties of object, while width and height are new settings for its Width and Height properties. Only left is required, but to specify other arguments, you must include all arguments that appear in the argument list before the argument you want to specify.

Absolute Movement

Absolute movement occurs when you move an object to specific coordinates in its container. The following statement uses absolute movement to move a control named txtField1 to the coordinates (100, 200):

txtField1.Move 100, 200

Relative Movement

Relative movement occurs when you move an object by specifying the distance it should move from its current position. The following statement uses relative movement to move txtField1 to a position 100 twips down and to the right of its current position:

txtField1.Move txtField1.Left + 100, txtField1.Top _
   + 100

This section shows control movement in the Blanker sample application. The Rebound demo moves a picture box diagonally around the form, so the picture box appears to "bounce" off the sides of the form. This demo uses a picture box instead of an image control because the image control flickers as the movement causes it to repaint.

Figure 12.11 shows the main form of the Blanker application (DemoForm) and the picture box used in this example.

Figure 12.11 Picture box (picBall) in the Blanker application

The name of the picture box is picBall. This control begins moving around the form after you choose the Rebound command from the Options menu and then click the Start Demo button. The event procedure for this command button then calls the CtlMoveDemo procedure.

The CtlMoveDemo procedure randomly selects a starting direction from one of these four possibilities:

  • Left and up

  • Right and up

  • Left and down

  • Right and down

The picBall picture box moves along the chosen direction until the control reaches one of the four edges of the form. Then the picture box changes direction away from the edge it has reached; the variable Motion controls the direction. For example, when the picture box is moving left and up, this portion of the procedure changes the value of Motion and directs the code to move picBall in another direction.

The following statements come from the CtlMoveDemo procedure in the Blanker application:

Select Case Motion
Case 1
   ' If motion is left and up, move the control
   '   20 twips.
   picBall.Move picBall.Left - 20, picBall.Top - 20
   ' If control touches left edge, change motion
   '   to right and up.
   If picBall.Left <= 0 Then
      Motion = 2
   ' If control touches top edge, change motion to
   '   left and down.
   ElseIf picBall.Top <= 0 Then
      Motion = 4
   End If

Notice that the line of code that moves picBall subtracts 20 twips from the current values of its Left and Top properties to establish the new location of the control. This ensures that the control always moves relative to its current position.

The speed and smoothness of the control’s movement depend on the number of twips (or other units) used in the Move method. Increasing the number of twips increases the speed but decreases the smoothness of motion. Decreasing the number of twips decreases the speed but improves the smoothness of the control’s motion.

For More Information For additional information on the Move method, see "Move Method" in the Language Reference.


Resizing Controls Dynamically

In a Visual Basic application, you can change the size and shape of a picture box, image control, or form at run time, just as you can change its position.

The following properties affect size.

Property | Applies to | Description
Align | Picture boxes and Data controls | If set to align a picture box to the top (1) or bottom (2) of a form, the width of the picture box always equals the width of the inside of the form. If set to align a picture box to the left (3) or the right (4) of a form, the height of the picture box is the height of the inside of the form.
Height | All forms and all controls except timers, menus, and lines | Height of the object expressed in the scale mode of the form (twips by default).
Width | All forms and all controls except timers, menus, and lines | Width of the object expressed in the scale mode of the form (twips by default).
AutoSize | Labels and picture boxes | If True, always causes Visual Basic to adjust the picture box dimensions to the size of the contents.
Stretch | Image controls | If True, the bitmap or metafile stretches to fit the size of the image control. If False, the size of the image control changes to match the size of the bitmap or metafile it contains.

In this example, a command button named cmdGrow grows larger each time the user clicks it:

Private Sub cmdGrow_Click ()
   cmdGrow.Height = cmdGrow.Height + 300
   cmdGrow.Width = cmdGrow.Width + 300
End Sub


Creating Simple Animation

You can create simple animation by changing pictures at run time. The easiest way to do this is to toggle between two images. You can also use a series of pictures to create animation with several frames. Also, by moving the picture dynamically, you can create more elaborate effects.

Toggling Between Two Pictures

Some icons can be used in pairs. For instance, there are two matching envelope icons in the \Vb\Graphics\Icons subdirectory, one with the envelope unopened and one with the envelope torn open, as shown in Figure 12.12. By switching, or toggling, between the two, you can create an animation that shows your user the status of mail.

Figure 12.12 Mail icons

The following statement changes the Picture property of an image control named imgMailStatus to toggle its picture from an unopened envelope to an open envelope.

imgMailStatus.Picture = imgMailOpen.Picture

Rotating Through Several Pictures

You can also rotate through several pictures to make longer animations. This technique is basically the same as toggling between two pictures, but requires the application to select which bitmap acts as the current image. One way to control the individual pictures in an animation is with a control array.

For More Information See "Creating Arrays of Objects" in "Programming with Objects" for more information about control arrays.

The Blanker sample application includes an animation that shows a rotating moon. The Spinning Moon demo uses an array of nine image controls to create the animation. To view how the images in a control array work with each other at run time, choose Spinning Moon from the Options menu, and then choose the Start Demo button, which calls the ImageDemo procedure.


Using Graphics Methods

In addition to the graphical controls, Visual Basic provides several methods for creating graphics. The graphics methods, summarized in the following table, apply to forms and picture boxes.

Method | Description
Cls | Clears all graphics and Print output.
PSet | Sets the color of an individual pixel.
Point | Returns the color value of a specified point.
Line | Draws a line, rectangle, or filled-in box.
Circle | Draws a circle, ellipse, or arc.
PaintPicture | Paints graphics at arbitrary locations.

Note The Print method can also be considered a graphics method, because its output is written to the object and is saved in the memory image (if AutoRedraw is on) just like the PSet, Line, and Circle methods. For more information about the Print method, see "Displaying Text on Forms and Picture Boxes" earlier in this chapter.

Advantages of Graphics Methods

The graphics methods work well in situations where using graphical controls require too much work. For example, creating gridlines on a graph would need an array of line controls but only a small amount of code using the Line method. Tracking the position of line controls in an array as the form changes size is more work than simply redrawing lines with the Line method.

When you want a visual effect to appear briefly on a form, such as a streak of color when you display an About dialog, you can write a couple of lines of code for this temporary effect instead of using another control.

Graphics methods offer some visual effects that are not available in the graphical controls. For example, you can only create arcs or paint individual pixels using the graphics methods. Graphics you create with these graphics methods appear on the form in a layer of their own. This layer is below all other controls on a form, so using the graphics methods can work well when you want to create graphics that appear behind everything else in your application.

For More Information See "Layering Graphics with AutoRedraw and ClipControls" earlier in this chapter.

Limitations of Graphics Methods

Creating graphics with the graphics methods takes place in code, which means you have to run the application to see the effect of a graphics method. Graphics methods therefore don't work as well as graphical controls for creating simple design elements of an interface. Changing the appearance of graphical controls at design time is easier than modifying and testing the code for a graphics method.

For More Information For information about creating graphical applications with the mouse events and the Line or Move methods, see "The MouseDown Event," "The MouseMove Event" and "Using Button to Enhance Graphical Mouse Applications" in "Responding to Mouse and Keyboard Events."


The Fundamentals of Drawing with Graphics Methods

Every graphics method draws output on a form, in a picture box, or to the Printer object. To indicate where you want to draw, precede a graphics method with the name of a form or picture box control. If you omit the object, Visual Basic assumes you want to draw on the form to which the code is attached. For example, the following statements draw a point on:

  • A form named MyForm
    MyForm.PSet (500, 500)
  • A picture box named picPicture1
    picPicture1.PSet (500, 500)
  • The current form
    PSet (500, 500)

Each drawing area has its own coordinate system that determines what units apply to the coordinates. In addition, every drawing area has its own complete set of graphics properties.

For More Information See "Printing from an Application" later in this chapter for more information about the Printer object. See "Understanding the Coordinate System" for more information about coordinates.

Clearing the Drawing Area

Any time you want to clear a drawing area and start over, use the Cls method. The specified drawing area is repainted in the background color (BackColor):

[object.]Cls

Using the Cls method without a specified object clears the form to which the code is attached.


Plotting Points

Controlling an individual pixel is a simple graphics operation. The PSet method sets the color of a pixel at a specified point:

[object.]PSet (x, y)[, color]

The x and y arguments are single precision, so they can take either integer or fractional input. The input can be any numeric expression, including variables.

If you don’t include the color argument, PSet sets a pixel to the foreground color (ForeColor). For example, the following statements set various points on the current form (the form to which the code is attached), MyForm, and picPicture1:

PSet (300, 100)
PSet (10.75, 50.33)
MyForm.PSet (230, 1000)
picPicture1.PSet (1.5, 3.2)

Adding a color argument gives you more control:

' Set 50, 75 to bright blue.
PSet (50, 75), RGB(0, 0, 255)

The Blanker application plots points with randomly selected colors to create the Confetti demo. The PSetDemo procedure creates the confetti:

Sub PSetDemo ()
   ' Set Red to random value.
   R = 255 * Rnd
   ' Set Green to random value.
   G = 255 * Rnd
   ' Set Blue to random value.
   B = 255 * Rnd
   ' Set horizontal position.
   XPos = Rnd * ScaleWidth
   ' Set vertical position.
   YPos = Rnd * ScaleHeight
   ' Plot point with random color.
   PSet (XPos, YPos), RGB(R, G, B)
End Sub

The resulting confetti display is shown in Figure 12.13.

Figure 12.13 Confetti display in the Blanker application

To "erase" a point, set it to the background color:

PSet (50, 75), BackColor

As described in "Drawing Lines and Shapes" later in this chapter, you can precede the (x, y) coordinates by Step, which makes the point relative to the last location drawn.

The Point method is closely related to the PSet method, but it returns the color value at a particular location:

PointColor = Point (500, 500)

For More Information For more information, see "PSet Method" and "Point Method" in the Language Reference.


Drawing Lines and Shapes

Although clearing the drawing area and plotting individual points can be useful, the most interesting graphics methods draw complete lines and shapes.

Drawing Lines

To draw a line between two coordinates, use the simple form of the Line method, which has this syntax:

[object.]Line [(x1, y1)]–(x2, y2)[, color]

Object is optional; if omitted, the method draws on the form to which the code is attached (the current form). The first pair of coordinates is also optional. As with all coordinate values, the x and y arguments can be either integer or fractional numbers. For example, this statement draws a slanted line on a form.

Line (500, 500)–(2000, 2000)

Visual Basic draws a line that includes the first end point, but not the last end point. This behavior is useful when drawing a closed figure from point to point. To draw the last point, use this syntax:

PSet [Step] (0, 0)[, color]

The first pair of coordinates (x1, y1) is optional. If you omit these coordinates, Visual Basic uses the object’s current x, y location (drawing coordinates) as the end point. The current location can be specified with the CurrentX and CurrentY properties, but otherwise it is equal to the last point drawn by a previous graphics or Print method. If you haven’t previously used a graphics or Print method or set CurrentX and CurrentY, the default location is the object’s upper-left corner.

For example, the following statements draw a triangle by connecting three points.

' Set x-coordinate of starting point.
CurrentX = 1500
' Set y-coordinate of starting point.
CurrentY = 500
' Draw line down and right of starting point.
Line -(3000, 2000)
' Draw line to the left of current point.
Line -(1500, 2000)
' Draw line up and right to starting point.
Line -(1500, 500)

The results are shown in Figure 12.14.

Figure 12.14 A triangle drawn with the Line method

The Blanker application uses the Line method to create interesting patterns. To view this, from the Options menu, choose Crossfire, and then choose the Start Demo button.

The Step Keyword

The PSet, Line, and Circle methods specify one or more points using this syntax:

(x, y)

You can precede each of these points with the Step keyword, specifying that the location of the point is relative to the last point drawn. Visual Basic adds the x and y values to the values of the last point drawn. For example, the statement:

Line (100, 200)–(150, 250)

is equivalent to:

Line (100, 200)–Step(50, 50)

In many situations, the Step keyword saves you from having to constantly keep track of the last point drawn. Often you may be more interested in the relative position of two points than their absolute position.

Using the Color Argument

To vary the color of the line, use the optional color argument with graphics methods. For example, this statement draws a dark blue line:

Line (500, 500)–(2000, 2000), RGB(0, 0, 255)

If you omit the color argument, the ForeColor property for the object where the line is being drawn determines its color.


Drawing Boxes

You can draw and fill boxes using the Line method. The following example draws a box with an upper-left corner at (500, 500) and measuring 1,000 twips on each side:

Line (500, 500)–Step(1000, 0)
Line -Step(0, 1000)
Line -Step(–1000, 0)
Line -Step(0, –1000)

However, Visual Basic provides a much simpler way to draw a box. When you use the B option with the Line method, Visual Basic draws a rectangle, treating the specified points as opposite corners of the rectangle. Thus, you could replace the four statements of the previous example with the following:

Line (500, 500)–Step(1000, 1000), , B

Note that two commas are required before B, to indicate the color argument was skipped. The syntax of the Line method is covered in "Drawing Lines and Shapes" earlier in the chapter.

FillStyle and FillColor

As long as you do not change the setting of the FillStyle property, the box appears empty. (The box does get filled with the default FillStyle and settings, but FillStyle defaults to 1-Transparent.) You can change the FillStyle property to any of the settings listed in the following table.

Setting | Description
0 | Solid. Fills in box with the color set for the FillColor property.
1 | Transparent (the default). Graphical object appears empty, no matter what color is used.
2 | Horizontal lines.
3 | Vertical lines.
4 | Upward diagonal lines.
5 | Downward diagonal lines.
6 | Crosshatch.
7 | Diagonal crosshatch.

Thus, setting FillStyle to 0 fills the box solidly with the color set for the FillColor property.

Another way to fill the box is to specify F after the B. (Note that F cannot be used without B.) When you use the F option, the Line method ignores FillColor and FillStyle. The box is always filled solid when you use the F option. The following statement fills the box with a solid pattern, using the ForeColor property:

Line (500, 500)–Step(1000, 1000), , BF

The result is shown in Figure 12.15.

Figure 12.15 A box filled with a solid pattern


Drawing Circles

The Circle method draws a variety of circular and elliptical (oval) shapes. In addition, Circle draws arcs (segments of circles) and pie-shaped wedges. You can produce many kinds of curved lines using variations of the Circle method.

To draw a circle, Visual Basic needs the location of a circle’s center and the length of its radius. The syntax for a perfect circle is:

[object.]Circle [Step](x, y), radius[, color]

The brackets indicate that both object and the Step keyword are optional. If you don’t specify object, the current form is assumed. The x and y arguments are the coordinates of the center, and radius is the radius of the circle. For example, this statement draws a circle with a center at (1200, 1000) and radius of 750:

Circle (1200, 1000), 750

The exact effect of this statement depends on the size and coordinate system of the form. Because the size of the form is unknown, you don’t know if the circle will be visible. Using the drawing area’s scale properties puts the center of the circle at the center of the form:

Circle ((ScaleWidth + ScaleLeft) / 2, (ScaleHeight + _
   ScaleTop) / 2), ScaleWidth / 4

For now, all you need to know about ScaleWidth and ScaleHeight is that they help position graphics in the center of a form.

For More Information "Changing an Object’s Coordinate System" earlier in this chapter discusses the ScaleWidth and ScaleHeight properties in detail.

Note The radius of the circle is always specified in terms of horizontal units. If your coordinate system uses the same horizontal and vertical units (which it does by default), you can ignore this fact. However, if you use a custom scale, horizontal and vertical units may correspond to different distances. In the preceding examples, the radius is specified in horizontal units, and the actual height of the circle is guaranteed to be equal to its actual width.

The Blanker application creates circles as part of the Rainbow Rug demo. This demo draws a series of dashed line circles around the center of the form. In time the circles resemble a woven circular rug. The CircleDemo procedure creates the circles in the Rainbow Rug demo with the following statements:

Sub CircleDemo ()
   Dim Radius
' Set Red to a random value.
   R = 255 * Rnd
' Set Green to a random value.
   G = 255 * Rnd
' Set Blue to a random value.
   B = 255 * Rnd
' Set x-coordinate in middle of form.
   XPos = ScaleWidth / 2
' Set y-coordinate in middle of form.
   YPos = ScaleHeight / 2
   ' Set radius between 0 & 50% of form height.
   Radius = ((YPos * 0.9) + 1) * Rnd
   ' Draw the circle using a random color.
   Circle (XPos, YPos), Radius, RGB(R, G, B)
End Sub

The results of the Rainbow Rug demo are shown in Figure 12.16.

Figure 12.16 The Rainbow Rug demo in the Blanker application

Drawing Arcs

To draw arcs with the Circle method, you need to give angle arguments in radians to define the start and the end of the arc. The syntax for drawing an arc is:

[object.]Circle [Step](x, y), radius, [color], start, end[, aspect]

If the start or end argument is negative, Visual Basic draws a line connecting the center of the circle to the negative end point. For example, the following procedure draws a pie with a slice removed.

Private Sub Form_Click ()
   Const PI = 3.14159265
   Circle (3500, 1500), 1000, , –PI / 2, –PI / 3
End Sub

Note The formula for converting from degrees to radians is to multiply degrees by Pi/180.


Drawing Ellipses

The aspect ratio of a circle controls whether or not it appears perfectly round (a circle) or elongated (an ellipse). The complete syntax for the Circle method is:

[object.]Circle [Step](x, y), radius, [color], [start], [end] [, aspect]

The start and end arguments are optional, but the commas are necessary if you want to skip arguments. For example, if you include the radius and aspect arguments, but no color, start, or end argument, you must add four successive commas to indicate that you’re skipping the three arguments:

Circle (1000, 1000), 500, , , , 2

The aspect argument specifies the ratio of the vertical to horizontal dimensions. Here, aspect is a positive floating-point number. This means you can specify integer or fractional expressions, but not negative values. Large values for aspect produce ellipses stretched out along the vertical axis, while small values for aspect produce ellipses stretched out along the horizontal axis. Since an ellipse has two radii — one horizontal x-radius and one vertical y-radius — Visual Basic applies the single argument radius in a Circle statement to the longer axis. If aspect is less than one, radius is the x-radius; if aspect is greater than or equal to one, radius is the y-radius.

Note The aspect argument always specifies the ratio between the vertical and horizontal dimensions in terms of true physical distance. To ensure that this happens (even when you use a custom scale), the radius is specified in terms of horizontal units.

The following procedure illustrates how different aspect values determine whether Circle uses the radius argument as the x-radius or the y-radius of an ellipse:

Private Sub Form_Click ()
' Draw solid ellipse.
   FillStyle = 0
   Circle (600, 1000), 800, , , , 3
' Draw empty ellipse.
   FillStyle = 1
   Circle (1800, 1000), 800, , , , 1 / 3
End Sub

The output is shown in Figure 12.17.

Figure 12.17 Ellipses drawn with the Circle method

For More Information For more information about drawing circles and arcs, see "Drawing Circles" earlier in this chapter.


Painting Graphics at Arbitrary Locations

You can paint graphics at arbitrary locations on a form, on a picture box, and to the Printer object using the PaintPicture method. The syntax for the PaintPicture method is:

[object.]PaintPicture pic, destX, destY[, destWidth[, destHeight[, srcX _

[, srcY[, srcWidth[, srcHeight[, Op]]]]]]]

The destination object is the form, picture box, or Printer object where the pic picture is rendered. If object is omitted, the current form is assumed. The pic argument must be a Picture object, as from the Picture property of a form or control.

The destX and destY arguments are the horizontal and vertical locations where the picture will be rendered in the ScaleMode of object. The destWidth and destHeight arguments are optional and set the width and height with which the picture will be rendered in the destination object.

The srcX and srcY arguments are optional and define the x-coordinate and y-coordinate of the upper-left corner of a clipping region within pic.

The optional Op argument defines a raster operation (such as AND or XOR) that is performed on the picture as it is being painted on the destination object.

The PaintPicture method can be used in place of the BitBlt Windows API function to perform a wide variety of bit operations while moving a rectangular block of graphics from one position to any other position.

For example, you can use the PaintPicture method to create multiple copies of the same bitmap, and tile them on a form. Using this method is faster than moving picture controls on a form. The following code tiles 100 copies of a picture control and flips every picture horizontally by supplying a negative value for destWidth.

For i = 0 To 10
   For j = 0 To 10
      Form1.PaintPicture picF.Picture, j * _
         picF.Width, i * picF.Height, _
         picF.Width, -picF.Height
Next j, i

For More Information See "PaintPicture Method" in the Language Reference.


Specifying Line Width

The DrawWidth property specifies the width of the line for output from the graphics methods. The BorderWidth property specifies the outline thickness of line and shape controls.

The following procedure draws lines of several different widths.

Private Sub Form_Click ()
   DrawWidth = 1
   Line (100, 1000)–(3000, 1000)
   DrawWidth = 5
   Line (100, 1500)–(3000, 1500)
   DrawWidth = 8
   Line (100, 2000)–(3000, 2000)
End Sub

The results are shown in Figure 12.18.

Figure 12.18 The effects of changing the DrawWidth property

Figure 12.19 shows three shape controls with different BorderWidth values.

Figure 12.19 The effects of changing the BorderWidth property


Specifying Solid or Broken Lines

The DrawStyle property specifies whether the lines created with graphics methods are solid or have a broken pattern. The BorderStyle property of a shape control serves the same function as the DrawStyle property, but applies to a variety of objects.

Note The BorderStyle property of a shape control serves a different purpose and uses different settings from the BorderStyle property in other controls and in forms. The BorderStyle property of a shape or line control serves a different purpose and uses different settings from the BorderStyle property on other objects. For shape and line controls, the BorderStyle property works like the DrawStyle property as described in this section. For forms and other controls, the BorderStyle property determines whether the control or form has a border and if so, whether the border is fixed or sizable.

Solid and Inside Solid Styles

The inside solid style (DrawStyle or BorderStyle = 6) is nearly identical to the solid style. They both create a solid line. The difference between these settings becomes apparent when you use a wide line to draw a box or a shape control. In these cases, the solid style draws the line half inside and half outside the box or shape. The inside solid style draws the line entirely inside the box or shape. See "Drawing Boxes, " earlier in this chapter, to see how to draw a box.

The following procedure demonstrates all of the supported settings of the DrawStyle property by creating a loop in which the setting goes from 0 to 6, one step at a time. The results are shown in Figure 12.20.

Private Sub Form_Click ()
   Dim I As Integer, Y As Long
   For I = 0 To 6
      DrawStyle = I
      Y = (200 * I) + 1000
      Line (200, Y)–(2400, Y)
   Next I
End Sub

Figure 12.20 The effects of changing the DrawStyle property

For More Information See "DrawStyle Property" or "BorderStyle Property" in the Language Reference.


Controlling Display Using DrawMode

The DrawMode property determines what happens when you draw one pattern on top of another. Although changing the DrawMode property usually has some effect (especially with color systems), it is often not necessary to use this property when you are drawing on a blank or pure white background, or on a background of undifferentiated color.

You can set DrawMode to a value from 1 to 16. Common settings appear in the following table.

Setting | Description
4 | Not Copy Pen. Draws the inverse of the line pattern, regardless of what is already there.
7 | Xor Pen. Displays the difference between the line pattern and the existing display, as explained later in this section. Drawing an object twice with this mode restores the background precisely as it was.
11 | No operation. In effect, this turns drawing off.
13 | Copy Pen (default). Applies the line’s pattern, regardless of what is already there.

For More Information See "DrawMode Property" in the Language Reference.

The Xor Pen

A DrawMode setting of 7 is useful for animation. Drawing a line twice restores the existing display precisely as it was before the line was drawn. This makes it possible to create one object that "moves over" a background without corrupting it, because you can restore the background as you go. Most modes are not guaranteed to preserve the old background.

For example, the following code moves a circle every time the mouse is clicked. No matter what pattern was underneath the circle, it gets restored.

Private Sub Form_Click ()
   ForeColor = 255 : DrawMode = 7
   Circle (CurrentX, CurrentY), 1000
   CurrentX = CurrentX + 220
   CurrentY = CurrentY + 220
   Circle (CurrentX, CurrentY), 1000
End Sub

The Xor Pen draw mode (and most of the other DrawMode settings) works by comparing each individual pixel in the draw pattern (called the "Pen") and the corresponding pixel in the existing area (called the "Destination"). On monochrome systems, the pixel is turned either on or off, and Visual Basic performs a simple logical comparison: It turns a pixel on if either the Pen or Destination pixel is on, but not if both are on.

In color systems, each pixel is assigned a color value. For DrawMode settings such as Xor Pen, Visual Basic compares each corresponding pair of pixels in the Pen and Destination and performs a binary (bitwise) comparison. The result determines the color value of the resulting pixel, as shown in Figure 12.21.

Figure 12.21 Using the Xor Pen to set the binary value of a pixel in a line


Creating Graphics When a Form Loads

When creating graphics that appear on a form when it loads, consider placing the graphics methods in the Form_Paint event. Form_Paint graphics will get repainted automatically in every paint event. If you place graphics in the Form_Load event, set the AutoRedraw property on the form to True. In this case, Form_Load should show the form, then draw the graphics. Remember, forms are not visible during the Form_Load event. Because Visual Basic does not process graphics methods on a form that is not visible, graphics methods in the Form_Load event are ignored unless AutoRedraw is set to True.


Working with Color

Visual Basic uses a consistent system for all color properties and graphics methods. A color is represented by a Long integer, and this value has the same meaning in all contexts that specify a color.

Specifying Colors at Run Time

There are four ways to specify a color value at run time:

  • Use the RGB function.

  • Use the QBColor function to choose one of 16 Microsoft QuickBasic® colors.

  • Use one of the intrinsic constants listed in the Object Browser.

  • Enter a color value directly.

This section discusses how to use the RGB and QBColor functions as simple ways to specify color. See "Using Color Properties" later in this chapter for information on using constants to define color or directly entering color values.

Using the RGB Function

You can use the RGB function to specify any color.

To use the RGB function to specify a color

  1. Assign each of the three primary colors (red, green, and blue) a number from 0 to 255, with 0 denoting the least intensity and 255 the greatest.

  2. Give these three numbers as input to the RGB function, using the order red-green-blue.

  3. Assign the result to the color property or color argument.

Every visible color can be produced by combining one or more of the three primary colors. For example:

' Set background to green.
Form1.BackColor = RGB(0, 128, 0)
' Set background to yellow.
Form2.BackColor = RGB(255, 255, 0)
' Set point to dark blue.
PSet (100, 100), RGB(0, 0, 64)

For More Information For information on the RGB function, see "RGB Function" in the Language Reference.


Using Color Properties

Many of the controls in Visual Basic have properties that determine the colors used to display the control. Keep in mind that some of these properties also apply to controls that aren't graphical. The following table describes the color properties.

Property | Description
BackColor | Sets the background color of the form or control used for drawing. If you change the BackColor property after using graphics methods to draw, the graphics are erased by the new background color.
ForeColor | Sets the color used by graphics methods to create text or graphics in a form or control. Changing ForeColor does not affect text or graphics already created.
BorderColor | Sets the color of the border of a shape control.
FillColor | Sets the color that fills circles created with the Circle method and boxes created with the Line method.

For More Information For detailed descriptions of these color properties, see "BackColor Property," "ForeColor Property," "BorderColor Property," and "FillColor Property" in the Language Reference.

Defining Colors

The color properties can use any of several methods to define the color value. The RGB function described in "Working with Color" is one way to define colors. This section discusses two more ways to define colors:

  • Using defined constants

  • Using direct color settings

Using Defined Constants

You don’t need to understand how color values are generated if you use the intrinsic constants listed in the Object Browser. In addition, intrinsic constants do not need to be declared. For example, you can use the constant vbRed whenever you want to specify red as a color argument or color property setting:

BackColor = vbRed

Using Direct Color Settings

Using the RGB function or the intrinsic constants to define color are indirect methods. They are indirect because Visual Basic interprets them into the single approach it uses to represent color. If you understand how colors are represented in Visual Basic, you can assign numbers to color properties and arguments that specify color directly. In most cases, it’s much easier to enter these numbers in hexadecimal.

The valid range for a normal RGB color is 0 to 16,777,215 (&HFFFFFF&). Each color setting (property or argument) is a 4-byte integer. The high byte of a number in this range equals 0. The lower 3 bytes, from least to most significant byte, determine the amount of red, green, and blue, respectively. The red, green, and blue components are each represented by a number between 0 and 255 (&HFF).

Consequently, you can specify a color as a hexadecimal number using this syntax:

&HBBGGRR&

The BB specifies the amount of blue, GG the amount of green, and RR the amount of red. Each of these fragments is a two-digit hexadecimal number from 00 to FF. The median value is 80. Thus, the following number specifies gray, which has the median amount of all three colors:

&H808080&

Setting the most significant bit to 1 changes the meaning of the color value: It no longer represents an RGB color, but an environment-wide color specified through the Windows Control Panel. The values that correspond to these system-wide colors range from &H80000000 to &H80000015.

Note Although you can specify over 16 million different colors, not all systems are capable of displaying them accurately. For more information on how Windows represents colors, see "Working with 256 Colors" later in this chapter.

Using System Colors

When setting the colors of controls or forms in your application, you can use colors specified by the operating system instead of specific color values. If you specify system colors, when users of your application change the values of system colors on their computers, your application automatically reflects the user-specified color values.

Each system color has both a defined constant and a direct color setting. The high byte of direct color settings for system colors differs from those of normal RGB colors. For RGB colors, the high byte equals 0 whereas for system colors the high byte equals 8. The rest of the number refers to a particular system color. For example, the hexadecimal number used to represent the color of an active window caption is &H80000002&.

When you select color properties at design time with the Properties window, selecting the System tab lets you choose system settings, which are automatically converted into the hexadecimal value. You can also find the defined constants for system colors in the Object Browser.


Working with 256 Colors

Visual Basic supports 256 colors on systems with video adapters and display drivers that handle 256 or more colors. The ability to display 256 simultaneous colors is particularly valuable in multimedia applications or applications that need to display near –photographic-quality images.

You can display 256-color images and define up to 256 colors for graphics methods in:

  • Forms

  • Picture boxes

  • Image controls (display images only)

Note Support for 256 colors does not apply to Windows metafiles. Visual Basic displays metafiles using the default palette of 16 VGA colors.

Color Palettes

Color palettes provide the basis for 256-color support in Visual Basic applications. In discussing palettes, it’s important to understand the relationship between different palette types. The hardware palette contains 256 entries defining the actual RGB values that will be displayed on screen. The system halftone palette is a predefined set of 256 RGB values made available by Windows itself. A logical palette is a set of up to 256 RGB values contained within a bitmap or other image.

Windows can draw using the 256 colors in the hardware palette. Twenty of these 256 colors, called static colors, are reserved by the system and cannot be changed by an application. Static colors include the 16 colors in the default VGA palette (the same as the colors defined by Visual Basic’s QBColor function), plus four additional shades of gray. The system halftone palette always contains these static colors.

The foreground window (the window with focus) determines the 236 nonstatic colors in the hardware palette. Each time the hardware palette is changed, all background windows are redrawn using these colors. If the colors in a background window’s logical palette don’t perfectly match those currently in the hardware palette, Windows will assign the closest match.

Displaying 256-Color Images

Forms, picture boxes, and image controls automatically display images in 256 colors if the user’s display hardware and software can support that many colors on screen. If the user’s system supports fewer colors than the image, then Visual Basic will map all colors to the closest available.

On true-color (16-million color) displays, Visual Basic always uses the correct color. On monochrome or 16-color displays, Visual Basic will dither background colors and colors set with the FillColor property. Dithering is a process used to simulate colors not available from the video adapter and display driver.

Drawing with Color Palettes

With 256-color video drivers, you can use up to 256 colors with graphics methods. By default, the 256 colors available in Visual Basic are those in the system halftone palette. Although you can specify an exact color using the RGB function, the actual color displayed will be the closest match from the halftone palette, as shown in Figure 12.22.

Figure 12.22 Color matching from a specified color to the display

Although the default palette for Visual Basic is the system halftone palette, you can also control the display of colors with the PaletteMode and Palette properties of forms, user controls, and user documents. In this case, the color match is much the same, except that colors will be matched to the closest color in the hardware palette.

For More Information To learn more about the Palette and PaletteMode properties, see "Managing Multiple Color Palettes" later in this chapter.


Managing Multiple Color Palettes

When you work with color palettes, keep in mind that many displays can display only 256 colors simultaneously on the screen.

This limitation becomes important when you use more than one color palette in your application. For example, on a single form, you might display a 256-color bitmap in an image control while displaying a second image in a picture box. If the logical palettes of these two images don’t contain exactly the same 256 colors, Windows must decide which logical palette places its colors in the hardware palette first. Remember: The hardware palette determines what actually appears on the screen.

A similar situation occurs when your Visual Basic application has two or more forms with differing logical palettes. As each form receives focus, its logical palette controls the hardware palette. This can often result in a less than optimal display on 256-color systems. As a Visual Basic programmer, you can control the hardware palette by using the PaletteMode property.

The PaletteMode Property

When designing applications that may run on 256-color systems, you can control the way that Windows chooses the display colors by setting the PaletteMode property of a form, user control, or user document. (User controls and user documents are only available in the Professional and Enterprise editions.) All controls contained on the form, user control, or user document will be displayed based on the PaletteMode. The following table shows the available PaletteMode settings:

Mode | Constant | Applies to
Halftone | vbPaletteModeHalftone | Forms, User Controls, User Documents
UseZOrder | vbPaletteModeUseZOrder | Forms, User Controls, User Documents
Custom | vbPaletteModeCustom | Forms, User Controls, User Documents
Container | vbPaletteModeContainer | User Controls
None | vbPaletteModeNone | User Controls
Object | vbPaletteModeObject | ActiveX designers that contain a palette

The PaletteMode property only applies to 256-color displays. On high-color or true-color displays, color selection is handled by the video driver using a palette of 32,000 or 16 million colors respectively. Even if you’re programming on a system with a high-color or true-color display, you still may want to set the PaletteMode, because many of your users may be using 256-color displays.

The PaletteMode property can be set at design time through the Properties window, or changed at run time via code. The Palettes sample application demonstrates the effects of displaying images with different palettes using several different PaletteMode settings.

Note For previous versions of Visual Basic, PaletteMode corresponded to UseZOrder.

Halftone PaletteMode

The default mode for forms and user documents is Halftone. In this mode, any controls, images contained on the form, or graphics methods draw using the system halftone palette.

Halftone mode is a good choice in most cases because it provides a compromise between the images in your form, and colors used in other forms or images. It may, however, result in a degradation of quality for some images. For example, an image with a palette containing 256 shades of gray may lose detail or display unexpected traces of other colors.

UseZOrder PaletteMode

Z-order is a relative ordering that determines how controls overlap each other on a form. When the PaletteMode of the form with the focus is set to UseZOrder, the palette of the topmost control always has precedence. This means that each time a new control becomes topmost (for instance, when you load a new image into a picture box), the hardware palette will be remapped. This will often cause a side effect known as palette flash: The display appears to flash as the new colors are displayed, both in the current form and in any other visible forms or applications.

Although the UseZOrder setting provides the most accurate color rendition, it comes at the expense of speed. Additionally, this method can cause the background color of the form or of controls that have no image to appear dithered. Setting the PaletteMode to UseZOrder is the best choice when accurate display of the topmost image outweighs the annoyance of palette flash, or when you need to maintain backward compatibility with earlier versions of Visual Basic.

Custom PaletteMode

If you need more precise control over the actual display of colors, you can use a 256-color image to define a custom palette. To do this, assign a 256-color image (.bmp, .cur, .ico, .dib, or .gif) to the Palette property of the form and set the PaletteMode property to Custom. The bitmap doesn’t have to be very large; even a single pixel can define up to 256 colors for the form or picture box. This is because the logical palette of a bitmap can list up to 256 colors, regardless of whether all those colors appear in the bitmap.

As with the default method, colors that you define using the RGB function must also exist in the bitmap. If the color doesn’t match, it will be mapped to the closest match in the logical palette of the bitmap assigned to the Palette property.

To set the Custom PaletteMode at run time, add the following code to the Form_Load event (assuming that the image containing your chosen palette has been assigned to a Image control named Image1):

' Assign the palette from Image1 to the form.
Form1.Palette = Image1.Picture
' Use the Custom mode.
Form1.PaletteMode = vbPaletteModeCustom

Alternatively, you can use the Picture object to achieve the same effect without the extra Image control:

Dim objPic As Picture
Set objPic = LoadPicture(App.Path & "\Pastel.bmp")
' Assign picture object's palette to the form.
Form1.Palette = objPic
' Use the Custom mode.
Form1.PaletteMode = vbPaletteModeCustom

The Custom PaletteMode is your best choice when you want to maintain a uniform palette throughout your application.

Note Using the Custom PaletteMode can also improve the performance of your application in cases where you aren’t using any 256-color graphics. If you set the PaletteMode of a form to Custom and leave the Palette property blank, your form will load faster because no palette matching will occur.

For More Information To learn more about the Picture object, see "Using the Picture Object" later in this chapter.

Other Palette Modes

Two additional PaletteMode settings are available when creating user controls: Container and None. The Container mode maps the palette of the user control and any contained controls to the ambient palette of the container (form or user document) at run time. If the container doesn’t supply an ambient palette, the Halftone mode will be invoked. Because you may not know in advance where your user control may be deployed, this mode can prevent your control from conflicting with other palette handling methods.

The None mode does just what you might expect: It eliminates palette handling altogether. When creating a user control that doesn’t display images or graphics, setting PaletteMode to None improves performance by eliminating the added overhead of handling palette messages.

Using Relative Palette Colors

When designing for 256 color displays, some colors may appear dithered. This can make text and other graphic elements difficult to read. By specifying a relative palette color, Visual Basic will display the closest undithered approximation of a specified color on 256 color displays while still displaying the exact color at higher color depths.

To force Visual Basic to use the closest solid, rather than dithered, color for a given property, put a 2 in the high order byte of the color property. For example, to force a form's background to be a solid light orange you could use the following code:

Private Function PaletteRGB(RGB As Long) As Long
 PaletteRGB = &H02000000 Or RGB
End Function

If you set this at design time:

Form1.BackColor = &H00C0E0FF&    'dithered light orange

And add the following to the Form_Click event:

Private Sub Form_Click()
   Form1.BackColor = PaletteRGB(Form1.BackColor)
End Sub

At run-time when the form is clicked the backcolor will change to a solid, rather than dithered, shade. It is now using the closest color out of the halftone palette. This effect may not be visible on systems running at a color depth greater than 256 colors.


Using the Picture Object

The Picture object is similar in some respects to the Printer object — you can’t see it, but it’s useful nonetheless. You could think of the Picture object as a invisible picture box that you can use as a staging area for images. For example, the following code loads a Picture object with a bitmap and uses that bitmap to set the Picture property of a picture box control:

Private Sub Command1_Click()
   Dim objPic As Picture
   Set objPic = LoadPicture("Butterfly.bmp")
   Set Picture1.Picture = objPic
End Sub

The Picture object supports bitmaps, GIF images, JPEG images, metafiles, and icons.

Using Arrays of Picture Objects

You can also use an array of Picture objects to keep a series of graphics in memory without using a form that contains multiple picture box or image controls. This is convenient for creating animation sequences or other applications where rapid image changes are required. Declare the array at the module level:

Dim objPics(1) As Picture

Add the following code to the Form_Load event:

' Load bitmaps int the Picture object array.
Set objPics(0) = LoadPicture("Butterfly1.bmp")
Set objPics(1) = LoadPicture("Butterfly2.bmp")

Then in Timer event you can cycle the images:

Static intCount As Integer
If intCount = 0 Then
   intCount = 1
Else
   intCount = 0
End If
' Use the PaintPicture method to display the bitmaps
'   on the form.
PaintPicture objPics(intCount), 0, 0

By adding a loop to increment the x and y coordinates, you could easily make the butterfly bitmaps "fly" across the form.

Using the Picture Object Instead of the Windows API

There are lots of things you can do with bitmaps, icons, or metafiles in the Windows API, but the Picture object already does most of them for you. This means that you are better off using the Picture object instead of the Windows API whenever possible. The Picture object also allows you to use .jpg and .gif files, whereas the Windows API does not.

There is no direct relationship between a Picture.Handle and a PictureBox.hDC. The hDC property of the picture box is the handle provided by the operating system to the device context of the picture box control. The Handle property of the Picture object is actually the handle of the GDI object that is contained in the Picture object.

There are now two completely different ways to paint graphics on a window (or blit). You can use BitBlt or StretchBlt on the hDC of an object, or you can use the PaintPicture method on the Picture object or property. If you have an Image control, you can only use PaintPicture because Image controls do not have an hDC.

For More Information For more information about the Windows API, see "Accessing DLLs and the Windows API" in the Component Tools Guide, available in the Professional and Enterprise editions.


Printing

Printing is one of the most complex tasks a Windows – based application performs. Good results depend on all parts of the process working together. Poor results can arise from problems in your application, variations in printer drivers, or limited printer capabilities. Although it is a good idea to test your application with commonly used printers and printer drivers, you can’t test all the possible combinations users may have.

Printing from your application involves these three components:

  • The code in your application that starts the printing process.

  • The printer drivers installed on both your system and the systems of users of your application.

  • The capabilities of the printers available to users of your application.

The code in your application determines the type and quality of print output available from your application. But the users’ printer drivers and printers also impact print quality. This section deals with enabling printing from a Visual Basic application. For information on printing from the Visual Basic development environment, see "Printing Information in the Immediate Window" in "Debugging Your Code and Handling Errors. "


Printing from an Application

Visual Basic provides three techniques for printing text and graphics.

  • You can produce the output you want on a form and then print the form using the PrintForm method.

  • You can send text and graphics to a printer by setting the default printer to a member of the Printers collection.

  • You can send text and graphics to the Printer object and then print them using the NewPage and EndDoc methods.

This section examines the advantages and disadvantages of these three approaches.

Using the PrintForm Method

The PrintForm method sends an image of the specified form to the printer. To print information from your application with PrintForm, you must first display that information on a form and then print that form with the PrintForm method. The syntax is as follows:

[form.]PrintForm

If you omit the form name, Visual Basic prints the current form. PrintForm prints the entire form, even if part of the form is not visible on the screen. If a form contains graphics, however, the graphics print only if the form’s AutoRedraw property is set to True. When printing is complete, PrintForm calls the EndDoc method to clear the printer.

For example, you could send text to a printer by printing it on a form and then calling PrintForm with the following statements:

Print "Here is some text."
PrintForm

The PrintForm method is by far the easiest way to print from your application. Because it may send information to the printer at the resolution of the user’s screen (typically 96 dots per inch), results can be disappointing on printers with much higher resolutions (typically 300 dots per inch for laser printers). The results may vary depending on objects on your form.

For More Information See "PrintForm Method" in the Language Reference.

Using the Printers Collection

The Printers collection is an object that contains all the printers that are available on the operating system. The list of Printers are the same as those available in the Print Setup dialog box or the Windows Control Panel. Each printer in the collection has a unique index for identification. Starting with 0, each printer in the collection can be referenced by its number.

Regardless of which printing method you use, all printed output from a Visual Basic application is directed to the Printer object, which initially represents the default printer specified in the Windows Control Panel. However, you can set the default printer to any one member in the Printers collection.

To select the printer from the collection, use the following syntax:

Set Printer = Printers(n)

The following statements print the device names of all the printers on the operating system to the Immediate window:

Private Sub Command1_Click()
Dim x As Printer
   For Each x In Printers
      Debug.Print x.DeviceName
   Next
End Sub

Note You cannot create new instances of the Printer object in code, and you cannot directly add or remove printers from the Printers collection. To add or remove printers on your system, use the Windows Control Panel.

Using the Printer Object

The Printer object is a device-independent drawing space that supports the Print, PSet, Line, PaintPicture, and Circle methods to create text and graphics. You use these methods on the Printer object just as you would on a form or picture box. The Printer object also has all the font properties described earlier in this chapter. When you finish placing the information on the Printer object, you use the EndDoc method to send the output to the printer. When applications close, they automatically use the EndDoc method to send any pending information on the Printer object.

The Printer object provides the best print quality across a variety of printers because Windows translates text and graphics from the device-independent drawing space of the Printer object to best match the resolution and abilities of the printer. You can also print multiple-page documents by using the NewPage method on the Printer object.

The main drawback to using the Printer object is the amount of code required to get the best results. Printing bitmaps on the Printer object also takes time and can therefore slow the performance of the application.


Printing with the Printer Object

There are several ways to place text and graphics on the Printer object. To print with the Printer object, do any of the following:

  • Assign the specific member of the Printers collection to the Printer object if you want to print to a printer other than the default printer.

  • Put text and graphics on the Printer object.

  • Print the contents of the Printer object with the NewPage or EndDoc method.

Printer Object Properties

The properties of the Printer object initially match those of the default printer set in the Windows Control Panel. At run time, you can set any of the Printer object properties, which include: PaperSize, Height, Width, Orientation, ColorMode, Duplex, TrackDefault, Zoom, DriverName, DeviceName, Port, Copies, PaperBin, and PrintQuality. For more details and syntax for these methods, see the Language Reference.

If the TrackDefault property is True and you change the default printer in the Windows Control Panel, the Printer object property values will reflect the properties of the new default printer.

You cannot change some properties in the middle of a page once a property has been set. Changes to these properties will only affect subsequent pages. The following statements show how you can print each page using a different print quality:

For pageno = 1 To 4
   Printer.PrintQuality = -1 * pageno
   Printer.Print "The quality of this page is"; pageno
   Printer.NewPage
Next

Print quality values can range from – 4 to – 1, or a positive integer corresponding to the print resolution in dots per inch (DPI). For example, the following code would set the printer’s resolution to 300 DPI:

Printer.PrintQuality = 300

For More Information For information on the Printer object properties, see the appropriate property in the Language Reference.

Note The effect of Printer property values depends on the driver supplied by the printer manufacturer. Some property settings may have no effect, or several different property settings may all have the same effect. Settings outside the accepted range may or may not produce an error. For more information on specific drivers, see the manufacturer’s documentation.

Scale Properties

The Printer object has these scale properties:

  • ScaleMode

  • ScaleLeft and ScaleTop

  • ScaleWidth and ScaleHeight

  • Zoom

The ScaleLeft and ScaleTop properties define the x- and y-coordinates, respectively, of the upper-left corner of a printable page. By changing the values of ScaleLeft and ScaleTop, you can create left and top margins on the printed page. For example, you can use ScaleLeft and ScaleTop to center a printed form (PFrm) on the page using these statements:

Printer.ScaleLeft = -((Printer.Width - PFrm.Width) / 2)
Printer.ScaleTop = -((Printer.Height - PFrm.Height) _
   / 2)

Many printers support the Zoom property. This property defines the percentage by which output is scaled. The default value of the Zoom property is 100, indicating that output will be printed at 100 percent of its size (actual size). You can use the Zoom property to make the page you print smaller or larger than the actual paper page. For example, setting Zoom to 50 makes your printed page appear half as wide and half as long as the paper page. The following syntax sets the Zoom property to half the size of the default Printer object:

Printer.Zoom = 50

Positioning Text and Graphics

You can set CurrentX and CurrentY properties for the Printer object, just as you can for forms and picture boxes. With the Printer object, these properties determine where to position output on the current page. The following statements set drawing coordinates to the upper-left corner of the current page:

Printer.CurrentX = 0
Printer.CurrentY = 0

You can also use the TextHeight and TextWidth methods to position text on the Printer object. For more information on using these text methods, see "Displaying Print Output at a Specific Location" earlier in this chapter.

Printing Forms on the Printer Object

You may want your application to print one or more forms along with information on those forms, especially if the design of the form corresponds to a printed document like an invoice or a time card. For the easiest way to do this, use the PrintForm method. For the best quality on a laser printer use the Print and graphics methods with the Printer object. Keep in mind that using the Printer object takes more planning, because you must recreate the form on the Printer object before you print.

Recreating a form on the Printer object may also require recreating:

  • The outline of the form, including title and menu bars.

  • The controls and their contents, including text and graphics.

  • The output of graphics methods applied directly to the form, including the Print method.

The extent to which you recreate these elements on the Printer object depends on your application and how much of the form you need to print.

Recreating Text and Graphics on a Form

When creating text and graphics on a form using the Print, Line, Circle, PaintPicture, or PSet methods, you may also want a copy of this output to appear on the Printer object. The easiest way to accomplish this is to write a device-independent procedure to recreate the text and graphics.

For example, the following procedure uses the PaintPicture method to print a form or control’s Picture property to any output object, such as a printer or another form:

Sub PrintAnywhere (Src As Object, Dest As Object)
   Dest.PaintPicture Src.Picture, Dest.Width / 2, _
      Dest.Height / 2
   If Dest Is Printer Then
      Printer.EndDoc
   End If
End Sub

You then call this procedure and pass it the source and destination objects:

PrintAnywhere MyForm, Printer
PrintAnywhere MyForm, YourForm

For More Information For more information, see "Print Method," "Line Method," "Circle Method," "Pset Method," or "PaintPicture Method" in the Language Reference.

Printing Controls on a Form

The Printer object can receive the output of the Print method and the graphics methods (such as the Line or PSet method). But you cannot place controls directly on the Printer object. If your application needs to print controls, you must either write procedures that redraw each type of control you use on the Printer object, or use the PrintForm method.

Printing the Contents of the Printer Object

Once you have placed text and graphics on the Printer object, use the EndDoc method to print the contents. The EndDoc method advances the page and sends all pending output to the spooler. A spooler intercepts a print job on its way to the printer and sends it to disk or memory, where the print job is held until the printer is ready for it. For example:

Printer.Print "This is the first line of text in _
   a pair."
Printer.Print "This is the second line of text in _
   a pair."
Printer.EndDoc

Note Visual Basic automatically calls EndDoc if your application ends without explicitly calling it.

Creating Multiple-Page Documents

When printing longer documents, you can specify in code where you want a new page to begin by using the NewPage method. For example:

Printer.Print "This is page 1."
Printer.NewPage
Printer.Print "This is page 2."
Printer.EndDoc

Canceling a Print Job

You can terminate the current print job by using the KillDoc method. For example, you can query the user with a dialog box to determine whether to print or terminate a document:

Sub PrintOrNot()
   Printer.Print "This is the first line to _
      illustrate KillDoc method"
   Printer.Print "This is the second line to _
      illustrate KillDoc method"
   Printer.Print "This is the third line to _
      illustrate KillDoc method"
   If vbNo = MsgBox("Print this fine document?", _
         vbYesNo) Then
      Printer.KillDoc
   Else
      Printer.EndDoc
   End If
End Sub

If the operating system’s Print Manager is handling the print job, the KillDoc method deletes the entire job you sent to the printer. However, if the Print Manager is not controlling the print job, page one may have already been sent to the printer, and will be unaffected by KillDoc. The amount of data sent to the printer varies slightly among printer drivers.

Note You cannot use the KillDoc method to terminate a print job that was initiated with the PrintForm method.


Trapping Printer Errors

Trappable run-time errors may occur while printing. The following table lists some examples that may be reported:

Error number | Error message
396 | Property cannot be set within a page.
 | This error occurs when the same property is set differently on the same page.
482 | Printer Error.
 | Visual Basic reports the error whenever the printer driver returns an error code.
483 | Printer driver does not support the property.
 | This error occurs when attempting to use a property that is not supported by the current printer driver.
484 | Printer driver unavailable.
 | This error occurs when the WIN.INI printer information is missing or insufficient.

Note Printer errors do not always occur immediately. If a statement causes a printer error, the error may not be raised until execution of the next statement that addresses that printer.

For More Information For a detailed discussion on run-time errors, see "Debugging Your Code and Handling Errors. "


Debugging Your Code and Handling Errors

No matter how carefully crafted your code, errors can (and probably will) occur. Ideally, Visual Basic procedures wouldn't need error-handling code at all. Unfortunately, sometimes files are mistakenly deleted, disk drives run out of space, or network drives disconnect unexpectedly. Such possibilities can cause run-time errors in your code. To handle these errors, you need to add error-handling code to your procedures.

Sometimes errors can also occur within your code; this type of error is commonly referred to as a bug. Minor bugs — for example, a cursor that doesn't behave as expected — can be frustrating or inconvenient. More severe bugs can cause an application to stop responding to commands, possibly requiring the user to restart the application, losing whatever work hasn't been saved.

The process of locating and fixing bugs in your application is known as debugging. Visual Basic provides several tools to help analyze how your application operates. These debugging tools are particularly useful in locating the source of bugs, but you can also use the tools to experiment with changes to your application or to learn how other applications work.

This chapter shows how to use the debugging tools included in Visual Basic and explains how to handle run-time errors — errors that occur while your code is running and that result from attempts to complete an invalid operation.

Topics

How to Handle Errors

An overview of error handling in Visual Basic.
Designing an Error Handler

An introduction to error trapping procedures.
Error Handling Hierarchy

A discussion of the sequence of events when an error occurs.
Testing Error Handling by Generating Errors

Methods for creating intentional errors to test your code.
Inline Error Handling

Adding error handling within a procedure.
Centralized Error Handling

Adding a error handling sub procedure to your application.
Turning Off Error Handling

Methods for disabling error handling.
Error Handling with ActiveX Components

Considerations for handling errors returned by other objects.
Approaches to Debugging

An introduction to debugging.
Avoiding Bugs

Tips for error-free coding.
Design Time, Run Time, and Break Mode

A discussion of Visual Basic's modes.
Using the Debugging Windows

Introducing the debugging tools in Visual Basic.
Using Break Mode

Testing your code's execution with breakpoints.
Running Selected Portions of Your Application

Debugging selected code segments.
Monitoring the Call Stack

Tracing your application's execution with the Call Stack.
Testing Data and Procedures with the Immediate Window

Using the Immediate window for interactive debugging.
Special Debugging Considerations

A discussion of events that can affect debugging.
Tips for Debugging

Some ideas to help simplify debugging.

Sample application

Errors.vbp

Many of the code samples in this chapter are taken from the Errors.vbp sample application. You'll find this application listed in the Samples directory.


How to Handle Errors

Ideally, Visual Basic procedures wouldn't need error-handling code at all. Reality dictates that hardware problems or unanticipated actions by the user can cause run-time errors that halt your code, and there's usually nothing the user can do to resume running the application. Other errors might not interrupt code, but they can cause it to act unpredictably.

For example, the following procedure returns true if the specified file exists and false if it does not, but doesn't contain error-handling code:

Function FileExists (filename) As Boolean
   FileExists = (Dir(filename) <> "")
End Function

The Dir function returns the first file matching the specified file name (given with or without wildcard characters, drive name, or path); it returns a zero-length string if no matching file is found.

The code appears to cover either of the possible outcomes of the Dir call. However, if the drive letter specified in the argument is not a valid drive, the error "Device unavailable" occurs. If the specified drive is a floppy disk drive, this function will work correctly only if a disk is in the drive and the drive door is closed. If not, Visual Basic presents the error "Disk not ready" and halts execution of your code.

To avoid this situation, you can use the error-handling features in Visual Basic to intercept errors and take corrective action. (Intercepting an error is also known as trapping an error.) When an error occurs, Visual Basic sets the various properties of the error object, Err, such as an error number, a description, and so on. You can use the Err object and its properties in an error-handling routine so that your application can respond intelligently to an error situation.

For example, device problems, such as an invalid drive or an empty floppy disk drive, could be handled by the following code:

Function FileExists (filename) As Boolean
   Dim Msg As String
   ' Turn on error trapping so error handler responds
   ' if any error is detected.
   On Error GoTo CheckError
      FileExists = (Dir(filename) <> "")
      ' Avoid executing error handler if no error
      ' occurs.
      Exit Function

CheckError:         ' Branch here if error occurs.
   ' Define constants to represent intrinsic Visual
   ' Basic error codes.
   Const mnErrDiskNotReady = 71, _
   mnErrDeviceUnavailable = 68
   ' vbExclamation, vbOK, vbCancel, vbCritical, and
   ' vbOKCancel are constants defined in the VBA type
   ' library.
   If (Err.Number = MnErrDiskNotReady) Then
      Msg = "Put a floppy disk in the drive "
      Msg = Msg & "and close the door."
      ' Display message box with an exclamation mark
      ' icon and with OK and Cancel buttons.
      If MsgBox(Msg, vbExclamation & vbOKCancel) = _
      vbOK Then
         Resume
      Else
         Resume Next
      End If
   ElseIf Err.Number = MnErrDeviceUnavailable Then
      Msg = "This drive or path does not exist: "
      Msg = Msg & filename
      MsgBox Msg, vbExclamation
      Resume Next
   Else
      Msg = "Unexpected error #" & Str(Err.Number)
      Msg = Msg & " occurred: " & Err.Description
      ' Display message box with Stop sign icon and
      ' OK button.
      MsgBox Msg, vbCritical
      Stop
   End If
   Resume
End Function

In this code, the Err object's Number property contains the number associated with the run-time error that occurred; the Description property contains a short description of the error.

When Visual Basic generates the error "Disk not ready," this code presents a message telling the user to choose one of two buttons — OK or Cancel. If the user chooses OK, the Resume statement returns control to the statement at which the error occurred and attempts to re-execute that statement. This succeeds if the user has corrected the problem; otherwise, the program returns to the error handler.

If the user chooses Cancel, the Resume Next statement returns control to the statement following the one at which the error occurred (in this case, the Exit Function statement).

Should the error "Device unavailable" occur, this code presents a message describing the problem. The Resume Next statement then causes the function to continue execution at the statement following the one at which the error occurred.

If an unanticipated error occurs, a short description of the error is displayed and the code halts at the Stop statement.

The application you create can correct an error or prompt the user to change the conditions that caused the error. To do this, use techniques such as those shown in the preceding example. The next section discusses these techniques in detail.

For More Information See "Guidelines for Complex Error Handling" in "Error-Handling Hierarchy" later in this chapter for an explanation of how to use the Stop statement.


Designing an Error Handler

An error handler is a routine for trapping and responding to errors in your application. You'll want to add error handlers to any procedure where you anticipate the possibility of an error (you should assume that any Basic statement can produce an error unless you explicitly know otherwise). The process of designing an error handler involves three steps:

  1. Set, or enable, an error trap by telling the application where to branch to (which error-handling routine to execute) when an error occurs.

    The On Error statement enables the trap and directs the application to the label marking the beginning of the error-handling routine.

    In the Errors.vpb sample application, the FileExists function contains an error-handling routine named CheckError.

  2. Write an error-handling routine that responds to all errors you can anticipate. If control actually branches into the trap at some point, the trap is then said to be active.

    The CheckError routine handles the error using an If...Then...Else statement that responds to the value in the Err object's Number property, which is a numeric code corresponding to a Visual Basic error. In the example, if "Disk not ready" is generated, a message prompts the user to close the drive door. A different message is displayed if the "Device unavailable" error occurs. If any other error is generated, the appropriate description is displayed and the program stops.

  3. Exit the error-handling routine.

    In the case of the "Disk not ready" error, the Resume statement makes the code branch back to the statement where the error occurred. Visual Basic then tries to re-execute that statement. If the situation has not changed, then another error occurs and execution branches back to the error-handling routine.

    In the case of the "Device unavailable" error, the Resume Next statement makes the code branch to the statement following the one at which the error occurred.

Details on how to perform these steps are provided in the remainder of this topic. Refer to the FileExists function example as you read through these steps.

Setting the Error Trap

An error trap is enabled when Visual Basic executes the On Error statement, which specifies an error handler. The error trap remains enabled while the procedure containing it is active — that is, until an Exit Sub, Exit Function, Exit Property, End Sub, End Function, or End Property statement is executed for that procedure. While only one error trap can be enabled at any one time in any given procedure, you can create several alternative error traps and enable different ones at different times. You can also disable an error trap by using a special case of the On Error statement — On Error GoTo 0.

To set an error trap that jumps to an error-handling routine, use a On Error GoTo line statement, where line indicates the label identifying the error-handling code. In the FileExists function example, the label is CheckError. (Although the colon is part of the label, it isn't used in the On Error GoTo line statement.)

For More Information For more information about disabling error handling, see the topic, "Turning Off Error Handling," later in this chapter.

Writing an Error-Handling Routine

The first step in writing an error-handling routine is adding a line label to mark the beginning of the error handling routine. The line label should have a descriptive name and must be followed by a colon. A common convention is to place the error-handling code at the end of the procedure with an Exit Sub, Exit Function, or Exit Property statement immediately before the line label. This allows the procedure to avoid executing the error-handling code if no error occurs.

The body of the error handling routine contains the code that actually handles the error, usually in the form of a Case or If…Then…Else statement. You need to determine which errors are likely to occur and provide a course of action for each, for example, prompting the user to insert a disk in the case of a "Disk not ready" error. An option should always be provided to handle any unanticipated errors by using the Else or Case Else clause — in the case of the FileExists function example, this option warns the user then ends the application.

The Number property of the Err object contains a numeric code representing the most recent run-time error. By using the Err object in combination with the Select Case or If...Then...Else statement, you can take specific action for any error that occurs.

Note The string contained in the Err object's Description property explains the error associated with the current error number. The exact wording of the description may vary among different versions of Microsoft Visual Basic. Therefore, use Err.Number, rather than Err.Description, to identify the specific error that occurred.

Exiting an Error-Handling Routine

The FileExists function example uses the Resume statement within the error handler to re-execute the statement that originally caused the error, and uses the Resume Next statement to return execution to the statement following the one at which the error occurred. There are other ways to exit an error-handling routine. Depending on the circumstances, you can do this using any of the statements shown in the following table.

Statement | Description
Resume [0] | Program execution resumes with the statement that caused the error or the most recently executed call out of the procedure containing the error-handling routine. Use it to repeat an operation after correcting the condition that caused the error.
Resume Next | Resumes program execution at the statement immediately following the one that caused the error. If the error occurred outside the procedure that contains the error handler, execution resumes at the statement immediately following the call to the procedure wherein the error occurred, if the called procedure does not have an enabled error handler.
Resume line | Resumes program execution at the label specified by line, where line is a line label (or nonzero line number) that must be in the same procedure as the error handler.
Err.Raise Number:= number | Triggers a run-time error. When this statement is executed within the error-handling routine, Visual Basic searches the calls list for another error-handling routine. (The calls list is the chain of procedures invoked to arrive at the current point of execution. See the section, "Error-Handling Hierarchy," later in this chapter.)

The Difference Between Resume and Resume Next Statements

The difference between Resume and Resume Next is shown in Figure 13.1.

Figure 13.1 Program flow with Resume and Resume Next

Generally, you would use Resume whenever the error handler can correct the error, and Resume Next when the error handler cannot. You can write an error handler so that the existence of a run-time error is never revealed to the user or to display error messages and allow the user to enter corrections.

For example, the Function procedure in the following code example uses error handling to perform "safe" division on its arguments without revealing errors that might occur. The errors that can occur when performing division are:

Error | Cause
"Division by zero" | Numerator is nonzero, but the denominator is zero.
"Overflow" | Both numerator and denominator are zero (during floating-point division).
"Illegal procedure call" | Either the numerator or the denominator is a nonnumeric value (or can't be considered a numeric value).

In all three cases, the following Function procedure traps these errors and returns Null:

Function Divide (numer, denom) as Variant
   Dim Msg as String
   Const mnErrDivByZero = 11, mnErrOverFlow = 6
   Const mnErrBadCall = 5
   On Error GoTo MathHandler
      Divide    = numer / denom
      Exit Function
MathHandler:
   If Err.Number = MnErrDivByZero Or _
   Err.Number = ErrOverFlow _
   Or Err = ErrBadCall Then
      Divide = Null   ' If error was Division by
                     ' zero, Overflow, or Illegal
                     ' procedure call, return Null.
   Else
      ' Display unanticipated error message.
      Msg = "Unanticipated error " & Err.Number
      Msg = Msg & ": " & Err.Description
      MsgBox Msg, vbExclamation
   End If         ' In all cases, Resume Next
                  ' continues execution at
   Resume Next      ' the Exit Function statement.
End Function

Resuming Execution at a Specified Line

Resume Next can also be used where an error occurs within a loop, and you need to restart the operation. Or, you can use Resume line, which returns control to a specified line label.

The following example illustrates the use of the Resume line statement. A variation on the FileExists example shown earlier, this function allows the user to enter a file specification that the function returns if the file exists.

Function VerifyFile As String
   Const mnErrBadFileName = 52, _
   mnErrDriveDoorOpen = 71
   Const mnErrDeviceUnavailable = 68, _
   mnErrInvalidFileName = 64
   Dim strPrompt As String, strMsg As String, _
   strFileSpec As String
   strPrompt = "Enter file specification to check:"
StartHere:
   strFileSpec = "*.*"   ' Start with a default
                        ' specification.
   strMsg = strMsg & vbCRLF & strPrompt
   ' Let the user modify the default.
   strFileSpec = InputBox(strMsg, "File Search", _
   strFileSpec, 100, 100)
   ' Exit if user deletes default.
   If strFileSpec = "" Then Exit Function
   On Error GoTo Handler
      VerifyFile = Dir(strFileSpec)
      Exit Function
Handler:
   Select Case Err.Number   ' Analyze error code and
                           ' load message.
      Case ErrInvalidFileName, ErrBadFileName
         strMsg = "Your file specification was "
         strMsg = strMsg & "invalid; try another."
      Case MnErrDriveDoorOpen
         strMsg = "Close the disk drive door and "
         strMsg = strMsg & "try again."
      Case MnErrDeviceUnavailable
         strMsg = "The drive you specified was not "
         strMsg = strMsg & "found. Try again."
      Case Else
         Dim intErrNum As Integer
         intErrNum = Err.Number
         Err.Clear            ' Clear the Err object.
         Err.Raise Number:= intErrNum   ' Regenerate
                                    ' the error.
   End Select
   Resume StartHere   ' This jumps back to StartHere
                     ' label so the user can try
                     ' another file name.
End Function

If a file matching the specification is found, the function returns the file name. If no matching file is found, the function returns a zero-length string. If one of the anticipated errors occurs, a message is assigned to the strMsg variable and execution jumps back to the label StartHere. This gives the user another chance to enter a valid path and file specification.

If the error is unanticipated, the Case Else segment regenerates the error so that the next error handler in the calls list can trap the error. This is necessary because if the error wasn't regenerated, the code would continue to execute at the Resume StartHere line. By regenerating the error you are in effect causing the error to occur again; the new error will be trapped at the next level in the call stack.

For More Information For more details, see "Error Handling Hierarchy" later in this chapter.

Note Although using Resume line is a legitimate way to write code, a proliferation of jumps to line labels can render code difficult to understand and debug.


Error Handling Hierarchy

An enabled error handler is one that was activated by executing an On Error statement and hasn't yet been turned off — either by an On Error GoTo 0 statement or by exiting the procedure where it was enabled. An active error handler is one in which execution is currently taking place. To be active, an error handler must first be enabled, but not all enabled error handlers are active. For example, after a Resume statement, a handler is deactivated but still enabled.

When an error occurs within a procedure lacking an enabled error-handling routine, or within an active error-handling routine, Visual Basic searches the calls list for another enabled error-handling routine. The calls list is the sequence of calls that leads to the currently executing procedure; it is displayed in the Call Stack dialog box. You can display the Call Stack dialog box only when in break mode (when you pause the execution of your application), by selecting the View, Call Stack menu item or by pressing CTRL+L.

Searching the Calls List

Suppose the following sequence of calls occurs, as shown in Figure 13.2:

  1. An event procedure calls Procedure A.

  2. Procedure A calls Procedure B.

  3. Procedure B calls Procedure C.

Figure 13.2 A sequence of calls

While Procedure C is executing, the other procedures are pending, as shown in the calls list in the Call Stack dialog box.

For More Information For more information, see "Monitoring the Call Stack" later in this chapter.

Figure 13.3 shows the calls list displayed in the Call Stack dialog box.

Figure 13.3 The calls list when procedures are pending

If an error occurs in Procedure C and this procedure doesn't have an enabled error handler, Visual Basic searches backward through the pending procedures in the calls list — first Procedure B, then Procedure A, then the initial event procedure (but no farther) — and executes the first enabled error handler it finds. If it doesn't encounter an enabled error handler anywhere in the calls list, it presents a default unexpected error message and halts execution.

If Visual Basic finds an enabled error-handling routine, execution continues in that routine as if the error had occurred in the same procedure that contains the error handler. If a Resume or a Resume Next statement is executed in the error-handling routine, execution continues as shown in the following table.

Statement | Result
Resume | The call to the procedure that Visual Basic just searched is re-executed. In the calls list given earlier, if Procedure A has an enabled error handler that includes a Resume statement, Visual Basic re-executes the call to Procedure B.
Resume Next | Execution returns to the statement following the last statement executed in that procedure. This is the statement following the call to the procedure that Visual Basic just searched back through. In the calls list given earlier, if Procedure A has an enabled error handler that includes a Resume Next statement, execution returns to the statement after the call to Procedure B.

Notice that the statement executed is in the procedure where the error-handling procedure is found, not necessarily in the procedure where the error occurred. If you don't take this into account, your code may perform in ways you don't intend. To make the code easier to debug, you can simply go into break mode whenever an error occurs, as explained in the section, "Turning Off Error Handling," later in this chapter.

If the error handler's range of errors doesn't include the error that actually occurred, an unanticipated error can occur within the procedure with the enabled error handler. In such a case, the procedure could execute endlessly, especially if the error handler executes a Resume statement. To prevent such situations, use the Err object's Raise method in a Case Else statement in the handler. This actually generates an error within the error handler, forcing Visual Basic to search through the calls list for a handler that can deal with the error.

In the VerifyFile procedure example in the Errors.vbp sample application, the number originally contained in Err.Number is assigned to a variable, intErrNum, which is then passed as an argument to the Err object's Raise method in a Case Else statement, thereby generating an error. When such an error occurs within an active error handler, the search back through the calls list begins.

Allocating Errors to Different Handlers

The effect of the search back through the calls list is hard to predict, because it depends on whether Resume or Resume Next is executed in the handler that processes the error successfully. Resume returns control to the most recently executed call out of the procedure containing the error handler. Resume Next returns control to whatever statement immediately follows the most recently executed call out of the procedure containing the error handler.

For example, in the calls list shown in Figure 13.3, if Procedure A has an enabled error handler and Procedure B and C don't, an error occurring in Procedure C will be handled by Procedure A's error handler. If that error handler uses a Resume statement, upon exit, the program continues with a call to Procedure B. However, if Procedure A's error handler uses a Resume Next statement, upon exit, the program will continue with whatever statement in Procedure A follows the call to Procedure B. In both cases the error handler does not return directly to either the procedure or the statement where the error originally occurred.

Guidelines for Complex Error Handling

When you write large Visual Basic applications that use multiple modules, the error-handling code can get quite complex. Keep these guidelines in mind:

  • While you are debugging your code, use the Err object's Raise method to regenerate the error in all error handlers for cases where no code in the handler deals with the specific error. This allows your application to try to correct the error in other error-handling routines along the calls list. It also ensures that Visual Basic will display an error message if an error occurs that your code doesn't handle. When you test your code, this technique helps you uncover the errors you aren't handling adequately. However, in a stand-alone .exe file, you should be cautious: If you execute the Raise method and no other procedure traps the error, your application will terminate execution immediately, without any QueryUnload or Unload events occurring.

  • Use the Clear method if you need to explicitly clear the Err object after handling an error. This is necessary when using inline error handling with On Error Resume Next. Visual Basic calls the Clear method automatically whenever it executes any type of Resume statement, Exit Sub, Exit Function, Exit Property, or any On Error statement.

  • If you don't want another procedure in the calls list to trap the error, use the Stop statement to force your code to terminate. Using Stop lets you examine the context of the error while refining your code in the development environment.

    Caution Be sure to remove any Stop statements before you create an .exe file. If a stand-alone Visual Basic application (.exe) encounters a Stop statement, it treats it as an End statement and terminates execution immediately, without any QueryUnload or Unload events occurring.

  • Write a fail-safe error-handling procedure that all your error handlers can call as a last resort for errors they cannot handle. This fail-safe procedure can perform an orderly termination of your application by unloading forms and saving data.

For More Information See the "Inline Error Handling," "Design Time, Run Time, and Break Mode," and "Testing Error Handling by Generating Errors" topics later in this chapter.


Testing Error Handling by Generating Errors

Simulating errors is useful when you are testing your applications, or when you want to treat a particular condition as being equivalent to a Visual Basic run-time error. For example, you might be writing a module that uses an object defined in an external application, and want errors returned from the object to be handled as actual Visual Basic errors by the rest of your application.

In order to test for all possible errors, you may need to generate some of the errors in your code. You can generate an error in your code with the Raise method:

object.Raise argumentlist

The object argument is usually Err, Visual Basic's globally defined error object. The argumentlist argument is a list of named arguments that can be passed with the method. The VerifyFile procedure in the Errors.vbp sample application uses the following code to regenerate the current error in an error handler:

Err.Raise Number:=intErrNum

In this case, intErrNum is a variable that contains the error number which triggered the error handler. When the code reaches a Resume statement, the Clear method of the Err object is invoked. It is necessary to regenerate the error in order to pass it back to the previous procedure on the call stack.

You can also simulate any Visual Basic run-time error by supplying the error code for that error:

Err.Raise Number:=71   ' Simulate "Disk Not Ready"
                     ' error.

Defining Your Own Errors

Sometimes you may want to define errors in addition to those defined by Visual Basic. For example, an application that relies on a modem connection might generate an error when the carrier signal is dropped. If you want to generate and trap your own errors, you can add your error numbers to the vbObjectError constant.

The vbObjectError constant reserves the numbers ranging from its own offset to its offset + 512. Using a number higher than this will ensure that your error numbers will not conflict with future versions of Visual Basic or other Microsoft Basic products. ActiveX controls may also define their own error numbers. To avoid conflicts with them, consult the documentation for controls you use in your application.

To define your own error numbers, you add constants to the Declarations section of your module:

' Error constants
Const gLostCarrier = 1 + vbObjectError + 512
Const gNoDialTone = 2 + vbObjectError + 512

You can then use the Raise method as you would with any of the intrinsic errors. In this case, the description property of the Err object will return a standard description — "Application-defined or object defined error." To provide your own error description, you will need to add it as a parameter to the Raise method.

For More Information To learn more about generating your own error, see "Raise Method."


Inline Error Handling

You may be accustomed to programming in a language that doesn't raise exceptions — in other words, it doesn't interrupt your code's execution by generating exceptions when errors occur, but instead records errors for you to check later. The C programming language works in this manner, and you may sometimes find it convenient to follow this practice in your Visual Basic code.

When you check for errors immediately after each line that may cause an error, you are performing inline error handling. This topic explains the different approaches to inline error handling, including:

  • Writing functions and statements that return error numbers when an error occurs.

  • Raising a Visual Basic error in a procedure and handling the error in an inline error handler in the calling procedure.

  • Writing a function to return a Variant data type, and using the Variant to indicate to the calling procedure that an error occurred.

Returning Error Numbers

There are a number of ways to return error numbers. The simplest way is to create functions and statements that return an error number, instead of a value, if an error occurs. The following example shows how you can use this approach in the FileExists function example, which indicates whether or not a particular file exists.

Function FileExists (p As String) As Long
   If Dir (p) <> " " Then
      FileExists = conSuccess   ' Return a constant
                                 ' indicating the
   Else                           ' file exists.
      FileExists = conFailure   ' Return failure
                                 ' constant.
   End If
End Function

Dim ResultValue As Long
ResultValue = FileExists ("C:\Testfile.txt")
If ResultValue = conFailure Then
   .
   .   ' Handle the error.
   .
Else
   .
   .   ' Proceed with the program.
   .
End If

The key to inline error handling is to test for an error immediately after each statement or function call. In this manner, you can design a handler that anticipates exactly the sort of error that might arise and resolve it accordingly. This approach does not require that an actual run-time error arise. This becomes useful when working with API and other DLL procedures which do not raise Visual Basic exceptions. Instead, these procedures indicate an error condition, either in the return value, or in one of the arguments passed to the procedures; check the documentation for the procedure you are using to determine how these procedures indicate an error condition.

Handling Errors in the Calling Procedure

Another way to indicate an error condition is to raise a Visual Basic error in the procedure itself, and handle the error in an inline error handler in the calling procedure. The next example shows the same FileExists procedure, raising an error number if it is not successful. Before calling this function, the On Error Resume Next statement sets the values of the Err object properties when an error occurs, but without trying to execute an error-handling routine.

The On Error Resume Next statement is followed by error-handling code. This code can check the properties of the Err object to see if an error occurred. If Err.Number doesn't contain zero, an error has occurred, and the error-handling code can take the appropriate action based on the values of the Err object's properties.

Function FileExists (p As String)
   If Dir (p) <> " " Then
      Err.Raise conSuccess   ' Return a constant
                           ' indicating the
   Else                        'file exists.
      Err.Raise conFailure   ' Raise error number
                           ' conFailure.
   End If
End Function

Dim ResultValue As Long
On Error Resume Next
ResultValue = FileExists ("C:\Testfile.txt")
If Err.Number = conFailure Then
   .
   .   ' Handle the error.
   .
Else
   .
   .   ' Continue program.
   .
End If

The next example uses both the return value and one of the passed arguments to indicate whether or not an error condition resulted from the function call.

Function Power (X As Long, P As Integer, _
ByRef Result As Integer)As Long
   On Error GoTo ErrorHandler
   Result = x^P
   Exit Function
ErrorHandler:
   Power = conFailure
End Function

' Calls the Power function.
Dim lngReturnValue As Long, lngErrorMaybe As Long
lngErrorMaybe = Power (10, 2, lngReturnValue)
If lngErrorMaybe Then
   .
   .   ' Handle the error.
   .
Else
   .
   .   ' Continue program.
   .
End If

If the function was written simply to return either the result value or an error code, the resulting value might be in the range of error codes, and your calling procedure would not be able to distinguish them. By using both the return value and one of the passed arguments, your program can determine that the function call failed, and take appropriate action.

Using Variant Data Types

Another way to return inline error information is to take advantage of the Visual Basic Variant data type and some related functions. A Variant has a tag that indicates what type of data is contained in the variable, and it can be tagged as a Visual Basic error code. You can write a function to return a Variant, and use this tag to indicate to the calling procedure that an error has occurred.

The following example shows how the Power function can be written to return a Variant.

Function Power (X As Long, P As Integer) As Variant
   On Error GoTo ErrorHandler
   Power = x^P
   Exit Function

ErrorHandler:
   Power = CVErr(Err.Number)   ' Convert error code to
                              ' tagged Variant.
End Function

' Calls the Power function.
Dim varReturnValue As Variant
varReturnValue = Power (10, 2)
If IsError (varReturnValue) Then
   .
   .   ' Handle the error.
   .
Else
   .
   .   ' Continue program.
   .
End If


Centralized Error Handling

When you add error-handling code to your applications, you'll quickly discover that you're handling the same errors over and over. With careful planning, you can reduce code size by writing a few procedures that your error-handling code can call to handle common error situations.

The following FileErrors function procedure shows a message appropriate to the error that occurred and, where possible, allows the user to choose a button to specify what action the program should take next. It then returns code to the procedure that called it. The value of the code indicates which action the program should take. Note that user-defined constants such as MnErrDeviceUnavailable must be defined somewhere (either globally, or at the module level of the module containing the procedure, or within the procedure itself). The constant vbExclamation is defined in the Visual Basic (VB) object library, and therefore does not need to be declared.

Function FileErrors () As Integer
   Dim intMsgType As Integer, strMsg As String
   Dim intResponse As Integer
   ' Return Value      Meaning
   ' 0                  Resume
   ' 1                  Resume Next
   ' 2                  Unrecoverable error
   ' 3                  Unrecognized error
   intMsgType = vbExclamation
   Select Case Err.Number
      Case MnErrDeviceUnavailable   ' Error 68.
         strMsg = "That device appears unavailable."
         intMsgType = vbExclamation + 4
      Case MnErrDiskNotReady      ' Error 71.
         strMsg = "Insert a disk in the drive "
         strMsg = strMsg & "and close the door."
         intMsgType = vbExclamation + 4
      Case MnErrDeviceIO         ' Error 57.
         strMsg = "Internal disk error."
         intMsgType = vbExclamation + 4
      Case MnErrDiskFull         ' Error 61.
         strMsg = "Disk is full. Continue?"
         intMsgType = vbExclamation + 3
      ' Error 64 & 52.
      Case ErrBadFileName, ErrBadFileNameOrNumber
         strMsg = "That filename is illegal."
         intMsgType = vbExclamation + 4
      Case ErrPathDoesNotExi      ' Error 76.
         strMsg = "That path doesn't exist."
         intMsgType = vbExclamation + 4
      Case ErrBadFileMode         ' Error 54.
         strMsg = "Can't open your file for that "
         strMsg = strMsg & "type of access."
         intMsgType = vbExclamation + 4
      Case ErrFileAlreadyOpen   ' Error 55.
         strMsg = "This file is already open."
         intMsgType = vbExclamation + 4
      Case ErrInputPastEndOfFile   ' Error 62.
         strMsg = "This file has a nonstandard "
         strMsg = strMsg & "end-of-file marker, "
         strMsg = strMsg & "or an attempt was made "
         strMsg = strMsg & "to read beyond "
         strMsg = strMsg & "the end-of-file marker."
         intMsgType = vbExclamation + 4
      Case Else
         FileErrors = 3
         Exit Function
   End Select
   intResponse = MsgBox (strMsg, intMsgType, _
   "Disk Error")
   Select Case intRresponse
      Case 1, 4      ' OK, Retry buttons.
         FileErrors = 0
      Case 5         ' Ignore button.
         FileErrors = 1
      Case 2, 3      ' Cancel, End buttons.
         FileErrors = 2
      Case Else
         FileErrors = 3
   End Select
End Function

This procedure handles common file and disk-related errors. If the error is not related to disk Input/Output, it returns the value 3. The procedure that calls this procedure should then either handle the error itself, regenerate the error with the Raise method, or call another procedure to handle it.

Note As you write larger applications, you'll find that you are using the same constants in several procedures in various forms and modules. Making those constants public and declaring them in a single standard module may better organize your code and save you from typing the same declarations repeatedly.

You can simplify error handling by calling the FileErrors procedure wherever you have a procedure that reads or writes to disk. For example, you've probably used applications that warn you if you attempt to replace an existing disk file. Conversely, when you try to open a file that doesn't exist, many applications warn you that the file does not exist and ask if you want to create it. In both instances, errors can occur when the application passes the file name to the operating system.

The following checking routine uses the value returned by the FileErrors procedure to decide what action to take in the event of a disk-related error.

Function ConfirmFile (FName As String, _
Operation As Integer) As Integer
' Parameters:
' Fname: File to be checked for and confirmed.
' Operation: Code for sequential file access mode
' (Output, Input, and so on).
' Note that the procedure works for binary and random
' access because messages are conditioned on Operation
' being <> to certain sequential modes.
' Return values:
' 1      Confirms operation will not cause a problem.
' 0      User decided not to go through with operation.
   Const conSaveFile = 1, conLoadFile = 2
   Const conReplaceFile = 1, conReadFile = 2
   Const conAddToFile = 3, conRandomFile = 4
   Const conBinaryFile = 5
   Dim intConfirmation As Integer
   Dim intAction As Integer
   Dim intErrNum As Integer, varMsg As Variant

   On Error GoTo ConfirmFileError   ' Turn on the error
                                 ' trap.
   FName = Dir(FName)         ' See if the file exists.
   On Error GoTo 0            ' Turn error trap off.
   ' If user is saving text to a file that already
   ' exists...
   If FName <> "" And Operation = conReplaceFile Then
      varMsg = "The file " & FName &
      varMsg = varMsg & "already exists on " & vbCRLF
      varMsg = varMsg & "disk. Saving the text box "
      varMsg = varMsg & & vbCRLF
      varMsg = varMsg & "contents to that file will "
      varMsg = varMsg & "destroy the file's current "
      varMsg = varMsg & "contents, " & vbCRLF _
      varMsg = varMsg & "replacing them with the "
      varMsg = varMsg & "text from the text box."
      varMsg = varMsg & vbCRLF & vbCRLF
      varMsg = varMsg & "Choose OK to replace file, "
      varMsg = varMsg & "Cancel to stop."
      intConfirmation = MsgBox(varMsg, 65, _
      "File Message")
   ' If user wants to load text from a file that
   ' doesn't exist.
   ElseIf FName = "" And Operation = conReadFile Then
      varMsg = "The file " & FName
      varMsg = varMsg & " doesn't exist." & vbCRLF
      varMsg = varMsg & "Would you like to create and       varMsg = varMsg & "then edit it?" & vbCRLF
      varMsg = varMsg & vbCRLF & "Choose OK to "
      varMsg = varMsg & "create file, Cancel to stop."
      intConfirmation = MsgBox(varMsg, 65, _
      "File Message")
   ' If FName doesn't exist, force procedure to return
   ' 0 by setting
   ' intConfirmation = 2.
   ElseIf FName = "" Then
      If Operation = conRandomFile Or _
      Operation = conBinaryFile Then
         intConfirmation = 2
      End If
   ' If the file exists and operation isn't
   ' successful,
   ' intConfirmation = 0 and procedure returns 1.
   End If
   ' If no box was displayed, intConfirmation = 0;
   ' if user chose OK, in either case,
   ' intConfirmation = 1 and ConfirmFile should
   ' return 1 to confirm that the intended operation
   ' is OK. If intConfirmation > 1, ConfirmFile should
   ' return 0, because user doesn't want to go through
   ' with the operation...
   If intConfirmation > 1 Then
      ConfirmFile = 0
   Else
      ConfirmFile = 1
      If Confirmation = 1 Then
         ' User wants to create file.
         If Operation = conLoadFile Then
            ' Assign conReplaceFile so caller will
            ' understand action that will be taken.
            Operation = conReplaceFile
         End If
      ' Return code confirming action to either
      ' replace existing file or create new one.
      End If
   End If
Exit Function
ConfirmFileError:
intAction = FileErrors
   Select Case intAction
      Case 0
         Resume
      Case 1
         Resume Next
      Case 2
         Exit Function
      Case Else
         intErrNum = Err.Number
         Err.Raise Number:=intErrNum
         Err.Clear
   End Select
End Function

The ConfirmFile procedure receives a specification for the file whose existence will be confirmed, plus information about which access mode will be used when an attempt is made to actually open the file. If a sequential file is to be saved (conReplaceFile), and a file is found that already has that name (and will therefore be overwritten), the user is prompted to confirm that overwriting the file is acceptable.

If a sequential file is to be opened (conReadFile) and the file is not found, the user is prompted to confirm that a new file should be created. If the file is being opened for random or binary access, its existence or nonexistence is either confirmed (return value 1) or refuted (return value 0). If an error occurs in the call to Dir, the FileErrors procedure is called to analyze the error and prompt the user for a reasonable course of action.


Turning Off Error Handling

If an error trap has been enabled in a procedure, it is automatically disabled when the procedure finishes executing. However, you may want to turn off an error trap in a procedure while the code in that procedure is still executing. To turn off an enabled error trap, use the On Error GoTo 0 statement. Once Visual Basic executes this statement, errors are detected but not trapped within the procedure. You can use On Error GoTo 0 to turn off error handling anywhere in a procedure — even within an error-handling routine itself.

For example, try single stepping, using Step Into, through a procedure such as this:

Sub ErrDemoSub ()
   On Error GoTo SubHandler   ' Error trapping is
                              ' enabled.
      ' Errors need to be caught and corrected here.
      ' The Kill function is used to delete a file.
      Kill "Oldfile.xyz"
   On Error GoTo 0   ' Error trapping is turned off
                     ' here.
      Kill "Oldfile.xyz"
   On Error GoTo SubHandler   ' Error trapping is
                              ' enabled again.
      Kill "Oldfile.xyz"
   Exit Sub
SubHandler:         ' Error-handling routine goes here.
   MsgBox "Caught error."
   Resume Next
End Sub

For More Information To learn how to use the Step Into feature, see "Running Selected Portions of Your Application" later in this chapter.

Debugging Code with Error Handlers

When you are debugging code, you may find it confusing to analyze its behavior when it generates errors that are trapped by an error handler. You could comment out the On Error line in each module in the project, but this is also cumbersome.

Instead, while debugging, you could turn off error handlers so that every time there's an error, you enter break mode.

To disable error handlers while debugging

  1. From the Code window context menu (available by right-clicking on the Code window), choose Toggle.

  2. Select the Break on All Errors option.

With this option selected, when an error occurs anywhere in the project, you will enter break mode and the Code window will display the code where the error occurred.

If this option is not selected, an error may or may not cause an error message to be displayed, depending on where the error occurred. For example, it may have been raised by an external object referenced by your application. If it does display a message, it may be meaningless, depending on where the error originated.


Error Handling with ActiveX Components

In applications that use one or more objects, it becomes more difficult to determine where an error occurs, particularly if it occurs in another application's object. For example, Figure 13.4 shows an application that consists of a form module, that references a class module, that in turn references a Microsoft Excel Worksheet object.

Figure 13.4 Regenerating errors between forms, classes, and ActiveX components

If the Worksheet object does not handle a particular error arising in the Worksheet, but regenerates it instead, Visual Basic will pass the error to the referencing object, MyClassA. When an error is raised in an external object and it is untrapped, it will be raised in the procedure that called the external object.

The MyClassA object can either handle the error (which is preferable), or regenerate it. The interface specifies that any object regenerating an error that arises in a referenced object should not simply propagate the error (pass the error code), but should instead remap the error number to something meaningful. When you remap the error, the number can either be a number defined by Visual Basic that indicates the error condition, if your handler can determine that the error is similar to a defined Visual Basic error (for instance, overflow or division by zero), or an undefined error number. Add the new number to the intrinsic Visual Basic constant vbObjectError to notify other handlers that this error was raised by your object.

Whenever possible, a class module should try to handle every error that arises within the module itself, and should also try to handle errors that arise in an object it references that are not handled by that object. However, there are some errors that it cannot handle because it cannot anticipate them. There are also cases where it is more appropriate for the referencing object to handle the error, rather than the referenced object.

When an error occurs in the form module, Visual Basic raises one of the predefined Visual Basic error numbers.

Note If you are creating a public class, be sure to clearly document the meaning of each non-Visual Basic error-handler you define. (Public classes cannot be created in the Learning Edition.) Other programmers who reference your public classes will need to know how to handle errors raised by your objects.

When you regenerate an error, leave the Err object's other properties unchanged. If the raised error is not trapped, the Source and Description properties can be displayed to help the user take corrective action.

Handling Errors in Objects

A class module could include the following error handler to accommodate any error it might trap, regenerating those it is unable to resolve:

MyServerHandler:
   Select Case ErrNum
      Case 7      ' Handle out-of-memory error.
         .
         .
         .
      Case 440      ' Handle external object error.
         Err.Raise Number:=vbObjectError + 9999
      ' Error from another Visual Basic object.
      Case Is > vbObjectError and Is < vbObjectError _
      + 65536
         ObjectError = ErrNum
      Select Case ObjectError
         ' This object handles the error, based on
         ' error code documentation for the object.
         Case vbObjectError + 10
         .
         .
         .
         Case Else
            ' Remap error as generic object error and
            ' regenerate.
            Err.Raise Number:=vbObjectError + 9999
         End Select
      Case Else
         ' Remap error as generic object error and
         ' regenerate.
         Err.Raise Number:=vbObjectError + 9999
   End Select
   Err.Clear
   Resume Next

The Case 440 statement traps errors that arise in a referenced object outside the Visual Basic application. In this example, the error is simply propagated using the value 9999, because it is difficult for this type of centralized handler to determine the cause of the error. When this error is raised, it is generally the result of a fatal automation error (one that would cause the component to end execution), or because an object didn't correctly handle a trapped error. Error 440 shouldn't be propagated unless it is a fatal error. If this trap were written for an inline handler as discussed previously in the topic, "Inline Error Handling," it might be possible to determine the cause of the error and correct it.

The statement

Case Is > vbObjectError and Is < vbObjectError + 65536

traps errors that originate in an object within the Visual Basic application, or within the same object that contains this handler. Only errors defined by objects will be in the range of the vbObjectError offset.

The error code documentation provided for the object should define the possible error codes and their meaning, so that this portion of the handler can be written to intelligently resolve anticipated errors. The actual error codes may be documented without the vbObjectError offset, or they may be documented after being added to the offset, in which case the Case Else statement should subtract vbObjectError, rather than add it. On the other hand, object errors may be constants, shown in the type library for the object, as shown in the Object Browser. In that case, use the error constant in the Case Else statement, instead of the error code.

Any error not handled should be regenerated with a new number, as shown in the Case Else statement. Within your application, you can design a handler to anticipate this new number you've defined. If this were a public class (not available in the Learning Edition), you would also want to include an explanation of the new error-handling code in your application's documentation.

The last Case Else statement traps and regenerates any other errors that are not trapped elsewhere in the handler. Because this part of the trap will catch errors that may or may not have the vbObjectError constant added, you should simply remap these errors to a generic "unresolved error" code. That code should be added to vbObjectError, indicating to any handler that this error originated in the referenced object.

Debugging Error Handlers in ActiveX Components

When you are debugging an application that has a reference to an object created in Visual Basic or a class defined in a class module, you may find it confusing to determine which object generates an error. To make this easier, you can select the Break in Class Module option on the General tab of the Options dialog box (available from the Tools menu). With this option selected, an error in a class module or an object in another application or project that is running in Visual Basic will cause that class to enter the debugger's break mode, allowing you to analyze the error. An error arising in a compiled object will not display the Immediate window in break mode; rather, such errors will be handled by the object's error handler, or trapped by the referencing module.

For More Information For a thorough discussion of the Break in Class Module option, see "Debugging Class Modules" in "Programming with Objects."


Approaches to Debugging

The debugging techniques presented in this chapter use the analysis tools provided by Visual Basic. Visual Basic cannot diagnose or fix errors for you, but it does provide tools to help you analyze how execution flows from one part of the procedure to another, and how variables and property settings change as statements are executed. Debugging tools let you look inside your application to help you determine what happens and why.

Visual Basic debugging support includes breakpoints, break expressions, watch expressions, stepping through code one statement or one procedure at a time, and displaying the values of variables and properties. Visual Basic also includes special debugging features, such as edit-and-continue capability, setting the next statement to execute, and procedure testing while the application is in break mode.

For More Information For a quick overview of Visual Basic debugging, see "Tips for Debugging" later in this chapter.

Kinds of Errors

To understand how debugging is useful, consider the three kinds of errors you can encounter:

  • Compile errors

  • Run-time errors

  • Logic errors

Compile Errors

Compile errors result from incorrectly constructed code. If you incorrectly type a keyword, omit some necessary punctuation, or use a Next statement without a corresponding For statement at design time, Visual Basic detects these errors when you compile the application.

Compile errors include errors in syntax. For example, you could have a statement as follows:

Left

Left is a valid word in the Visual Basic language, but without an object, it doesn't meet the syntax requirements for that word (object.Left). If you have selected the Auto Syntax Check option in the Editor tab on the Options dialog box, Visual Basic will display an error message as soon as you enter a syntax error in the Code window.

To set the Auto Syntax Check option

  1. From the Tools menu, select Options, and click the Editor tab on the Options dialog box.

  2. Select Auto Syntax Check.

For More Information See the section "Avoiding Bugs" later in this chapter for other techniques to use to avoid errors in your code.

Run-Time Errors

Run-time errors occur while the application is running (and are detected by Visual Basic) when a statement attempts an operation that is impossible to carry out. An example of this is division by zero. Suppose you have this statement:

Speed = Miles / Hours

If the variable Hours contains zero, the division is an invalid operation, even though the statement itself is syntactically correct. The application must run before it can detect this error.

For More Information You can include code in your application to trap and handle run-time errors when they occur. For information on dealing with run-time errors, see "How to Handle Errors" earlier in this chapter.

Logic Errors

Logic errors occur when an application doesn't perform the way it was intended. An application can have syntactically valid code, run without performing any invalid operations, and yet produce incorrect results. Only by testing the application and analyzing results can you verify that the application is performing correctly.

How Debugging Tools Help

Debugging tools are designed to help you with:

  • Logic and run-time errors.

  • Observing the behavior of code that has no errors.

For instance, an incorrect result may be produced at the end of a long series of calculations. In debugging, the task is to determine what and where something went wrong. Perhaps you forgot to initialize a variable, chose the wrong operator, or used an incorrect formula.

There are no magic tricks to debugging, and there is no fixed sequence of steps that works every time. Basically, debugging helps you understand what's going on while your application runs. Debugging tools give you a snapshot of the current state of your application, including:

  • Appearance of the user interface (UI).

  • Values of variables, expressions, and properties.

  • Active procedure calls.

The better you understand how your application is working, the faster you can find bugs.

For More Information For more details on viewing and testing variables, expressions, properties, and active procedure calls, see "Testing Data and Procedures with the Immediate Window" and "Monitoring the Call Stack" later in this chapter.

The Debug Toolbar

Among its many debugging tools, Visual Basic provides several buttons on the optional Debug toolbar that are very helpful. Figure 13.5 shows these tools. To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.

Figure 13.5 The Debug toolbar

The following table briefly describes each tool's purpose. The topics in this chapter discuss situations where each of these tools can help you debug or analyze an application more efficiently.

Debugging tool | Purpose
Breakpoint | Defines a line in the Code window where Visual Basic suspends execution of the application.
Step Into | Executes the next executable line of code in the application and steps into procedures.
Step Over | Executes the next executable line of code in the application without stepping into procedures.
Step Out | Executes the remainder of the current procedure and breaks at the next line in the calling procedure.
Locals Window | Displays the current value of local variables.
Immediate Window | Allows you to execute code or query values while the application is in break mode.
Watch window | Displays the values of selected expressions.
Quick Watch | Lists the current value of an expression while the application is in break mode.
Call Stack | While in break mode, presents a dialog box that shows all procedures that have been called but not yet run to completion.

For More Information The debugging tools are only necessary if there are bugs in your application. See "Avoiding Bugs" later in this chapter.


Avoiding Bugs

There are several ways to avoid creating bugs in your applications:

  • Design your applications carefully by writing down the relevant events and the way your code will respond to each one. Give each event procedure and each general procedure a specific, well-defined purpose.

  • Include numerous comments. As you go back and analyze your code, you'll understand it much better if you state the purpose of each procedure in comments.

  • Explicitly reference objects whenever possible. Declare objects as they are listed in the Classes/Modules box in the Object Browser, rather than using a Variant or the generic Object data types.

  • Develop a consistent naming scheme for the variables and objects in your application. For more information, see "Coding Conventions."

  • One of the most common sources of errors is incorrectly typing a variable name or confusing one control with another. You can use Option Explicit to avoid misspelling variable names. For more information on requiring explicit variable declaration, see "Introducing Variables, Constants, and Data Types" in "Programming Fundamentals."


Design Time, Run Time, and Break Mode

To test and debug an application, you need to understand which of three modes you are in at any given time. You use Visual Basic at design time to create an application, and at run time to run it. This chapter introduces break mode, which suspends the execution of the program so you can examine and alter data.

Identifying the Current Mode

The Visual Basic title bar always shows you the current mode. Figure 13.6 shows the title bar for design time, run time, and break mode.

Figure 13.6 Identifying the current mode with the Visual Basic title bar

The characteristics of the three modes are listed in the following table.

Mode Description
Design time Most of the work of creating an application is done at design time. You can design forms, draw controls, write code, and use the Properties window to set or view property settings. You cannot use the debugging tools, except for setting breakpoints and creating watch expressions.

From the Run menu, choose Start, or click the Run button to switch to run time.

If your application contains code that executes when the application starts, choose Step Into from the Run menu (or press F8) to place the application in break mode at the first executable statement.
Run time When an application takes control, you interact with the application the same way a user would. You can view code, but you cannot change it.

From the Run menu, choose End, or click the End button to switch back to design time.
Break mode From the Run menu, choose Break, click the Break button, or press CTRL+BREAK to switch to break mode.

Execution is suspended while running the application. You can view and edit code (choose Code from the View menu, or press F7), examine or modify data, restart the application, end execution, or continue execution from the same point.

You can set breakpoints and watch expressions at design time, but other debugging tools work only in break mode. See "Using Break Mode" later in this chapter.

Using the Toolbar to Change Modes

The toolbar provides three buttons that let you change quickly from one mode to another. These buttons appear in Figure 13.7.

Figure 13.7 Start, Break, and End buttons on the toolbar

Whether any of these buttons is available depends on whether Visual Basic is in run-time mode, design-time mode, or break mode. The following table lists the buttons available for different modes.

Mode | Toolbar buttons available
Design time | Start
Run time | Break, End
Break | Continue, End (in break mode, the Start button becomes the Continue button)


Using the Debugging Windows

Sometimes you can find the cause of a problem by executing portions of code. More often, however, you'll also have to analyze what's happening to the data. You might isolate a problem in a variable or property with an incorrect value, and then have to determine how and why that variable or property was assigned an incorrect value.

With the debugging windows, you can monitor the values of expressions and variables while stepping through the statements in your application. There are three debugging windows: the Immediate window, the Watch window, and the Locals window

  • The Immediate window shows information that results from debugging statements in your code, or that you request by typing commands directly into the window.

    Figure 13.8 The Immediate window

    For More Information To learn more about the Immediate window, see "Testing Data and Procedures with the Immediate Window" later in this chapter.

  • The Watch window shows the current watch expressions, which are expressions whose values you decide to monitor as the code runs. A break expression is a watch expression that will cause Visual Basic to enter break mode when a certain condition you define becomes true. In the Watch window, the Context column indicates the procedure, module, or modules in which each watch expression is evaluated. The Watch window can display a value for a watch expression only if the current statement is in the specified context. Otherwise, the Value column shows a message indicating the statement is not in context. To access the Watch window, select Watch Window from the View menu. Figure 13.9 shows the Watch window.

    Figure 13.9 The Watch window

    For More Information To learn more about the Watch window, see "Monitoring Data with Watch Expressions" later in this chapter.

  • The Locals window shows the value of any variables within the scope of the current procedure. As the execution switches from procedure to procedure, the contents of the Locals window changes to reflect only the variables applicable to the current procedure. To access the Locals window, select Locals Window from the View menu. Figure 13.10 shows the Locals window.

    Figure 13.10 The Locals window

The current procedure and form (or module) determine which variables can be displayed according to the scoping rules presented in "Understanding the Scope of Variables" in "Programming Fundamentals." For example, suppose the Immediate window indicates that Form1 is the current form. In this case, you can display any of the form-level variables in Form1. You can also use Debug.Print to examine local variables of the procedure displayed in the Code window. (You can always examine the value of a public variable.) For more information about printing information in the Immediate window, see "Testing data and Procedures with the Immediate Window" later in this chapter.


Using Break Mode

At design time, you can change the design or code of an application, but you cannot see how your changes affect the way the application runs. At run time, you can watch how the application behaves, but you cannot directly change the code.

Break mode halts the operation of an application and gives you a snapshot of its condition at any moment. Variable and property settings are preserved, so you can analyze the current state of the application and enter changes that affect how the application runs. When an application is in break mode, you can:

  • Modify code in the application.

  • Observe the condition of the application's interface.

  • Determine which active procedures have been called.

  • Watch the values of variables, properties, and statements.

  • Change the values of variables and properties.

  • View or control which statement the application will run next.

  • Run Visual Basic statements immediately.

  • Manually control the operation of the application.

Note You can set breakpoints and watch expressions at design time, but other debugging tools work only in break mode.

Entering Break Mode at a Problem Statement

When debugging, you may want the application to halt at the place in the code where you think the problem might have started. This is one reason Visual Basic provides breakpoints and Stop statements. A breakpoint defines a statement or set of conditions at which Visual Basic automatically stops execution and puts the application in break mode without running the statement containing the breakpoint. See "Using Stop Statements" later in this chapter for a comparison of Stop statements and breakpoints.

You can enter break mode manually if you do any of the following while the application is running:

  • Press CTRL+BREAK.

  • Choose Break from the Run menu.

  • Click the Break button on the toolbar.

It's possible to break execution when the application is idle (when it is between processing of events). When this happens, execution does not stop at a specific line, but Visual Basic switches to break mode anyway.

You can also enter break mode automatically when any of the following occurs:

  • A statement generates an untrapped run-time error.

  • A statement generates a run-time error and the Break on All Errors error trapping option has been selected.

  • A break expression defined in the Add Watch dialog box changes or becomes true, depending on how you defined it.

  • Execution reaches a line with a breakpoint.

  • Execution reaches a Stop statement.

Fixing a Run-Time Error and Continuing

Some run-time errors result from simple oversights when entering code; these errors are easily fixed. Frequent errors include misspelled names and mismatched properties or methods with objects — for example, trying to use the Clear method on a text box, or the Text property with a file list box. Figure 13.11 shows a run-time error message.

Figure 13.11 Run-time errors halt execution

Often you can enter a correction and continue program execution with the same line that halted the application, even though you've changed some of the code. Simply choose Continue from the Run menu or click the Continue button on the toolbar. As you continue running the application, you can verify that the problem is fixed.

If you select Break on All Errors from the Default Error Trapping State option group on the General tab on the Options dialog box (available from the Tools menu), Visual Basic disables error handlers in code, so that when a statement generates a run-time error, Visual Basic enters break mode. If Break on All Errors is not selected, and if an error handler exists, it will intercept code and take corrective action.

Note When you change the Default Error Trapping State option via the Options dialog box, this setting becomes the default for all subsequent sessions of VB. To change error handling for just the current session, select Toggle from the code window context menu to open a submenu that allows selection of the break mode.

Some changes (most commonly, changing variable declarations or adding new variables or procedures) require you to restart the application. When this happens, Visual Basic presents a message that asks if you want to restart the application.


Monitoring Data with Watch Expressions

As you debug your application, a calculation may not produce the result you want or problems might occur when a certain variable or property assumes a particular value or range of values. Many debugging problems aren't immediately traceable to a single statement, so you may need to observe the behavior of a variable or expression throughout a procedure.

Visual Basic automatically monitors watch expressions — expressions that you define — for you. When the application enters break mode, these watch expressions appear in the Watch window, where you can observe their values.

You can also direct watch expressions to put the application into break mode whenever the expression's value changes or equals a specified value. For example, instead of stepping through perhaps tens or hundreds of loops one statement at a time, you can use a watch expression to put the application in break mode when a loop counter reaches a specific value. Or you may want the application to enter break mode each time a flag in a procedure changes value.

Adding a Watch Expression

You can add a watch expression at design time or in break mode. You use the Add Watch dialog box (shown in Figure 13.12) to add watch expressions.

Figure 13.12 The Add Watch dialog box

The following table describes the Add Watch dialog box.

Component | Description
Expression box | Where you enter the expression that the watch expression evaluates. The expression is a variable, a property, a function call, or any other valid expression.
Context option group | Sets the scope of variables watched in the expression. Use if you have variables of the same name with different scope. You can also restrict the scope of variables in watch expressions to a specific procedure or to a specific form or module, or you can have it apply to the entire application by selecting All Procedures and All Modules. Visual Basic can evaluate a variable in a narrow context more quickly.
Watch Type option group | Sets how Visual Basic responds to the watch expression. Visual Basic can watch the expression and display its value in the Watch window when the application enters break mode. Or you can have the application enter break mode automatically when the expression evaluates to a true (nonzero) statement or each time the value of the expression changes.

To add a watch expression

  1. From the Debug menu, choose Add Watch.

  2. The current expression (if any) in the Code Editor will appear in the Expression box on the Add Watch dialog box. If this isn't the expression you want to watch, enter the expression to evaluate in the Expression box.

  3. If necessary, set the scope of the variables to watch.

    If you select the Procedure or Module option under Context, select a procedure, form, or module name from the appropriate list box.

  4. If necessary, select an option button in the Watch Type group to determine how you want Visual Basic to respond to the watch expression.

  5. Choose OK.

Note You can also add an expression by dragging and dropping from the Code Editor to the Watch window.

Editing or Deleting a Watch Expression

The Edit Watch dialog box, shown in Figure 13.13, lists all the current watch expressions. You can edit and delete any watch listed in the Watch window.

Figure 13.13 The Edit Watch dialog box

To edit a watch expression

  1. In the Watch window, double click the watch expression you want to edit.

    -or-

    Select the watch expression you want to edit and choose Edit Watch from the Debug menu.

  2. The Edit Watch dialog box is displayed and is identical to the Add Watch dialog box except for the title bar and the addition of a Delete button.

  3. Make any changes to the expression, the scope for evaluating variables, or the watch type.

  4. Choose OK.

To delete a watch expression

In the Watch window, select the watch expression you want to delete.

  • Press the DELETE key.

Identifying Watch Types

At the left edge of each watch expression in the Watch window is an icon identifying the watch type of that expression. Figure 13.14 defines the icon for each of the three watch types.

Figure 13.14 Watch type icons

Using Quick Watch

While in break mode, you can check the value of a property, variable, or expression for which you have not defined a watch expression. To check such expressions, use the Quick Watch dialog box, shown in Figure 13.15.

Figure 13.15 The Quick Watch dialog box

The Quick Watch dialog box shows the value of the expression you select from the Code window. To continue watching this expression, click the Add button; the Watch window, with relevant information from the Quick Watch dialog box already entered, is displayed. If Visual Basic cannot evaluate the value of the current expression, the Add button is disabled.

To display the Quick Watch dialog box

  1. Select a watch expression in the Code window.

  2. Click the Quick Watch button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press SHIFT+F9.

    -or-

    From the Debug menu, choose Quick Watch.

  3. If you want to add a watch expression based on the expression in the Quick Watch dialog box, choose the Add button.


Using a Breakpoint to Selectively Halt Execution

At run time, a breakpoint tells Visual Basic to halt just before executing a specific line of code. When Visual Basic is executing a procedure and it encounters a line of code with a breakpoint, it switches to break mode.

You can set or remove a breakpoint in break mode or at design time, or at run time when the application is idle.

To set or remove a breakpoint

  1. In the Code window, move the insertion point to the line of code where you want to set or remove a breakpoint.

    -or-

    Click in the margin on the left edge of the Code window next to the line where you want to set or remove a breakpoint.

  2. From the Debug menu, choose Toggle Breakpoint.

    -or-

    Click the Toggle Breakpoint button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press F9.

When you set a breakpoint, Visual Basic highlights the selected line in bold, using the colors that you specified on the Editor Format tab of the Options dialog box, available from the Tools menu.

For example, Figure 13.16 shows a procedure with a breakpoint on the fifth line. In the Code window, Visual Basic indicates a breakpoint by displaying the text on that line in bold and in the colors specified for a breakpoint.

Figure 13.16 A procedure halted by a breakpoint

Identifying the Current Statement

In Figure 13.16, a rectangular highlight surrounds the seventh line of code. This outline indicates the current statement, or next statement to be executed. When the current statement also contains a breakpoint, only the rectangular outline highlights the line of code. Once the current statement moves to another line, the line with the breakpoint is displayed in bold and in color again.

To specify the color of text of the current statement

  1. From the Tools menu, choose Options and click the Editor Format tab on the Options dialog box.

  2. Under Code Colors, select Execution Point Text, and set the Foreground, Background, and Indicator colors.

Examining the Application at a Breakpoint

Once you reach a breakpoint and the application is halted, you can examine the application's current state. Checking results of the application is easy, because you can move the focus among the forms and modules of your application, the Code window, and the debugging windows.

A breakpoint halts the application just before executing the line that contains the breakpoint. If you want to observe what happens when the line with the breakpoint executes, you must execute at least one more statement. To do this, use Step Into or Step Over.

Note Although it’s possible to set a breakpoint in a MouseMove event procedure or in a Timer event, this can cause unexpected results. The normal flow of events is disrupted when entering break mode; single-stepping through the code from within these procedures may present different behavior than that which would occur in run mode.

For More Information See the section, "Running Selected Portions of Your Application," later in this chapter.

When you are trying to isolate a problem, remember that a statement might be indirectly at fault because it assigns an incorrect value to a variable. To examine the values of variables and properties while in break mode, use the Locals window, Quick Watch, watch expressions, or the Immediate window.

For More Information To learn how to use the Immediate window to test the values of properties and variables, see "Testing Data and Procedures with the Immediate Window," later in this chapter. To learn more about watch expressions, see "Monitoring Data with Watch Expressions."


Using Stop Statements

Placing a Stop statement in a procedure is an alternative to setting a breakpoint. Whenever Visual Basic encounters a Stop statement, it halts execution and switches to break mode. Although Stop statements act like breakpoints, they aren't set or cleared the same way.

Caution Be sure to remove any Stop statements before you create an .exe file. If a stand-alone Visual Basic application (.exe) encounters a Stop statement, it treats it as an End statement and terminates execution immediately, without any QueryUnload or Unload events occurring.

Note It's usually better to use the Assert method rather than a Stop statement. The Assert method halts execution only when a specified condition isn't met; unlike the Stop statement, calls to the Assert method are automatically removed when the application is compiled. For more information, see "Verifying Your Code with Assertions" later in this chapter.

Remember that a Stop statement does nothing more than temporarily halt execution, while an End statement halts execution, resets variables, and returns to design time. You can always choose Continue from the Run menu to continue running the application.

For More Information See "Stop Statement." And see "How to Handle Errors" earlier in this chapter for an example that uses the Stop statement.


Running Selected Portions of Your Application

If you can identify the statement that caused an error, a single breakpoint might help you locate the problem. More often, however, you know only the general area of the code that caused the error. A breakpoint helps you isolate that problem area. You can then use Step Into and Step Over to observe the effect of each statement. If necessary, you can also skip over statements or back up by starting execution at a new line.

Step Mode | Description
Step Into | Execute the current statement and break at the next line, even if it's in another procedure.
Step Over | Execute the entire procedure called by the current line and break at the line following the current line.
Step Out | Execute the remainder of the current procedure and break at the statement following the one that called the procedure.

Note You must be in break mode to use these commands. They are not available at design time or run time.

Using Step Into

You can use Step Into to execute code one statement at a time. (This is also known as single stepping.) After stepping through each statement, you can see its effect by looking at your application's forms or the debugging windows.

To step through code one statement at a time

  • From the Debug menu, choose Step Into.

    -or-

    Click the Step Into button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press F8.

When you use Step Into to step through code one statement at a time, Visual Basic temporarily switches to run time, executes the current statement, and advances to the next statement. Then it switches back to break mode.

Note Visual Basic allows you to step into individual statements, even if they are on the same line. A line of code can contain two or more statements, separated by a colon (:). Visual Basic uses a rectangular outline to indicate which of the statements will execute next. Breakpoints apply only to the first statement of a multiple-statement line.

Using Step Over

Step Over is identical to Step Into, except when the current statement contains a call to a procedure. Unlike Step Into, which steps into the called procedure, Step Over executes it as a unit and then steps to the next statement in the current procedure. Suppose, for example, that the statement calls the procedure SetAlarmTime:

SetAlarmTime 11, 30, 0

If you choose Step Into, the Code window shows the SetAlarmTime procedure and sets the current statement to the beginning of that procedure. This is the better choice only if you want to analyze the code within SetAlarmTime.

If you use Step Over, the Code window continues to display the current procedure. Execution advances to the statement immediately after the call to SetAlarmTime, unless SetAlarmTime contains a breakpoint or a Stop statement. Use Step Over if you want to stay at the same level of code and don't need to analyze the SetAlarmTime procedure.

You can alternate freely between Step Into and Step Over. The command you use depends on which portions of code you want to analyze at any given time.

To use Step Over

  • From the Debug menu, choose Step Over.

    -or-

    Click the Step Over button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press SHIFT+F8.

Using Step Out

Step Out is similar to Step Into and Step Over, except it advances past the remainder of the code in the current procedure. If the procedure was called from another procedure, it advances to the statement immediately following the one that called the procedure.

To use Step Out

  • From the Debug menu, choose Step Out.

    -or-

    Click the Step Out button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press CTRL+SHIFT+F8.

Bypassing Sections of Code

When your application is in break mode, you can use the Run To Cursor command to select a statement further down in your code where you want execution to stop. This lets you "step over" uninteresting sections of code, such as large loops.

To use Run To Cursor

  1. Put your application in break mode.

  2. Place the cursor where you want to stop.

  3. Press CTRL+F8.

    -or-

    From the Debug menu, choose Run To Cursor.

Setting the Next Statement to Be Executed

While debugging or experimenting with an application, you can use the Set Next Statement command to skip a certain section of code — for instance, a section that contains a known bug — so you can continue tracing other problems. Or you may want to return to an earlier statement to test part of the application using different values for properties or variables.

With Visual Basic, you can set a different line of code to execute next, provided it falls within the same procedure. The effect is similar to using Step Into, except Step Into executes only the next line of code in the procedure. By setting the next statement to execute, you choose which line executes next.

To set the next statement to be executed

  1. In break mode, move the insertion point (cursor) to the line of code you want to execute next.

  2. From the Debug menu, choose Set Next Statement.

  3. To resume execution, from the Run menu, choose Continue.

    -or-

    From the Debug menu, choose Run To Cursor, Step Into, Step Over, or Step Out.

Showing the Next Statement to Be Executed

You can use Show Next Statement to place the cursor on the line that will execute next. This feature is convenient if you've been executing code in an error handler and aren't sure where execution will resume. Show Next Statement is available only in break mode.

To show the next statement to be executed

  1. While in break mode, from the Debug menu, choose Show Next Statement.

  2. To resume execution, from the Run menu, choose Continue.

    -or-

    From the Debug menu, choose Run To Cursor, Step Into, Step Over, or Step Out.


Monitoring the Call Stack

The Call Stack dialog box shows a list of all active procedure calls. Active procedure calls are the procedures in the application that were started but not completed.

The Call Stack dialog box helps you trace the operation of an application as it executes a series of nested procedures. For example, an event procedure can call a second procedure, which can call a third procedure — all before the event procedure that started this chain is completed. Such nested procedure calls can be difficult to follow and can complicate the debugging process. Figure 13.17 shows the Call Stack dialog box.

Note If you put the application in break mode during an idle loop, no entries appear in the Call Stack dialog box.

Figure 13.17 The Call Stack dialog box

You can display the Call Stack dialog box only when the application is in break mode.

To display the Call Stack dialog box

  • From the View menu, choose Call Stack.

    -or-

    Click the Call Stack button on the Debug toolbar. (To display the Debug toolbar, right-click on the Visual Basic toolbar and select the Debug option.)

    -or-

    Press CTRL+L.

    -or-

    Click the button next to the Procedure box in the Locals window.

Tracing Nested Procedures

The Call Stack dialog box lists all the active procedure calls in a series of nested calls. It places the earliest active procedure call at the bottom of the list and adds subsequent procedure calls to the top of the list.

The information given for each procedure begins with the module or form name, followed by the name of the called procedure. Because the Call Stack dialog box doesn't indicate the variable assigned to an instance of a form, it does not distinguish between multiple instances of forms or classes. For more information on multiple instances of a form, see "Programming with Objects" and "Multiple-Document Interface (MDI) Applications" in "Designing a User Interface."

You can use the Call Stack dialog box to display the statement in a procedure that passes control of the application to the next procedure in the list.

To display the statement that calls another procedure in the Calls Stack dialog box

  1. In the Call Stack dialog box, select the procedure call you want to display.

  2. Choose the Show button.

    The dialog box is closed and the procedure appears in the Code window.

The cursor location in the Code window indicates the statement that calls the next procedure in the Call Stack dialog box. If you choose the current procedure in the Call Stack dialog box, the cursor appears at the current statement.

Checking Recursive Procedures

The Call Stack dialog box can be useful in determining whether "Out of stack space" errors are caused by recursion. Recursion is the ability of a routine to call itself. You can test this by adding the following code to a form in a new project:

Sub Main()
   Static intX As Integer
   intX = intX + 1
   Main
End Sub

Private Sub Form_Click()
   Main
End Sub

Run the application, click the form, and wait for the "Out of stack space" error message. Choose the Debug button, and then choose Call Stack on the View menu. You'll see multiple calls to the Main procedure, as shown in Figure 13.18.

Figure 13.18 The Call Stack dialog box lists a recursive procedure

As a double check, highlight intX in the Code window, and choose Quick Watch from the Debug menu. The value for intX is the number of times the Main procedure executed before the break.


Testing Data and Procedures with the Immediate Window

Sometimes when you are debugging or experimenting with an application, you may want to execute individual procedures, evaluate expressions, or assign new values to variables or properties. You can use the Immediate window to accomplish these tasks. You evaluate expressions by printing their values in the Immediate window.

The following topics explain the many uses of the Immediate window:

  • Printing Information in the Immediate Window Using the Immediate window to monitor your application.

  • Assigning Values to Variables and Properties Interactively changing values at run time.

  • Testing Procedures with the Immediate Window Using the Immediate window to interactively execute procedures.

  • Checking Error Numbers Determining the meaning of run time error numbers.

  • Tips for Using the Immediate Window Some helpful suggestions.


Printing Information in the Immediate Window

There are two ways to print to the Immediate window:

  • Include Debug.Print statements in the application code.

  • Enter Print methods directly in the Immediate window.

These printing techniques offer several advantages over watch expressions:

  • You don't have to break execution to get feedback on how the application is performing. You can see data or other messages displayed as you run the application.

  • Feedback is displayed in a separate area (the Immediate window), so it does not interfere with output that a user sees.

  • Because you can save this code as part of the form, you don't have to redefine these statements the next time you work on the application.

Printing from Application Code

The Print method sends output to the Immediate window whenever you include the Debug object prefix:

Debug.Print [items][;]

For example, the following statement prints the value of Salary to the Immediate window every time it is executed:

Debug.Print "Salary = "; Salary

This technique works best when there is a particular place in your application code at which the variable (in this case, Salary) is known to change. For example, you might put the previous statement in a loop that repeatedly alters Salary.

Note When you compile your application into an .exe file, Debug.Print statements are removed. Thus, if your application only uses Debug.Print statements with strings or simple variable types as arguments, it will not have any Debug.Print statements. However, Visual Basic will not strip out function calls appearing as arguments to Debug.Print. Thus, any side-effects of those functions will continue to happen in a compiled .exe file, even though the function results are not printed.

For More Information See "Debug Object."

Printing from Within the Immediate Window

Once you're in break mode, you can move the focus to the Immediate window to examine data.

To examine data in the Immediate window

  1. Click the Immediate window (if visible).

    -or-

    From the View menu, choose Immediate Window.

    Once you have moved focus to the Immediate window, you then can use the Print method without the Debug object.

  2. Type or paste a statement into the Immediate window, and then press ENTER.

    The Immediate window responds by carrying out the statement, as shown in Figure 13.19.

    Figure 13.19 Using the Print method to print to the Immediate window

A question mark (?) is useful shorthand for the Print method. The question mark means the same as Print, and can be used in any context where Print is used. For example, the statements in Figure 13.19 could be entered as shown in Figure 13.20.

Figure 13.20 Using a question mark instead of the Print method

Printing Values of Properties

You can evaluate any valid expression in the Immediate window, including expressions involving properties. The currently active form or module determines the scope. If the execution halts within code that is attached to a form or class, you can refer to the properties of that form (or one of its controls) and make the reference to the form implicit with statements like the following:

? BackColor
? Text1.Height

Assuming that Text1 is a control on the currently active form, the first statement prints the numeric value of the current form's background color to the Immediate window. The second statement prints the height of Text1.

If execution is suspended in a module or another form, you must explicitly specify the form name as follows:

? Form1.BackColor
? Form1.Text1.Height

Note Referencing an unloaded form in the Immediate window (or anywhere else) loads that form.

For More Information To learn about changing properties and values in the Immediate window, see "Assigning Values to Variables and Properties" later in this chapter.


Assigning Values to Variables and Properties

As you start to isolate the possible cause of an error, you may want to test the effects of particular data values. In break mode, you can set values with statements like these in the Immediate window:

BackColor = 255
VScroll1.Value = 100
MaxRows = 50

The first statement alters a property of the currently active form, the second alters a property of VScroll1, and the third assigns a value to a variable.

After you set the values of one or more properties and variables, you can continue execution to see the results. Or you can test the effect on procedures, as described in the next topic, "Testing Procedures with the Immediate Window."


Testing Procedures with the Immediate Window

The Immediate window evaluates any valid Visual Basic executable statement, but it doesn't accept data declarations. You can enter calls to Sub and Function procedures, however, which allows you to test the possible effect of a procedure with any given set of arguments. Simply enter a statement in the Immediate window (while in break mode) as you would in the Code window. For example:

X = Quadratic(2, 8, 8)
DisplayGraph 50, Arr1
Form_MouseDown 1, 0, 100, 100

When you press the ENTER key, Visual Basic switches to run time to execute the statement, and then returns to break mode. At that point, you can see results and test any possible effects on variables or property values.

Scope applies to procedure calls just as it does to variables. You can call any procedure within the currently active form. You can always call a procedure in a module, unless you define the procedure as Private, in which case you can call the procedure only while executing in the module.

For More Information Scope is discussed in "Introduction to Variables, Constants, and Data Types" in "Programming Fundamentals."

Viewing and Testing Multiple Instances of Procedures

You can use the Immediate window to run a procedure repeatedly, testing the effect of different conditions. Each separate call of the procedure is maintained as a separate instance by Visual Basic. This allows you to separately test variables and property settings in each instance of the procedure. To see how this works, open a new project and add the following code to the form module:

Private Sub Form_Click()
   AProcedure
End Sub

Sub AProcedure()
   Dim intX As Integer
   intX = 10
   BProcedure
End Sub

Sub BProcedure()
   Stop
End Sub

Run the application and click the form. The Stop statement puts Visual Basic into break mode and the Immediate window is displayed. Change the value of intX to 15 in the procedure "AProcedure," switch to the Immediate window, and type the following:

AProcedure

This calls the procedure "AProcedure" and restarts the application. If you switch to the Immediate window and run "AProcedure" again, and then open the Call Stack dialog box, you'll see a listing much like the one in Figure 13.21. Each separate run of the program is listed, separated by the [<Debug Window>] listing.

Figure 13.21 The Call Stack dialog box shows multiple instances of procedures

Visual Basic maintains a listing of the procedures executed by each command from the Immediate window. Newer listings are at the top of the list. You can use the Call Stack dialog box to select any instance of a procedure, and then print the values of variables from that procedure in the Immediate window.

For example, if you double-click the earliest instance of "AProcedure" and use the Immediate window to print the value of intX, it will return 10, as shown in Figure 13.22. If you changed the value of intX to 15 for the second run of the "AProcedure," that value is stored with the second instance of the procedure.

Figure 13.22 Printing the values of variables in the Immediate window

Note Although most statements are supported in the Immediate window, a control structure is valid only if it can be completely expressed on one line of code; use colons to separate the statements that make up the control structure. The following For loop is valid in the Immediate window:

For I = 1 To 20 : Print 2 * I : Next I


Checking Error Numbers

You can use the Immediate window to display the message associated with a specific error number. For example, enter this statement in the Immediate window:

error 58

Press ENTER to execute the statement. The appropriate error message is displayed, as shown in Figure 13.23.

Figure 13.23 Displaying error messages from the Immediate window


Tips for Using the Immediate Window

Here are some shortcuts you can use in the Immediate window:

  • You can use Data Tips to inspect the value of a variable or object. Data Tips are similar to ToolTips except that they display the current value when the mouse is held over a variable or object property in the Code Window in Break mode. The display of Data Tips is limited to variables and objects that are currently in scope.

  • Data Tips are also available in the Immediate window in Break mode. Unlike the Code Editor, the Immediate window will display values for object properties regardless of scope if a fully qualified object name is provided. For example, a Data Tip would always be displayed for Form1.Text1.Width, but not for Text1.Width unless Text1 was currently in scope.

  • Once you enter a statement, you can execute it again by moving the insertion point back to that statement and pressing ENTER anywhere on the line.

  • Before pressing ENTER, you can edit the current statement to alter its effects.

  • You can use the mouse or the arrow keys to move around in the Immediate window. Don't press ENTER unless you are at a statement you want to execute.

  • CTRL+HOME will take you to the top of the Immediate window; CTRL+END will take you to the bottom.

  • The HOME and END keys move to the beginning and end of the current line.

Note Although you will primarily use the Immediate window in Break mode, it is also possible to use the Immediate window in Design mode. This capability is provided in order to debug ActiveX components within the design environment. Using the Immediate window at Design time for a Standard project type may cause unexpected results.

For More Information See "Immediate Window."


Special Debugging Considerations

Certain events that are a common part of using Microsoft Windows can pose special problems for debugging an application. It's important to be aware of these special problems so they don't confuse or complicate the debugging process.

If you remain aware of how break mode can put events at odds with what your application expects, you can usually find solutions. In some event procedures, you may need to use Debug.Print statements to monitor values of variables or properties instead of using watch expressions or breakpoints. You may also need to change the values of variables that depend on the sequence of events. This is discussed in the following topics.

Breaking Execution During MouseDown

If you break execution during a MouseDown event procedure, you may release the mouse button or use the mouse to do any number of tasks. When you continue execution, however, the application assumes that the mouse button is still pressed down. You don't get a MouseUp event until you press the mouse button down again and then release it.

When you press the mouse button down during run time, you break execution in the MouseDown event procedure again, assuming you have a breakpoint there. In this scenario, you never get to the MouseUp event. The solution is usually to remove the breakpoint in the MouseDown procedure.

Breaking Execution During KeyDown

If you break execution during a KeyDown procedure, similar considerations apply. If you retain a breakpoint in a KeyDown procedure, you may never get a KeyUp event. (KeyDown and KeyUp are described in "Responding to Mouse and Keyboard Events.")

Breaking Execution During GotFocus or LostFocus

If you break execution during a GotFocus or LostFocus event procedure, the timing of system messages can cause inconsistent results. Use a Debug.Print statement instead of a breakpoint in GotFocus or LostFocus event procedures.

Modal Dialogs and Message Boxes Suppress Events

The development environment cannot raise events while a modal form or message box is displayed, because of potential conflicts in the debugger. Therefore, events are suppressed until the modal form or message box is dismissed.

Important Suppression of events only happens in the development environment. Once a project is compiled, events will be raised even when a modal form or message box is displayed.

Some example scenarios in which this can occur:

  • A form with a Timer control on it is running in the development environment. Selecting Options from the Tools menu will open the Options dialog box, which is modal. Until the dialog is dismissed, the Timer control's Timer event will not be raised.

  • An instance of a UserControl with a Timer control on it is placed on a form at design time. (The timer may be used to make the control appear animated; this effect can occur even in design mode, because controls can execute code at design time.) Selecting Add Class Module from the Project menu will open the Add Class Module dialog, which is modal. The Timer control's Timer event will be suppressed until the dialog is dismissed.

  • A UserDocument contains a Timer control, and a command button that displays a message box. If the UserDocument is being debugged using Internet Explorer, pressing the button to display the message box will cause the Timer control's Timer event to be suppressed until the message box is dismissed.

Testing and Using Command-Line Arguments

You can choose to have your application use command-line arguments, which provide data to your application at startup. The user can enter them by choosing the operating environment's Run command, and then typing arguments after the application name. You can also use command-line arguments when creating an icon for the application.

For example, suppose you create an alarm clock application. One of the techniques for setting the alarm time is to let the user type in the selected time directly. The user might enter the following string in the Run dialog box:

Alarm 11:00:00

The Command function returns all arguments entered after the application name (in this case, Alarm). The Alarm application has only one argument, so in the application code, you can assign this argument directly to the string that stores the selected time:

AlarmTime = Command

If Command returns an empty string, there are no command-line arguments. The application must either ask for the information directly or select a default action.

To test code that uses Command, you can specify sample command-line arguments from within the Visual Basic environment. The application evaluates sample command-line input the same way it does if the user types the argument.

To set sample command-line arguments

  1. From the Project menu, choose Properties.

  2. Click the Make tab on the Project Properties dialog box.

  3. Enter the sample arguments in the Command Line Arguments field. (Do not type the name of the application itself.)

  4. Choose OK.

  5. Run the application.

For More Information See "Command Function."


Removing Debugging Information Before Compiling

If you do not want debugging statements included in the application you distribute to users, use conditional compilation to conveniently delete these statements when the Make EXE File command is used.

For example:

Sub Assert(Expr As Boolean, Msg As String)
   If Not Expr Then
      MsgBox Msg
   End If
End Sub

Sub AProcedure(intX As Integer)
   # If fDebug Then
      Assert intX < 10000 and intX > 0, _
      "Argument out of range"
   # End If
      ' The code can now assume the correct value.
End Sub

Because the call to the Assert procedure is conditionally compiled, it is only included in the .exe file if fDebug is set to True. When you compile the distribution version of the application, set fDebug to False. As a result, the .exe file will be as small and fast as possible.

Note Since the release of Visual Basic version 5.0, it is no longer necessary to create your own Assert procedures. The Debug.Assert statement performs the same function and is automatically stripped from the compiled code. See "Verifying Your Code with Assertions" later in this chapter for more information.


Verifying Your Code with Assertions

Assertions are a convenient way to test for conditions that should exist at specific points in your code. Think of an Assert statement as making an assumption. If your assumption is True, the assertion will be ignored; if your assumption is False, VB will bring it to your attention.

In Visual Basic, assertions take the form of a method: the Assert method of the Debug object. The Assert method takes a single argument of the type Boolean which states the condition to be evaluated. The syntax for the Assert method is as follows:

Debug.Assert(boolean expression)

A Debug.Assert statement will never appear in a compiled application, but when you're running in the design environment it causes the application to enter break mode with the line containing the statement highlighted (assuming that the expression evaluates to False). The following example shows the Debug.Assert statement:

Debug.Assert Trim(CustName) = "John Doe"

In this case, if the CustName isn't John Doe, the application will enter break mode; otherwise the execution will continue as usual. Using Debug.Assert is similar to setting a watch with the Break When Value Is True option selected, except that it will break when the value is false.


Using Compile on Demand

Compile on Demand and Background Compile are related features that allow your application to run faster in the development environment. It's possible that using these features may hide compile errors in your code until you make an exe for your entire project. Both features are turned on by default, and they can be turned on or off on the General tab of the Options dialog box available from the Tools menu.

Compile on Demand allows your application, in the development environment, to compile code only as needed. When Compile on Demand is on and you choose Start from the Run menu (or press the F5 key), only the code necessary to start the application is compiled. Then, as you exercise more of your application's capabilities in the development environment, more code is compiled as needed.

Background Compile allows Visual Basic at run time in the development environment to continue compiling code if no other actions are occurring.

With these features turned on, some code may not be compiled when a project is run in the development environment. Then, when you choose to Make EXE file (or turn off Compile on Demand), you may see new and unexpected errors as that code is newly compiled.

There are three techniques you can use at development milestones, or any other time, to flush out any errors hidden by using Compile on Demand.

  • Turn Compile on Demand off and then run the application. This forces Visual Basic to check the entire application for compile errors.

  • Make an executable with your project. This will also force Visual Basic to check the entire application for compile errors.

  • Choose Start With Full Compile from the Run menu.


Tips for Debugging

There are several ways to simplify debugging:

  • When your application doesn't produce correct results, browse through the code and try to find statements that may have caused the problem. Set breakpoints at these statements and restart the application.

  • When the program halts, test the values of important variables and properties. Use Quick Watch or set watch expressions to monitor these values. Use the Immediate window to examine variables and expressions.

  • Use the Break on All Errors option to determine where an error occurred. To temporarily change this option, select Toggle from the Code window context menu, then toggle the option from the submenu. Step through your code, using watch expressions and the Locals window to monitor how values change as the code runs.

  • If an error occurs in a loop, define a break expression to determine where the problem occurs. Use the Immediate window together with Set Next Statement to re-execute the loop after making corrections.

  • If you determine that a variable or property is causing problems in your application, use a Debug.Assert statement to halt execution when the wrong value is assigned to the variable or property.

  • To set the error trapping state that Visual Basic defaults to at the beginning of any debugging session, open the Options dialog box (available from the Tools menu), select the General tab, and set the Default Error Trapping State option. Visual Basic will use this setting the next time you start it, even if the setting was entered for another project.

Occasionally you may encounter a bug that’s especially difficult to track down. Don’t panic – here are some things that you can do:

  • First and foremost, make a backup. This is the point at which even experienced programmers frequently lose many hours of work. When you experiment, it is far too easy to accidentally overwrite or delete necessary sections of code.

  • Use the debugging facilities built in to Visual Basic. Attempt to identify the line or lines of code generating the error. Isolate the code. If you can isolate the problem to one block of code, try to reproduce the same problem with this block of code separated from the rest of your program. Select the code, copy it, start a new project, paste the code into the new project, run the new project, and see if the error still occurs.

  • Create a log file. If you cannot isolate the code or if the problem is erratic or if the problem only happens when compiled, then the debugging facility of Visual Basic will be less effective. In these situations you can create a log file which records the activity of your program. This will allow you to progressively isolate the location of the suspect code. Call the following procedure from various points in your program. You should pass in a string of text which indicates the current location of the code executing in your program.
    Sub LogFile (Message As String)
Dim LogFile As Integer
LogFile = FreeFile
Open "C:\VB\LogFile.Log" For Append As #LogFile
Print #LogFile, Message
Close #LogFile
End Sub

Sub Sub1 ()
'...
Call LogFile("Here I am in Sub1")
'...
End Sub
  • Simplify the problem. If possible, remove any third party controls and custom controls from your project. Replace them with Visual Basic standard controls. Eliminate any code that does not seem to relate to the problem.

  • Reduce the search space. If you cannot resolve the problem with any of the above methods, then it is time to eliminate all other non-Visual Basic causes from the problem search space. Copy your AUTOEXEC.BAT and CONFIG.SYS files to backup files. Comment out any and all drivers and programs from these two files that are not absolutely essential to running your program under Windows. Change your Windows video driver to the standard Windows VGA driver. Shut down Windows and reboot your machine. This will eliminate the possibility that there is some other program or driver which is interfering with your program.

  • If you cannot locate a solution and are unable to isolate or resolve the problem with any of these methods, it's time to look for help. See the technical support documentation.

For More Information Breakpoints are described in "Using a Breakpoint to Selectively Halt Execution" earlier in this chapter. Read more about Watch expressions in "Monitoring Data with Watch Expressions." The Immediate window is discussed in "Testing Data and Procedures with the Immediate Window." See "Verifying Your Code with Assertions" for more about the Assert method of the Debug object.


Processing Drives, Folders, and Files

When programming in Windows, it's very important to have the ability to add, move, change, create, or delete folders (directories) and files, and get information about and manipulate drives.

Visual Basic allows you to process drives, folders, and files in two different ways: through traditional methods such as the Open statement, Write#, and so forth, and through a new set of tools, the File System Object (FSO) object model.

Topics

Introduction to the File System Object Model

An overview of the File System Object model.
The File System Objects

An introduction to the File System objects.
Programming in the FSO Object Model

A discussion of how to program in the FSO object model.
Working with Drives and Folders

Methods for handling drives and folders.
Working with Files

Methods for handling files.
Processing Files with Older File I/O Statements and Functions

Using older file I/O statements to process files.
Using Sequential File Access

How to create sequential files using older file I/O statements.
Using Random File Access

How to create random files using older file I/O statements.
Using Binary File Access

How to create binary files using older file I/O statements.


Introduction to the File System Object Model

A new feature for Visual Basic is the File System Object (FSO) object model, which provides an object-based tool for working with folders and files. This allows you to use the familiar object.method syntax with a rich set of properties, methods, and events to process folders and files, in addition to using the traditional Visual Basic statements and commands.

The FSO object model gives your applications the ability to create, alter, move, and delete folders, or to detect if particular folders exist, and if so, where. It also enables you to gain information about folders, such as their names, the date they were created or last modified, and so forth.

The FSO object model makes processing files much easier as well. When processing files, your primary goal is to store data in a space- and resource-efficient, easy-to-access format. You need to be able to create files, insert and change the data, and output (read) the data. While you can store data in a database, such as Jet or SQL, it adds a significant amount of overhead to your application. For many reasons, you may not want to have such an overhead, or your data access requirements may not require all the extra features associated with a full-featured database. In this case, storing your data in a binary or text file is the most efficient solution.

The FSO object model, which is contained in the Scripting type library (Scrrun.Dll), supports text file creation and manipulation through the TextStream object. It does not as yet, however, support the creation or manipulation of binary files. To manipulate binary files, use the Open command with the Binary flag. Full information on how to manipulate binary files is contained in "Using Binary File Access" in this chapter.


The File System Objects

The FSO object model has these objects:

Object | Description
Drive | Allows you to gather information about drives attached to the system, such as how much room is available, what its share name is, and so forth. Note that a "drive" isn't necessarily a hard disk. It can be a CD-ROM drive, a RAM disk, and so forth. Also, drives aren't required to be physically attached to the system; they can be also be logically connected through a LAN.
Folder | Allows you to create, delete, or move folders, plus query the system as to their names, paths, and so on.
Files | Allows you to create, delete, or move files, plus query the system as to their names, paths, and so on.
FileSystemObject | The main object of the group, full of methods that allow you to create, delete, gain information about, and generally manipulate drives, folders, and files. Many of the methods associated with this object duplicate those in the other objects.
TextStream | Enables you to read and write text files.

For information about the various properties, methods, and events in the FSO object model, use the Object Browser in Visual Basic (press F2) and look at the Scripting type library.


Programming in the FSO Object Model

Programming in the FSO object model involves three main tasks:

  • Using the CreateObject method, or dimension a variable as a FileSystemObject object to create a FileSystemObject object.

  • Using the appropriate method on the newly-created object.

  • Accessing the object's properties.

The FSO object model is contained in a type library called Scripting, which is located in the file Scrrun.Dll. If you don't already have a reference to it, check "Microsoft Scripting Runtime" in the References dialog available from the Properties menu. You can then use the Object Browser to view its objects, collections, properties, methods, and events, as well as its constants.

Creating a FileSystemObject Object

The first step is to create a FileSystemObject object to work with. You can do this in two ways:

  • Dimension a variable as type FileSystemObject object:
    Dim fso As New FileSystemObject
  • Use the CreateObject method to create a FileSystemObject object:
    Set fso = CreateObject("Scripting.FileSystemObject")

    In the above syntax, Scripting is the name of the type library, and FileSystemObject is the name of the object which you want to create an instance of.

Note The first method works only in Visual Basic, while the second method works either in Visual Basic or VBScript.

Using the Appropriate Method

The next step is to use the appropriate method of the FileSystemObject object. For example, if you want to create a new object, you can use either CreateFolder or CreateTextFile. (The FSO object model doesn't support the creation or deletion of drives.)

If you want to delete objects, you can use the DeleteFile and DeleteFolder methods of the FileSystemObject object, or the Delete method of the File and Folder objects.

Using the appropriate methods, you can also copy and move files and folders.

Note that some functionality in the FileSystemObject object model is redundant. For example, you can copy a file using either the CopyFile method of the FileSystemObject object, or you can use the Copy method of the File object. The methods work the same. Both exist to give you maximum programming flexibility.

Accessing Existing Drives, Files, and Folders

To gain access to an existing drive, file, or folder, use the appropriate "get" method of the FileSystemObject object:

  • GetDrive

  • GetFolder

  • GetFile

For example:

Dim fso As New FileSystemObject, fil As File
Set fil = fso.GetFile("c:\test.txt")

Note, however, that you don't need to use the "get" methods for newly-created objects, since the "create" functions already return a handle to the newly-created object. For example, if you create a new folder using the CreateFolder method, you don't then need to use the GetFolder method to access its properties, such as Name, Path, Size, and so forth. Just set a variable to the CreateFolder function to gain a handle to the newly-created folder, then access its properties, methods, and events:

Private Sub Create_Folder()
   Dim fso As New FileSystemObject, fldr As Folder
   Set fldr = fso.CreateFolder("C:\MyTest")
   MsgBox "Created folder: " & fldr.Name
End Sub

Accessing the Object's Properties

Once you have a handle to an object, you can access its properties. For example, say you want to obtain the name of a particular folder. First you create an instance of the object, then you get a handle to it with the appropriate method (in this case, the GetFolder method, since the folder already exists):

Set fldr = fso.GetFolder("c:\")

Now that you have a handle to a Folder object, you can check its Name property:

Debug.Print "Folder name is: "; fldr.Name

If you want to find out the last time a file was modified, use the following syntax:

Dim fso As New FileSystemObject, fil As File
Set fil = fso.GetFile("c:\detlog.txt")  ' Get a File object to query.
Debug.Print "File last modified: "; fil.DateLastModified ' Print info.


Working with Drives and Folders

With the FSO object model you can work with drives and folders programmatically just as you can in the Windows Explorer interactively. You can copy and move folders, get information about drives and folders, and so forth.

Getting Information About Drives

The Drive object allows you to gain information about the various drives attached to a system, either physically or over a network. Its properties allow you to obtain information about:

  • The total size of the drive in bytes (TotalSize property)

  • How much space is available on the drive in bytes (AvailableSpace or FreeSpace properties)

  • What letter is assigned to the drive (DriveLetter property)

  • What type of drive it is, such as removable, fixed, network, CD-ROM, or RAM disk (DriveType property)

  • The drive's serial number (SerialNumber property)

  • The type of file system the drive uses, such as FAT, FAT32, NTFS, and so forth (FileSystem property)

  • Whether a drive is available for use (IsReady property)

  • The name of the share and/or volume (ShareName and VolumeName properties)

  • The path or root folder of the drive (Path and RootFolder properties)

Example Usage of the Drive Object

The example below shows how to use the Drive object to gather information about a drive. Remember that you won't see a reference to an actual Drive object in the following code; rather, you use the GetDrive method to get a reference to an existing Drive object (in this case, drv):

Private Sub Command3_Click()
   Dim fso As New FileSystemObject, drv As Drive, s As String
   Set drv = fso.GetDrive(fso.GetDriveName("c:"))
   s = "Drive " & UCase("c:") & " - "
   s = s & drv.VolumeName & vbCrLf
   s = s & "Total Space: " & FormatNumber(drv.TotalSize / 1024, 0)
   s = s & " Kb" & vbCrLf
   s = s & "Free Space: " & FormatNumber(drv.FreeSpace / 1024, 0)
   s = s & " Kb" & vbCrLf
   MsgBox s
End Sub

Using CurDir, ChDrive, ChDir, or App.Path

If you use the CurDir function, the ChDrive and ChDir statements, or the Path property (App.Path), be aware that they may return a UNC path (that is, \\Server\Share…) rather than a drive path (such as E:\Folder), depending on how you run your program or project.

App.Path returns a UNC path:

  • When you run a project after loading it from a network share, even if the network share is mapped to a drive letter.

  • When you run a compiled executable file from a network share (but only if it is run using a UNC path).

ChDrive cannot handle UNC paths, and thus raises an error when App.Path returns one. You can handle this error by adding On Error Resume Next before the ChDrive statement, or by testing the first two characters of App.Path to see if they are backslashes:

On Error Resume Next
ChDrive App.Path
ChDir App.Path

This modification handles all cases in which the program is started from Windows using a UNC path (for example, in the Run dialog accessed from the Start menu), because Windows sets the current directory to a UNC path. ChDir handles changes between UNC paths correctly. (The failure of ChDrive can be ignored, because there is no drive letter for a UNC path.)

However, the above code above won't work if you run the program by entering a UNC path at MS-DOS command prompt. This is because the command prompt always has a drive path for the current directory, so CurDir is set to a drive path. ChDir does not raise an error, but it fails to change the directory from a drive path to a UNC path. The only workaround for this situation is to locate a local drive that's mapped to the share specified in the UNC path, or to use network commands to create such a mapping.

If the project is loaded into the Visual Basic IDE from a network share – either a UNC path or a mapped drive path – then App.Path returns a UNC path when the project is run and ChDrive fails and raises an error. ChDir doesn't raise an error, but the directory is not changed. The only workaround is to manually set the drive and directory:

Const PROJECTSHARE = "E:\VBPROJ\MYPROJECT"
#Const Debug = True
#If Debug Then
    ChDrive PROJECTSHARE
    ChDir PROJECTSHARE
#Else
    On Error Resume Next
    ChDrive App.Path
    ChDir App.Path
#End If

If more than one person might open the project on the network share, a DOS environment variable can be used to allow each person to have their own mapping for the share:

#Const Debug = True
#If Debug Then
    ChDrive Environ("MYPROJECTDIR")
    ChDir Environ("MYPROJECTDIR")
#Else
    On Error Resume Next
    ChDrive App.Path
    ChDir App.Path
#End If

The value of MYPROJECTDIR specifies the mapped drive letter and the path, for example:

SET MYPROJECTDIR=M:\VBProj\MyProject

Working with Folders

This list shows common folder tasks and the methods for doing them:

Task | Method
Create a folder | FileSystemObject.CreateFolder
Delete a folder | Folder.Delete or FileSystemObject.DeleteFolder
Move a folder | Folder.Move or FileSystemObject.MoveFolder
Copy a folder | Folder.Copy or FileSystemObject.CopyFolder
Retrieve the name of a folder | Folder.Name
Find out if a folder exists on a drive | FileSystemObject.FolderExists
Get an instance of an existing Folder object | FileSystemObject.GetFolder
Find out the name of a folder's parent folder | FileSystemObject.GetParentFolderName
Find out the path of system folders | FileSystemObject.GetSpecialFolder

Example

This example demonstrates usage of the Folder and FileSystemObject objects to manipulate folders and gain information about them:

Private Sub Command10_Click()
   ' Get instance of FileSystemObject.
   Dim fso As New FileSystemObject, fldr As Folder, s As String
   ' Get Drive object.
   Set fldr = fso.GetFolder("c:")
   ' Print parent folder name.
   Debug.Print "Parent folder name is: " & fldr
   ' Print drive name.
   Debug.Print "Contained on drive " & fldr.Drive
   ' Print root file name.
   If fldr.IsRootFolder = True Then
      Debug.Print "This folder is a root folder."
   Else
      Debug.Print "This folder isn't a root folder."
   End If
   ' Create a new folder with the FileSystemObject object.
   fso.CreateFolder ("c:\Bogus")
   Debug.Print "Created folder C:\Bogus"
   ' Print the base name of the folder.
   Debug.Print "Basename = " & fso.GetBaseName("c:\bogus")
   ' Get rid of the newly-created folder.
   fso.DeleteFolder ("c:\Bogus")
   Debug.Print "Deleted folder C:\Bogus"
End Sub


Working With Files

You can work with files in Visual Basic by using the new object-oriented FSO objects such as Copy, Delete, Move, and OpenAsTextStream, among others, or by using the older existing functions such as Open, Close, FileCopy, GetAttr, and so forth. Note that you can move, copy, or delete files regardless of their file type.

For more information on usage of the older existing functions, see "Processing Files with Older File I/O Statements and Functions" in this chapter. The rest of this section describes using the new FSO objects, methods, and properties to work with files.

There are two major categories of file manipulation:

  • Creating, adding, or removing data, and reading files

  • Moving, copying, and deleting files

Creating Files and Adding Data with File System Objects

There are three ways to create a sequential text file (sometimes referred to as a "text stream"). One way is to use the CreateTextFile method. To create an empty text file:

Dim fso As New FileSystemObject, fil As File
Set fil = fso.CreateTextFile("c:\testfile.txt", True)

Note The FSO object model does not yet support the creation of random or binary files. To create random and binary files, use the Open command with either the Random or Binary flag. Full information on how to manipulate random and binary files is contained in "Using Random File Access" and "Using Binary File Access" in this chapter.

Another way is to use either the OpenTextFile method of the FileSystemObject object with the ForWriting flag set:

Dim fso As New FileSystemObject, ts As New TextStream
Set ts = fso.OpenTextFile("c:\test.txt", ForWriting)

Or you can use the OpenAsTextStream method with the ForWriting flag set:

Dim fso As New FileSystemObject, fil As File, ts As TextStream
Set fso = CreateObject("Scripting.FileSystemObject")
fso.CreateTextFile ("test1.txt")
Set fil = fso.GetFile("test1.txt")
Set ts = fil.OpenAsTextStream(ForWriting)

Adding Data to the File

Once the text file is created, you can add data to it in three steps:

  1. Open the text file for the writing of data.

  2. Write the data.

  3. Close the file.

To open the file, you can use either of two methods: the OpenAsTextStream method of the File object, or the OpenTextFile method of the FileSystemObject object.

To write data to the open text file, use either the Write or WriteLine methods of the TextStream object. The only difference between Write and WriteLine is that WriteLine adds newline characters to the end of the specified string.

If you want to add a newline to the text file, use the WriteBlankLines method.

To close an open file, use the Close method of the TextStream object.

Here's an example of how to open a file, use all three write methods to add data to the file, then close the file:

Sub Create_File()
    Dim fso, txtfile
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set txtfile = fso.CreateTextFile("c:\testfile.txt", True)
    txtfile.Write ("This is a test. ") ' Write a line.
    ' Write a line with a newline character.
    txtfile.WriteLine("Testing 1, 2, 3.")
    ' Write three newline characters to the file.
    txtfile.WriteBlankLines(3)
    txtfile.Close
End Sub

Reading Files with File System Objects

To read data from a text file, use the Read, ReadLine, or ReadAll methods of the TextStream object:

Task | Method
Read a specified number of characters from a file | Read
Read an entire line (up to, but not including, the newline character) | ReadLine
Read the entire contents of a text file | ReadAll

If you use the Read or ReadLine method and you want to skip to a particular portion of data, you can use the Skip or SkipLine method.

The resulting text of the read methods is stored in a string which can be displayed in a control, parsed by string operators (such as Left, Right, and Mid), concatenated, and so forth.

Note The vbNewLine constant contains a character or characters (depending on the operating system) to advance the cursor to the beginning of the next line (carriage-return/linefeed). Be aware that the ends of some strings may have such nonprinting characters.

Example

Sub Read_Files()
    Dim fso As New FileSystemObject, txtfile, _
      fil1 As File, ts As TextStream
    Set txtfile = fso.CreateTextFile("c:\testfile.txt", True)
    MsgBox "Writing file"
    ' Write a line.
    Set fil1 = fso.GetFile("c:\testfile.txt")
    Set ts = fil1.OpenAsTextStream(ForWriting)
    ts.Write "Hello World"
    ts.Close
    ' Read the contents of the file.
    Set ts = fil1.OpenAsTextStream(ForReading)
    s = ts.ReadLine
    MsgBox s
    ts.Close
End Sub

Moving, Copying, and Deleting Files

The FSO object model has two methods each for moving, copying, and deleting files:

Task | Method
Move a file | File.Move or FileSystemObject.MoveFile
Copy a file | File.Copy or FileSystemObject.CopyFile
Delete a file | File.Delete or FileSystemObject.DeleteFile

Example

This example creates a text file in the root directory of drive C, writes some information to it, moves it to a directory called \tmp, makes a copy of it in a directory called \temp, then deletes the copies from both directories.

To run this example, make sure that you have directories named \tmp and \temp in the root directory of drive C.

Sub Manip_Files()
    Dim fso as New FileSystemObject, txtfile, fil1, fil2
    Set txtfile = fso.CreateTextFile("c:\testfile.txt", True)
    MsgBox "Writing file"
    ' Write a line.
    txtfile.Write ("This is a test.")
    ' Close the file to writing.
    txtfile.Close
    MsgBox "Moving file to c:\tmp"
    ' Get a handle to the file in root of C:\.
    Set fil1 = fso.GetFile("c:\testfile.txt")
    ' Move the file to \tmp directory.
    fil1.Move ("c:\tmp\testfile.txt")
    MsgBox "Copying file to c:\temp"
    ' Copy the file to \temp.
    fil1.Copy ("c:\temp\testfile.txt")
    MsgBox "Deleting files"
    ' Get handles to files' current location.
    Set fil1 = fso.GetFile("c:\tmp\testfile.txt")
    Set fil2 = fso.GetFile("c:\temp\testfile.txt")
    ' Delete the files.
    fil1.Delete
    fil2.Delete
    MsgBox "All done!"
End Sub


Processing Files with Older File I/O Statements and Functions

Ever since the first version of Visual Basic, files have been processed using the Open statement and other related statements and functions (listed below). These mechanisms will eventually be phased out in favor of the FSO object model, but they are fully supported in Visual Basic 6.0.

If you can design your application to use database files, you will not need to provide direct file access in your application. The data control and bound controls let you read and write data to and from a database, which is much easier than using direct file-access techniques.

However, there are times when you need to read and write to files other than databases. This set of topics shows how to process files directly to create, manipulate, and store text and other data.

File Access Types

By itself, a file consists of nothing more than a series of related bytes located on a disk. When your application accesses a file, it must assume what the bytes are supposed to represent (characters, data records, integers, strings, and so on).

Depending upon what kind of data the file contains, you use the appropriate file access type. In Visual Basic, there are three types of file access:

  • Sequential — For reading and writing text files in continuous blocks.

  • Random — For reading and writing text or binary files structured as fixed-length records.

  • Binary — For reading and writing arbitrarily structured files.

Sequential access is designed for use with plain text files. Each character in the file is assumed to represent either a text character or a text formatting sequence, such as a newline character (NL). Data is stored as ANSI characters. It is assumed that a file opened for random access is composed of a set of identical-length records. You can employ user-defined types to create records made up of numerous fields — each can have different data types. Data is stored as binary information.

Binary access allows you to use files to store data however you want. It is similar to random access, except there are no assumptions made about data type or record length. However, you must know precisely how the data was written to the file to retrieve it correctly.

For More Information To learn more about file access types, see "Using Sequential File Access," "Using Random File Access," and "Using Binary File Access."

File Access Functions and Statements

The following functions are used with all three types of file access:

Dir | FileLen | LOF
EOF | FreeFile | Seek
FileCopy | GetAttr | SetAttr
FileDateTime | Loc |

The following table lists all of the file access statements and functions available for each of the three types of direct file access.

Statements & Functions | Sequential | Random | Binary
Close | X | X | X
Get |  | X | X
Input( ) | X |  | X
Input # | X |  |
Line Input # | X |  |
Open | X | X | X
Print # | X |  |
Put |  | X | X
Type...End Type |  | X |
Write # | X |  |

For More Information For additional information on file access functions and statements, look up the function or statement topic in the index.


Using Sequential File Access

It is recommended that you use File System Objects to create text files, but this information is provided in case you need to use the older text file creation methods.

Sequential access works best when you want to process files consisting only of text, such as the files created with a typical text editor — that is, files in which data is not divided into a series of records. Sequential access may not be well suited for storing long series of numbers, because each number is stored as a character string. A four-digit number would require 4 bytes of storage instead of the 2 bytes it requires to store the same number as an integer.

Opening Files for Sequential Access

When you open a file for sequential access, you open it to perform one of the following operations:

  • Input characters from a file (Input)

  • Output characters to a file (Output)

  • Append characters to a file (Append)

To open a file for sequential access, use the following syntax for the Open statement:

Open pathname For [Input | Output | Append] As filenumber [Len = buffersize]

When you open a sequential file for Input, the file must already exist; otherwise, an error occurs. When you try to open a nonexistent file for Output or Append, however, the Open statement creates the file first and then opens it.

The optional Len argument specifies the number of characters to buffer when copying data between the file and your program.

After opening a file for an Input, Output, or Append operation, you must close it, using the Close statement, before reopening it for another type of operation.

Editing Files Opened for Sequential Access

If you want to edit a file, first read its contents to program variables, then change the variables, and finally, write the variables back to the file. The following sections discuss how to edit records opened for sequential access.

Reading Strings from Files

To retrieve the contents of a text file, open the file for sequential Input. Then use the Line Input #, Input( ), or Input # statement to copy the file into program variables.

Visual Basic provides statements and functions that will read and write sequential files one character at a time or one line at a time.

For example, the following code fragment reads a file line by line:

Dim LinesFromFile, NextLine As String

Do Until EOF(FileNum)
   Line Input #FileNum, NextLine
   LinesFromFile = LinesFromFile + NextLine + Chr(13) + Chr(10)
Loop

Although Line Input # recognizes the end of a line when it comes to the carriage return–linefeed sequence, it does not include the carriage return–linefeed when it reads the line into the variable. If you want to retain the carriage return–linefeed, your code must add it.

You can also use the Input # statement, which reads a list of numbers and/or string expressions written to the file. For example, to read in a line from a mailing list file, you might use the following statement:

Input #FileNum, name, street, city, state, zip

You can use the Input function to copy any number of characters from a file to a variable, provided the variable is large enough. For example, the following code uses Input to copy the specified number of characters to a variable:

LinesFromFile = Input(n, FileNum)

To copy an entire file to a variable, use the InputB function to copy bytes from a file to a variable. Since the InputB function returns an ANSI string, you must use the StrConv function to convert the ANSI string to a UNICODE string as follows:

LinesFromFile = StrConv(InputB(LOF(FileNum), FileNum), vbUnicode)

Writing Strings to Files

To store the contents of variables in a sequential file, open it for sequential Output or Append, and then use the Print # statement. For example, a text editor might use the following line of code to copy the contents of a text box into a file:

Print #FileNum, TheBox.Text

Visual Basic also supports the Write # statement, which writes a list of numbers and/or string expressions to a file. It automatically separates each expression with a comma and puts quotation marks around string expressions:

Dim AnyString As String, AnyNumber As Integer

AnyString = "AnyCharacters"
AnyNumber = 23445
Write #FileNum, AnyString, AnyNumber

This code segment writes two expressions to the file specified by FileNum. The first contains a string and the second contains the number 23445. Therefore, Visual Basic writes the following characters (including all punctuation) to the file:

"AnyCharacters",23445

Note If you are using Write # and Input # with sequential access, consider using random or binary access instead, because they are better suited to record-oriented data.

For More Information For additional information on sequential file access, see "Open Statement."


Using Random File Access

The File System Object model does not provide random file creation or access methods. If you need to create or read random files, this information will help you do so.

The bytes in random-access files form identical records, each containing one or more fields. A record with one field corresponds to any standard type, such as an integer or fixed-length string. A record with more than one field corresponds to a user-defined type. For example, the Worker Type defined below creates 19-byte records that consist of three fields:

Type Worker
   LastName As String * 10
   Title    As String * 7
   Rank     As String * 2
End Type

Declaring Variables

Before your application opens a file for random access, it should declare all variables required to handle data from the file. This includes user-defined types, which correspond to records in the file, as well as standard types for other variables that hold data related to processing a file opened for random access.

Defining Record Types

Before opening a file for random access, define a type that corresponds to the records the file does or will contain. For example, an Employee Records file could declare a user-defined data type called Person as follows:

Type Person
   ID               As Integer
   MonthlySalary      As Currency
   LastReviewDate      As Long
   FirstName         As String * 15
   LastName            As String * 15
   Title            As String * 15
   ReviewComments      As String * 150
End Type

Declaring Field Variables in a Type Definition

Because all records in a random-access file must have the same length, it is often useful for string elements in a user-defined type to have a fixed length, as shown in the Person type declaration above, where, for instance, FirstName and LastName have a fixed length of 15 characters.

If the actual string contains fewer characters than the fixed length of the string element to which it is written, Visual Basic fills the trailing spaces in the record with blanks (character code 32). Also, if the string is longer than the field size, it is truncated. If you use variable-length strings, the total size of any record stored with Put or retrieved with Get must not exceed the record length specified in the Open statement’s Len clause.

Declaring Other Variables

After defining a type that corresponds to a typical record, declare any other variables that your application needs to process a file opened for random access. For example:

' A record variable.
Public Employee As Person
' Tracks the current record.
Public Position As Long
' The number of the last record in the file.
Public LastRecord As Long

Opening Files for Random Access

To open a file for random access, use the following syntax for the Open statement:

Open pathname [For Random] As filenumber Len = reclength

Because Random is the default access type, the For Random keywords are optional.

The expression Len = reclength specifies the size of each record in bytes. Note that every string variable in Visual Basic stores a Unicode string and that you must specify the byte length of that Unicode string. If reclength is less than the actual length of the record written to the file, an error is generated. If reclength is greater than the actual length of the record, the record is written, although some disk space may be wasted.

You could use the following code to open a file:

Dim FileNum As Integer, RecLength As Long, Employee As Person

' Calculate the length of each record.
RecLength = LenB(Employee)
' Get the next available file number.
FileNum = FreeFile
' Open the new file with the Open statement.
Open "MYFILE.FIL" For Random As FileNum Len = RecLength

Editing Files Opened for Random Access

If you want to edit a random access file, first read records from the file into program variables, then change the values in the variables, and finally, write the variables back into the file. The following sections discuss how to edit files opened for random access.

Reading Records into Variables

Use the Get statement to copy records into variables. For instance, to copy a record from the Employee Records file into the Employee variable, you could use the following code:

Get FileNum, Position, Employee

In this line of code, FileNum contains the number that the Open statement used to open the file; Position contains the record number of the record to copy; and Employee, declared as user-defined type Person, receives the contents of the record.

Writing Variables to Records

Use the Put statement to add or replace records into files opened for random access.

Replacing Records

To replace records, use a Put statement, specifying the position of the record you want to replace; for example:

Put #FileNum, Position, Employee

This code will replace the record number specified by Position, with the data in the Employee variable.

Adding Records

To add new records to the end of a file opened for random access, use the Put statement shown in the preceding code fragment. Set the value of the Position variable equal to one more than the number of records in the file. For example, to add a record to a file that contains five records, set Position equal to 6.

The following statement adds a record to the end of the file:

LastRecord = LastRecord + 1
Put #FileNum, LastRecord, Employee

Deleting Records

You could delete a record by clearing its fields, but the record would still exist in the file. Usually you don’t want empty records in your file, because they waste space and interfere with sequential operations. It is better to copy the remaining records to a new file, and then delete the old file.

To remove a deleted record in a random-access file

  1. Create a new file.

  2. Copy all the valid records from the original file into the new file.

  3. Close the original file and use the Kill statement to delete it.

  4. Use the Name statement to rename the new file with the name of the original file.

For More Information For additional information on random file access, see "Open Statement."


Using Binary File Access

The File System Object model does not provide binary file creation or access methods. If you need to create or read binary files, this information will help you do so.

Binary access gives you complete control over a file, because the bytes in the file can represent anything. For example, you can conserve disk space by building variable-length records. Use binary access when it is important to keep file size small.

Note When writing binary data to a file, use a variable that is an array of the Byte data type, instead of a String variable. Strings are assumed to contain characters, and binary data may not be properly stored in String variables.

Opening a File for Binary Access

To open a file for binary access, use the following syntax for the Open statement:

Open pathname For Binary As filenumber

As you can see, Open for binary access differs from Open for random access in that Len = reclength is not specified. If you include a record length in a binary-access Open statement, it is ignored.

Storing Information in Variable-Length Fields

To best appreciate binary access, consider a hypothetical Employee Records file. This file uses fixed-length records and fields to store information about employees.

Type Person
   ID               As Integer
   MonthlySalary      As Currency
   LastReviewDate      As Long
   FirstName         As String * 15
   LastName            As String * 15
   Title            As String * 15
   ReviewComments      As String * 150
End Type

Regardless of the actual contents of the fields, every record in that file takes 209 bytes.

You can minimize the use of disk space by using binary access. Because this doesn’t require fixed-length fields, the type declaration can omit the string length parameters.

Type Person
   ID               As Integer
   MonthlySalary      As Currency
   LastReviewDate      As Long
   FirstName         As String
   LastName            As String
   Title            As String
   ReviewComments      As String
End Type

Public Empl As Person      ' Defines a record.

Each employee record in the Employee Records file now stores only the exact number of bytes required because the fields are variable-length. The drawback to binary input/output with variable-length fields is that you can’t access records randomly — you must access records sequentially to learn the length of each record. You can seek directly to a specified byte position in a file, but there is no direct way to know which record is at which byte position if the records are of variable length.

For More Information For additional information on binary file access, see "Open Statement."


Designing for Performance and Compatibility

In an ideal world, every user of your applications would have a computer with the fastest possible processor, plenty of memory, unlimited drive space, and a blazingly fast network connection. Reality dictates that for most users, the actual performance of an application will be constrained by one or more of the above factors. As you create larger and more sophisticated applications, the amount of memory the applications consume and the speed with which they execute become more significant. You may decide you need to optimize your application by making it smaller and by speeding calculations and displays.

As you design and code your application, there are various techniques that can be used to optimize the performance. Some techniques can help to make your application faster; others can help to make it smaller. In this chapter you will learn some of the more common optimization tricks that you can use in your own applications.

Visual Basic shares most of its language features with Visual Basic for Applications, which is included in Microsoft Office and many other applications. Visual Basic, Scripting Edition (VBScript), a language for Internet scripting, is also a subset of the Visual Basic language. If you’re also developing in Visual Basic for Applications or VBScript, you’ll probably want to share some of your code between these languages.

This chapter discusses the differences between the three versions of the Visual Basic language and provides some tips for creating portable code.

Contents

  • Understanding Optimization

  • Optimizing for Speed

  • Optimizing for Size

  • Optimizing Objects

  • Compiled vs. Interpreted Applications

  • Compatibility with Other Microsoft Applications

Sample Application: Optimize.vbp

Many of the optimization techniques in this chapter are illustrated in the Optimize.vbp sample application which is listed in the Samples directory.

Topics

Understanding Optimization

An introduction to optimization.
Optimizing for Speed

Techniques for making your application more efficient.
Optimizing for Size

Techniques for reducing the size of your application in memory and on disk.
Optimizing Objects

Techniques for optimizing the use of objects in your application.
Compiled vs. Interpreted Applications

A discussion of the pros and cons of native-code executables.
Compatibility with Other Microsoft Applications

Information on sharing code between versions of Visual Basic.

Sample application

Optimize.vbp

Many of the optimization techniques in this chapter are illustrated in the Optimize.vbp sample application which is listed in the Samples directory.


Understanding Optimization

Optimization could be thought of as both a science and an art. The science is the techniques of optimization; the art is determining where and when optimizations should be applied. By definition, optimization is "the process of producing more efficient (smaller and/or faster) programs through selection and design of data structures, algorithms, and instruction sequences."

It is a common misconception that optimization is process that takes place at the end of the development cycle. To create a truly optimized application, you must be optimizing it while you are developing it. You choose your algorithms carefully, weighing speed against size and other constraints; you form hypotheses about what parts of your application will be fast or slow, large or compact; and you test those hypotheses as you go.

The first step in the process of optimization is determining your goal. You can optimize your program for many different characteristics:

  • Real speed (how fast your application actually calculates or performs other operations).

  • Display speed (how fast your application paints the screen).

  • Perceived speed (how fast your application appears to run; this is often related to display speed but not always to real speed).

  • Size in memory.

  • Size of graphics (this directly affects size in memory, but often has additional ramifications when working in Microsoft Windows).

Rarely, however, can you optimize for multiple characteristics. Typically, an approach that optimizes size compromises on speed; likewise, an application that is optimized for speed is often larger than its slower counterpart. For this reason, recommended optimization techniques in one area may directly contradict suggestions in another.

It’s important to note that optimization is not always completely beneficial. Sometimes the changes you make to speed up or trim down your application result in code that is harder to maintain or debug. Some optimization techniques contradict structured coding practice, which may cause problems when you try to expand your application in the future or incorporate it into other programs.

In designing an optimization strategy for your application there are three things to consider: knowing what to optimize, knowing where to optimize, and knowing when to stop.

Knowing What to Optimize: Understanding the Real Problem

If you don’t start with a clear goal in mind, you can waste a lot of time optimizing the wrong things. Your goal should be based on the needs and expectations of the user. For example, speed might be a major concern for calculating sales tax in a point-of-sale application, whereas application size would be most important for an application that will be downloaded via the Internet. The key to developing a good optimization strategy is to understand the real problem that the optimization will address.

Although your optimization strategy will target a specific goal, it helps to think about optimization throughout the development process. When writing code, you can learn a lot by simply stepping through your code and thinking carefully about what's actually happening. You may forget that setting properties causes events to occur, and if there is a lot of code in those event procedures, an innocuous line of code can cause a tremendous delay in your program. Even if your primary goal is size, speed optimizations can sometimes be implemented without adding to code size.

Knowing Where to Optimize: Maximum Benefit with Minimum Effort

If you’re like most developers, you can’t afford the time to optimize everything in your application. It’s sometimes useful to think of having an "optimization budget." After all, added time equates to added development cost. Where can you spend your time to get a maximum return on your investment? Obviously you want to focus on the areas that seem to be the slowest or fattest, but to maximize the results of your efforts, you want to concentrate on code where a little work will make a lot of difference.

For example, if speed is your primary goal, the bodies of loops are usually a good place to start. Whenever you speed up the operations inside a loop, that improvement is multiplied by the number of times the loop is executed. For loops with a large number of iterations, just one less string operation in the body can make a big difference. The same principle applies to frequently called subroutines as well.

Knowing When to Stop: Weighing the Results

Sometimes things aren't worth optimizing. For example, writing an elaborate but fast sorting routine is pointless if you're only sorting a dozen items. It’s possible to sort things by adding them to a sorted list box and then reading them back out in order. With large numbers of items this is horribly inefficient, but if there aren't a lot of items it is just as quick as any other method, and the code is admirably simple (if a bit obscure).

There are other cases where optimization is wasted effort. If your application is ultimately bound by the speed of your disk or network, there is little you can do in your code to speed things up. Instead you need to think about ways to make these delays less problematic for your users: progress bars to tell them your code isn't simply hung, caching data so they see the delays less often, yielding so that they can use other programs while they wait, and so on.

For More Information See "Interrupting Background Processing" in "Responding to Mouse and Keyboard Events."


Optimizing for Speed

Speed is often a major determining factor in a user’s overall impression of and satisfaction with an application. Unfortunately, many of the things that influence the speed of an application are beyond your control as a programmer: the speed of the processor, the lack of adequate memory, or the speed of data connections. For this reason, it’s often necessary to optimize your application so that it will run faster (or at least appear to run faster).

Optimizations for speed can be divided into three general categories: real speed (the actual time spent performing calculations and executing code), display speed (the time spent displaying graphics or painting the screen), and perceived speed (how fast your application appears to run). The types of optimizations that you will actually use depend on the type and purpose of the application — not all optimizations are appropriate or beneficial in all cases.

As with any type of optimization, you need to weigh the potential benefit against the cost. It doesn’t make much sense to spend hours optimizing a routine that is rarely called. Determine the areas where speed improvements will affect (and be noticed by) the most users, such as the initial load time for the application.

To learn more about speed optimizations, see the following topics:

  • Optimizing Code Coding practices that will make your application run faster.

  • Optimizing Display Speed Tips for improving graphics performance.

  • Optimizing Perceived Speed Making your application appear to run faster.

  • Measuring Performance Testing and verifying your optimizations.


Optimizing Code

Unless you're doing tasks like generating fractals, your applications are unlikely to be limited by the actual processing speed of your code. Typically other factors — such as video speed, network delays, or disk activities — are the limiting factor in your applications. For example, when a form is slow to load, the cause might be the number of controls and graphics on the form rather than slow code in the Form_Load event. However, you may find points in your program where the speed of your code is the gating factor, especially for routines that are called frequently. When that's the case, there are several techniques you can use to increase the real speed of your applications:

  • Avoid using Variant variables.

  • Use Long integer variables and integer math.

  • Cache frequently used properties in variables.

  • Use module-level variables instead of Static variables

  • Replace procedure calls with inline code.

  • Use constants whenever possible.

  • Pass arguments with ByVal instead of ByRef.

  • Use typed optional arguments.

  • Take advantage of collections.

Even if you’re not optimizing your code for speed, it helps to be aware of these techniques and their underlying principles. If you get in the habit of choosing more efficient algorithms as you code, the incremental gains can add up to a noticeable overall improvement in speed.

Avoid Using Variant Variables

The default data type in Visual Basic is Variant. This is handy for beginning programmers and for applications where processing speed is not an issue. If you are trying to optimize the real speed of your application, however, you should avoid Variant variables. Because Visual Basic converts Variants to the appropriate data type at run time, operations involving other simple data types eliminate this extra step and are faster than their Variant equivalents.

A good way to avoid Variants is to use the Option Explicit statement, which forces you to declare all your variables. To use Option Explicit, check the Require Variable Declaration check box on the Editor tab of the Options dialog box, available from the Tools menu.

Be careful when declaring multiple variables: If you don’t use the As type clause, they will actually be declared as Variants. For example, in the following declaration, X and Y are variants:

Dim X, Y, Z As Long

Rewritten, all three variables are Longs:

Dim X As Long, Y As Long, Z As Long

For More Information To learn more about Visual Basic data types, see "Data Types" in "Programming Fundamentals."

Use Long Integer Variables and Integer Math

For arithmetic operations avoid Currency, Single, and Double variables. Use Long integer variables whenever you can, particularly in loops. The Long integer is the 32-bit CPU's native data type, so operations on them are very fast; if you can’t use the Long variable, Integer or Byte data types are the next best choice. In many cases, you can use Long integers when a floating-point value might otherwise be required. For example, if you always set the ScaleMode property of all your forms and picture controls to either twips or pixels, you can use Long integers for all the size and position values for controls and graphics methods.

When performing division, use the integer division operator (\) if you don’t need a decimal result. Integer math is always faster than floating-point math because it doesn’t require the offloading of the operation to a math coprocessor. If you do need to do math with decimal values, the Double data type is faster than the Currency data type.

The following table ranks the numeric data types by calculation speed.

Numeric data types | Speed
Long | Fastest
Integer |
Byte |
Single |
Double |
Currency | Slowest

Cache Frequently Used Properties in Variables

You can get and set the value of variables faster than those of properties. If you are getting the value of a property frequently (such as in a loop), your code runs faster if you assign the property to a variable outside the loop and then use the variable instead of the property. Variables are generally 10 to 20 times faster than properties of the same type.

Never get the value of any given property more than once in a procedure unless you know the value has changed. Instead, assign the value of the property to a variable and use the variable in all subsequent code. For example, code like this is very slow:

For i = 0 To 10
   picIcon(i).Left = picPallete.Left
Next I

Rewritten, this code is much faster:

picLeft = picPallete.Left
For i = 0 To 10
      picIcon(i).Left = picLeft
Next I

Likewise, code like this . . .

Do Until EOF(F)
   Line Input #F, nextLine
   Text1.Text = Text1.Text + nextLine
Loop

. . . is much slower than this:

Do Until EOF(F)
   Line Input #F, nextLine
   bufferVar = bufferVar & nextLine & vbCrLf
Loop
Text1.Text = bufferVar

However, this code does the equivalent job and is even faster:

   Text1.Text = Input(F, LOF(F))

As you can see, there are several methods for accomplishing the same task; the best algorithm is also the best optimization.

This same technique can be applied to return values from functions. Caching function return values avoids frequent calls to the run-time dynamic-link library (DLL), Msvbvm60.dll.

Use Module-level Variables Instead of Static Variables

While variables declared as Static are useful for storing a value over multiple executions of a procedure, they are slower than local variables. By storing the same value in a module-level variable your procedure will execute faster. Note, however, that you will need to make sure that only one procedure is allowed to change the module-level variable. The tradeoff here is that your code will be less readable and harder to maintain.

Replace Procedure Calls with Inline Code

Although using procedures makes your code more modular, performing each procedure call always involves some additional work and time. If you have a loop that calls a procedure many times, you can eliminate this overhead by removing the procedure call and placing the body of the procedure directly within the loop. If you place the same code inline in several loops, however, the duplicate code increases the size of your application. It also increases the chances that you may not remember to update each section of duplicate code when you make changes.

Likewise, calling a procedure that resides in the same module is faster than calling the same module in a separate .BAS module; if the same procedure needs to be called from multiple modules this gain will be negated.

Use Constants Whenever Possible

Using constants makes your application run faster. Constants also make your code more readable and easier to maintain. If there are strings or numbers in your code that don’t change, declare them as constants. Constants are resolved once when your program is compiled, with the appropriate value written into the code. With variables, however, each time the application runs and finds a variable, it needs to get the current value of the variable.

Whenever possible, use the intrinsic constants listed in the Object Browser rather than creating your own. You don’t need to worry about including modules that contain unused constants in your application; when you make an .exe file, unused constants are removed.

Pass Unmodified Arguments with ByVal Instead of ByRef

When writing Sub or Function procedures that include unmodified arguments, it is faster to pass the arguments by value (ByVal) than to pass them by reference (ByRef). Arguments in Visual Basic are ByRef by default, but relatively few procedures actually modify the values of their arguments. If you don’t need to modify the arguments within the procedure, define the them as ByVal, as in the following example:

Private Sub DoSomething(ByVal strName As String, _
ByVal intAge As Integer)

Use Typed Optional Arguments

Typed optional arguments can improve the speed of your Sub or Function calls. In prior versions of Visual Basic, optional arguments had to be Variants. If your procedure had ByVal arguments, as in the following example, the 16 bytes of the Variant would be placed on the stack.

Private Sub DoSomething(ByVal strName As String, _
Optional ByVal vntAge As Variant, _
Optional ByVal vntWeight As Variant)

Your function uses less stack space per call, and less data is moved in memory, if you use typed optional arguments:

Private Sub DoSomething(ByVal strName As String, _
Optional ByVal intAge As Integer, _
Optional ByVal intWeight As Integer)

The typed optional arguments are faster to access than Variants, and as a bonus, you'll get a compile-time error message if you supply information of the wrong data type.

Take Advantage of Collections

The ability to define and use collections of objects is a powerful feature of Visual Basic. While collections can be very useful, for the best performance you need to use them correctly:

  • Use For Each...Next rather than For...Next.

  • Avoid using Before and After arguments when adding objects to a collection.

  • Use keyed collections rather than arrays for groups of objects of the same type.

Collections allow you to iterate through them using an integer For...Next loop. However, the For Each...Next construct is more readable and in many cases faster. The For Each...Next iteration is implemented by the creator of the collection, so the actual speed will vary from one collection object to the next. However, For Each...Next will rarely be slower than For...Next because the simplest implementation is a linear For...Next style iteration. In some cases the implementor may use a more sophisticated implementation than linear iteration, so For Each...Next can be much faster.

It is quicker to add objects to a collection if you don't use the Before and After arguments. Those arguments require Visual Basic to find another object in the collection before it can add the new object.

When you have a group of objects of the same type, you can usually choose to manage them in a collection or an array (if they are of differing types, a collection is your only choice). From a speed standpoint, which approach you should choose depends on how you plan to access the objects. If you can associate a unique key with each object, then a collection is the fastest choice. Using a key to retrieve an object from a collection is faster than traversing an array sequentially. However, if you do not have keys and therefore will always have to traverse the objects, an array is the better choice. Arrays are faster to traverse sequentially than collections.

For small numbers of objects, arrays use less memory and can often be searched more quickly. The actual number where collections become more efficient than arrays is around 100 objects; however, this can vary depending on processor speed and available memory.

For More Information See "Using Collections as an Alternative to Arrays" in "More About Programming."


Measuring Performance

Determining the best algorithm for a given situation isn’t always obvious. Sometimes you’ll want to test your hypotheses; this can be easily done by creating a simple application to measure performance, as shown below. The Optimize.vbp sample application also contains examples of several different test scenarios.

To create a performance testing application

  1. Open a new .exe project.

  2. Create a form with two command buttons: Command1 and Command2.

  3. In the Command1_Click Event add the following code:
    Private Sub Command1_Click()
Dim dblStart As Double
Dim dblEnd As Double
Dim i as Long

dblStart = Timer        ' Get the start time.

For i = 0 To 9999
Routine to test    ' Enter your routine here.
Next

dblEnd = Timer            ' Get the end time.

Debug.Print dblEnd - dblStart    ' Display the
' elapsed time.
End Sub
  4. Add the same code to the Command2_Click event, substituting the second version of your routine inside the loop.

  5. Run the application and monitor the results in the Immediate window.

This example uses the default property of Visual Basic’s Timer class to time the execution of the routine within the loop. By placing your code inside the loop for each command button, you can quickly compare the performance of two algorithms. The code can be within the loop or can be a call to other procedures.

You may need to experiment with different values for the upper bounds of the loop counter, especially for fast routines. Make sure that you run each version several times to get an average; results can vary from one run to the next.

You can also optimize your application by increasing data access speed.


Optimizing Display Speed

Because of the graphical nature of Microsoft Windows, the speed of graphics and other display operations is crucial to the perceived speed of the application. The faster forms appear and paint, the faster your application will seem to the user. There are several techniques you can use to speed up the apparent speed of your application, including:

  • Set the ClipControls property of containers to False.

  • Use AutoRedraw appropriately.

  • Use image controls instead of picture box controls.

  • Hide controls when setting properties to avoid multiple repaints.

  • Use Line instead of PSet.

Set the ClipControls Property of Containers to False

Unless you are using graphics methods (Line, PSet, Circle, and Print), you should set ClipControls to False for the form and for all frame and picture box controls (it may cause unpredictable results if your code includes graphics methods that draw behind other controls). When ClipControls is False, Visual Basic doesn’t overpaint controls with the background before repainting the controls themselves. On forms that contain a lot of controls, the resulting speed improvements are significant.

For More Information See "Layering Graphics with AutoRedraw and ClipControls" in "Working with Text and Graphics."

Use AutoRedraw Appropriately

When AutoRedraw is set to True for a form or control, Visual Basic maintains a bitmap to repaint that form or control. Although this improves the speed of simple repaints (for example, when the form or control is revealed after a window that covers it is removed), it slows graphics methods. Visual Basic has to perform the graphics methods on the AutoRedraw bitmap and then copy the entire bitmap to the screen. This process also consumes a considerable amount of memory.

If your application generates complex graphics but doesn’t change them frequently, setting AutoRedraw to True is appropriate. But if your application draws graphics that must change frequently, you will get better performance if you set AutoRedraw to False and perform the graphics methods for the form or control in the Paint event.

For More Information See "Layering Graphics with AutoRedraw and ClipControls" in "Working with Text and Graphics."

Use Image Controls Instead of Picture Box Controls

This optimization improves the speed and minimizes the size of your application; use it whenever possible. When you are simply displaying pictures and reacting to click events and mouse actions on them, use the image control instead of the picture box. Don’t use a picture box unless you need the capabilities only the picture box provides, such as graphics methods, the ability to contain other controls, or dynamic data exchange (DDE).

Hide Controls When Setting Properties to Avoid Multiple Repaints

Every repaint is expensive. The fewer repaints Visual Basic must perform, the faster your application will appear. One way to reduce the number of repaints is to make controls invisible while you are manipulating them. For example, suppose you want to resize several list boxes in the Resize event for the form:

Sub Form_Resize ()
Dim i As Integer, sHeight As Integer
   sHeight = ScaleHeight / 4
   For i = 0 To 3
      lstDisplay(i).Move 0, i * sHeight, _
      ScaleWidth, sHeight
   Next
End Sub

This creates four separate repaints, one for each list box. You can reduce the number of repaints by placing all the list boxes within a picture box, and hiding the picture box before you move and size the list boxes. Then, when you make the picture box visible again, all of the list boxes are painted in a single pass:

Sub Form_Resize ()
Dim i As Integer, sHeight As Integer
   picContainer.Visible = False
   picContainer.Move 0, 0, ScaleWidth, ScaleHeight
   sHeight = ScaleHeight / 4
   For i = 0 To 3
      lstDisplay(i).Move 0, i * sHeight, _
      ScaleWidth, sHeight
   Next
   picContainer.Visible = True
End Sub

Note that this example uses the Move method instead of setting the Top and Left properties. The Move method sets both properties in a single operation, saving additional repaints.

Use Line Instead of PSet

The Line method is faster than a series of PSet methods. Avoid using the PSet method and batch up the points into a single Line method. Shape and line controls are appropriate for simple graphical elements that rarely change; complex graphics, or graphics that change rapidly, are generally best handled with graphics methods.


Optimizing Perceived Speed

Often the subjective speed of your application has little to do with how quickly it actually executes its code. To the user, an application that starts up rapidly, repaints quickly, and provides continuous feedback feels "snappier" than an application that just "hangs up" while it churns through its work. You can use a variety of techniques to give your application that "snap":

  • Keep forms hidden but loaded.

  • Preload data.

  • Use timers to work in the background.

  • Use progress indicators.

  • Speed the start of your application.

Keep Forms Hidden but Loaded

Hiding forms instead of unloading them is a trick that has been around since the early days of Visual Basic 1.0, but it is still effective. The obvious downside to this technique is the amount of memory the loaded forms consume, but it can't be beat if you can afford the memory cost and making forms appear quickly is of the highest importance.

Preload Data

You can also improve the apparent speed of your application by prefetching data. For example, if you need to go to disk to load the first of several files, why not load as many of them as you can? Unless the files are extremely small, the user is going to see a delay anyway. The incremental time spent loading the additional files will probably go unnoticed, and you won't have to delay the user again.

Use Timers to Work in the Background

In some applications you can do considerable work while you are waiting for the user. The best way to accomplish this is through a timer control. Use static (or module-level) variables to keep track of your progress, and do a very small piece of work each time the timer goes off. If you keep the amount of work done in each timer event very small, users won't see any effect on the responsiveness of the application and you can prefetch data or do other things that further speed up your application.

For More Information To learn more about the timer control, see "Timer Control" in "Using Visual Basic’s Standard Controls." For a discussion of background processing, see "Interrupting Background Processing" in "Responding to Mouse and Keyboard Events."

Use Progress Indicators

When you can't avoid a long delay in your program, you need to give the user some indication that your application hasn't simply hung. Windows 95 uses a standard progress bar to indicate this to users. You can use the ProgressBar control in the Microsoft Windows Common Controls included with the Professional and Enterprise editions of Visual Basic. Use DoEvents at strategic points, particularly each time you update the value of the ProgressBar, to allow your application to repaint while the user is doing other things.

At the very least, you should display the wait cursor to indicate the delay by setting the form’s MousePointer property to vbHourglass (11).

Speed the Start of Your Application

Apparent speed is most important when your application starts. Users' first impression of the speed of an application is measured by how quickly they see something after clicking on its name in the Start menu. With the various run-time DLLs that need to be loaded for Visual Basic for Applications, ActiveX controls, and so forth, some delay is unavoidable with any application. However, there are some things you can do to give a response to the user as quickly as possible:

  • Use Show in the Form_Load event.

  • Simplify your startup form.

  • Don’t load modules you don’t need.

  • Run a small Visual Basic application at startup to preload the run-time DLLs.

Use Show in the Form_Load Event

When a form is first loaded, all of the code in the Form_Load event occurs before the form is displayed. You can alter this behavior by using the Show method in the Form_Load code, giving the user something to look at while the rest of the code in the event executes. Follow the Show method with DoEvents to ensure that the form gets painted:

Sub Form_Load()
   Me.Show            ' Display startup form.
   DoEvents            ' Ensure startup form is painted.
   Load MainForm      ' Load main application fom.
   Unload Me         ' Unload startup form.
   MainForm.Show      ' Display main form.
End Sub

Simplify Your Startup Form

The more complicated a form is, the longer it takes to load. Keep your startup form simple. Most applications for Microsoft Windows display a simple copyright screen (also known as a splash screen) at startup; your application can do the same. The fewer controls on the startup form, and the less code it contains, the quicker it will load and appear. Even if it immediately loads another, more complicated form, the user will know that the application has started.

For large applications you may want to preload the most commonly used forms at startup so that they can be shown instantly when needed. A satisfying way to do this is to display a progress bar in your startup form and update it as you load each of the other forms. Call DoEvents after loading each form so that your startup form will repaint. Once all the important forms have been loaded, the startup form can show the first one and unload itself. Of course, each form you preload will run the code in its Form_Load event, so take care that this doesn't cause problems or excessive delays.

Don’t Load Modules You Don’t Need

Visual Basic loads code modules on demand, rather than all at once at startup. This means that if you never call a procedure in a module, that module will never be loaded. Conversely, if your startup form calls procedures in several modules, then all of those modules will be loaded as your application starts up, which slows things down. You should therefore avoid calling procedures in other modules from your startup form.

Run a Small Visual Basic Application at Startup to Preload the Run-time DLLs

A large part of the time required to start a Visual Basic application is spent loading the various run-time DLLs for Visual Basic, ActiveX, and ActiveX controls. Of course, if these are already loaded, none of that time need be spent. Thus users will see your application start up faster if there is another application already running that uses some or all of these DLLs.

One way to significantly improve the startup performance of your applications is to provide another small, useful application that the user always runs. For example, you might write a small application to display a calendar and install it in the startup group for Windows. It will then load automatically on system startup, and while it is useful in itself, it also ensures that the various Visual Basic run-time DLLs are loaded.

Finally, with the Professional and Enterprise editions of Visual Basic you can divide your application into a main skeleton application and several component executables or DLLs. A smaller main application will load faster, and it can then load the other parts as needed.


Optimizing for Size

In the past, available memory and system resources were often limiting factors in designing an application. With 32-bit operating systems, such as Windows 95 and Windows NT, these factors are rarely a concern for most Visual Basic programmers. However, there are a number of scenarios where minimizing the size of an application is still important.

Size is extremely important for applications that will be downloaded from the Internet or transferred as attachments to e-mail. For those not fortunate enough to have high-speed data connections, transferring a 1-megabyte file could take an hour or more. In addition to the .exe file, many applications will require additional .dll or .ocx files, adding to the size (and time) of the download. In these scenarios, you would want to optimize your application’s size on disk.

Even if users won’t be downloading your application, it’s usually a good idea to make your application as compact as possible. Smaller applications load faster, and because they consume less memory, you can run additional applications at the same time. You can often improve performance by optimizing your application’s size in memory.

To learn more about size optimizations, see the following topics:

  • Reducing Code Size Tips for creating compact optimized code.

  • Cutting Back on Graphics Techniques for optimizing graphics.

  • Segmented Applications Using components to reduce application size.


Reducing Code Size

When reducing the size of an application is important, there are a number of techniques that you can apply to make your code more compact. In addition to reducing the application’s size in memory, most of these optimizations will also reduce the size of the .exe file. As an additional benefit, a smaller application will load faster.

Most size optimization techniques involve eliminating unnecessary elements from your code. Visual Basic automatically eliminates certain elements when you compile your application. There is no reason to restrict the length or number of the following elements:

  • Identifier names

  • Comments

  • Blank lines

None of these elements affect the size of your application in memory when it is running as an .exe file.

Other elements, such as variables, forms, and procedures, do take up space in memory. It is usually best to streamline these. There are several techniques you can use to reduce the memory your application occupies when it is running as an .exe file. These techniques can reduce code size:

  • Reduce the number of loaded forms.

  • Reduce the number of controls.

  • Use labels instead of text boxes.

  • Keep data in disk files or resources and load only when needed.

  • Organize your modules.

  • Consider alternatives to Variant data types.

  • Use dynamic arrays and erase to reclaim memory.

  • Reclaim space used by strings or object variables.

  • Eliminate dead code and unused variables.

Reduce the Number of Loaded Forms

Each loaded form, whether visible or not, consumes a significant amount of memory (which varies with the number and types of controls on the form, the size of bitmaps on the form, and so on). Load forms only when you need to display them, and unload them (rather than hide them) when you no longer need them. Remember that any reference to properties, methods, or controls on a form, or a form variable declared with New, causes Visual Basic to load the form.

When you unload a form using the Unload method, only a portion of the memory occupied by the form is released. To free all memory, invalidate the reference to the form by using the Nothing keyword:

Set Form = Nothing

Reduce the Number of Controls

When designing your application, try to place as few controls on a form as possible. The actual limit depends on the type of controls as well as available system, but in practice, any form with a large number of controls will perform slowly. A related technique is to use control arrays where possible, rather than putting a large number of controls of the same type on a form at design time.

For More Information To learn more about control arrays, see "Working with Control Arrays" in "Using Visual Basic’s Standard Controls."

Use Labels Instead of Text Boxes

Label controls use fewer Windows resources than text boxes do, so you should use labels in place of text boxes whenever possible. For example, if you need a hidden control on a form for storing text, it is more efficient to use a label.

Even a data entry form that requires numerous text fields can be optimized using this technique. You can create a label for each field and use a single text box for input, moving it to the next label's location in the LostFocus event:

Private Sub Label1_LostFocus()
   ' Update Label1
   Label1.Caption = Text1.Text
   ' Move the Textbox over the next label
   Text1.Move Label2.Left, Label2.Top
   ' Update Text1 contents
   Text1.Text = Label2.Caption
End Sub

You can make a label look like a text box by setting the BackColor and BorderStyle properties. Although this technique requires more code, it can significantly reduce resource usage for a form that contains numerous fields.

Keep Data in Disk Files or Resources and Load Only When Needed

Data you place directly into your application at design time (as properties or as literal strings and numbers in your code) increases the memory the application consumes at run time. You can reduce memory by loading the data from disk file or resources at run time. This is particularly valuable for large bitmaps and strings.

For More Information For information on adding resources to your application, see "Resource Files" in "Advanced Programming Features."

Organize Your Modules

Visual Basic loads modules on demand — that is, it loads a module into memory only when your code calls one of the procedures in that module. If you never call a procedure in a particular module, Visual Basic never loads that module. Placing related procedures in the same module causes Visual Basic to load modules only as needed.

Consider Alternatives to Variant Data Types

The Variant data type is extremely flexible, but it is also larger than any of the other data types. When you must squeeze every last byte out of your application, consider replacing Variant variables, and especially arrays of Variant variables, with other data types.

Each Variant takes 16 bytes, compared to 2 for an Integer or 8 for a Double. Variable-length String variables use 4 bytes plus 1 byte per character in the string, but each Variant containing a string takes 16 bytes plus 1 byte per character in the string. Because they are so large, Variant variables are particularly troublesome when used as local variables or arguments to procedures, because they quickly consume stack space.

In some cases, however, using other data types forces you to add more code to compensate for the loss of flexibility that the Variant data type provides, resulting in no net reduction in size.

Use Dynamic Arrays and Erase to Reclaim Memory

Consider using dynamic arrays instead of fixed arrays. When you no longer need the data in a dynamic array, use Erase or ReDim Preserve to discard unneeded data, and reclaim the memory used by the array. For example, you can reclaim the space used by a dynamic array with the following code:

Erase MyArray

Whereas Erase completely eliminates the array, ReDim Preserve makes the array smaller without losing its contents:

ReDim Preserve MyArray(10, smallernum)

Erasing a fixed-size array will not reclaim the memory for the array — it simply clears out the values of each element of the array. If each element was a string, or a Variant containing a string or array, then erasing the array would reclaim the memory from those strings or Variants, not the memory for the array itself.

Reclaim Space Used by Strings or Object Variables

The space used by (nonstatic) local string and array variables is reclaimed automatically when the procedure ends. However, global and module-level string and array variables remain in existence for as long as your program is running. If you are trying to keep your application as small as possible, you should reclaim the space used by these variables as soon as you can. You reclaim string space by assigning the zero-length string to it:

SomeStringVar = ""      ' Reclaim space.

Similarly, you can reclaim some (but not all) of the space used by an object variable by setting it to Nothing. For example, to remove a Recordset object variable:

Private rs As New RecordSet

   …                  ' Code to initialize and use recordset would go here
   rs.Close            ' Close the recordset
   Set rs = Nothing   ' Set the object reference to Nothing

If you don’t explicitly set an object reference to Nothing, a reference to the object will remain in memory until the application is terminated; for an application that uses a lot of objects this can quickly consume your available memory and slow the application.

You can also reclaim space by unloading forms and setting them to Nothing rather than simply hiding them when they are no longer needed.

Eliminate Dead Code and Unused Variables

As you develop and modify your applications, you may leave behind dead code — entire procedures that are not called from anywhere in your code. You may also have declared variables that are no longer used. Although Visual Basic does remove unused constants, it does not remove unused variables and dead code when you create an .exe. Consider reviewing your code to find and remove unused procedures and variables. For example, Debug.Print statements, while ignored in the run-time .exe, are sometimes present in the .exe file.

Debug.Print statements with strings or variables as arguments are not compiled when you create an .exe. However, where Debug.Print statements have a function call as an argument, the Debug.Print statement itself is ignored by the compiler, but the function call is compiled. Then, when the application is run, the function is called but the return is ignored. Because functions that appear as arguments to Debug.Print will take up space and cycle time in an .exe, it may be beneficial to delete these statements before you make an .exe.

Use the Find command on the Edit menu to search for references to a particular variable. Or, if you have Option Explicit statements in each of your modules, you can quickly discover if a variable is used in your application by removing or commenting out its declaration and running the application. If the variable is used, Visual Basic will generate an error. If you don’t see an error, the variable was not used.

For More Information To learn more about the Debug.Print statement, see "Printing Information in the Immediate Window" in "Debugging Your Code and Handling Errors."


Cutting Back on Graphics

Graphics (pictures and graphics methods) can consume a lot of memory. To some extent, this is unavoidable: Graphics contain a lot of information, so they tend to be large. But in many cases, you can reduce the impact that graphics have on the size of your application by applying some of the following techniques:

  • Use the image control to display bitmaps.

  • Load bitmaps from files as needed and share pictures.

  • Use the PaintPicture method.

  • Free the memory used by graphics.

  • Use rle-format bitmaps or metafiles.

Use the Image Control to Display Bitmaps

The picture controls in many Visual Basic applications exist merely to be clicked or to be dragged and dropped. If this is all you're doing with a picture control, you are wasting a lot of Windows resources. For these purposes, image controls are superior to picture controls. Each picture control is an actual window and uses significant system resources. The image control is a "lightweight" control rather than a window and doesn't use nearly as many resources. In fact, you can typically use five to 10 times as many image controls as picture controls. Moreover, image controls repaint faster than picture controls. Only use a picture controls when you need a feature only it provides, such as dynamic data exchange (DDE), graphics methods, or the ability to contain other controls.

Load Bitmaps from Files As Needed and Share Pictures

When you set a Picture property at design time, you add the picture to the form and thereby increase the memory the form consumes at run time. You can reduce memory consumption by storing pictures in a resource file and using the LoadResPicture function to load them at run time. If you never use all the pictures associated with a form at the same time, this technique saves memory over storing all the pictures in controls on the form. It can speed up form load because not all the pictures need to be loaded before the form can be shown.

You can share the same picture between multiple picture controls, image controls, and forms. If you use code like this you only maintain one copy of the picture:

Picture = LoadPicture("C:\Windows\Chess.bmp")
Image1.Picture = Picture      ' Use the same picture.
Picture1.Picture = Picture   ' Use the same picture.

Contrast that with this code, which causes three copies of the bitmap to be loaded, taking more memory and time:

Picture = LoadPicture("C:\Windows\Chess.bmp")
Image1.Picture = LoadPicture("C:\Windows\Chess.bmp")
Picture1.Picture = LoadPicture("C:\Windows\Chess.bmp")

Similarly, if you load the same picture into several forms or controls at design time, a copy of that picture is saved with each form or control. Instead, you could place the picture in one form and then share it with the other forms and controls as described above. This makes your application both smaller (because it doesn't contain redundant copies of the picture) and faster (because the picture doesn't have to be loaded from disk multiple times).

Use the PaintPicture Method

Rather than placing bitmaps in controls, you can use the PaintPicture method to display bitmaps anywhere on forms. This is particularly useful when you want to tile a bitmap repeatedly across a form: You only need to load the bitmap once, but you can use PaintPicture to draw it multiple times.

Free the Memory Used by Graphics

When you are no longer using a picture in the Picture property of a form, picture box, or image control, set the Picture property to Nothing to empty it:

Set Picture1.Picture = Nothing

If you use the Image property of a picture box or form, Visual Basic creates an AutoRedraw bitmap (even if the AutoRedraw property for that form or picture box is False). When you have finished using the Image property, you can reclaim the memory used by this bitmap by using the Cls method before setting AutoRedraw to False. For example, the following code reclaims the memory used by the Image property for a control called mypic:

mypic.AutoRedraw = True   ' Turn on AutoRedraw bitmap.
mypic.Cls                  ' Clear it.
mypic.AutoRedraw = False      ' Turn off bitmap.

Use Rle-Format Bitmaps or Metafiles

Although the default picture format is the bitmap (.bmp), Visual Basic can also utilize other graphics file formats. Several painting and graphics programs allow you to save bitmaps in a standard compressed bitmap format called Run Length Encoded (.rle). Rle bitmaps can be several times smaller than their uncompressed counterparts, particularly for bitmaps that contain large swatches of solid color, and they aren't appreciably slower to load or display. Using metafiles (.wmf) can produce even greater savings — 10 times or more in some cases. Try to use metafiles at their normal size: They are much slower to paint when they have to be stretched larger or smaller.

You can also use .gif and .jpg formats. They are generally much smaller; however there is some tradeoff in image quality and loading speed.


Segmented Applications

Visual Basic enables you to think about the architecture of your application in new ways. Instead of a single, monolithic executable, you can write an application that consists of a core front-end executable supported by a number of ActiveX components. This approach offers several significant optimization benefits:

  • The components are loaded on demand and can be unloaded when no longer needed.

  • Cross-process components can be 32-bit executables on Windows 95 or Windows NT, even if other parts of the application are 16-bit components.

  • Remote components can use the resources of other machines on the network.

In addition, the components can be debugged independently and reused in other applications. This may not improve the speed of your application, but it may improve your speed in creating the next one.

To determine how to best optimize your application by segmenting it, you must evaluate the kinds of components you can create and how they fit into your application. There are three kinds of components you can create with the Professional or Enterprise editions of Visual Basic:

  • Cross-process

  • In-process

  • Remote

These three kinds are not exclusive: You could use all three in a single application. But from the standpoint of optimizing your application, they each have very different characteristics.

For More Information Component creation is discussed in depth in the Component Tools Guide included with the Professional and Enterprise editions of Visual Basic.

Cross-Process Components

A cross-process component is an executable program that offers its services to other programs. Like all executables, it starts up and runs with its own stack in its own process space; thus, when a application acting as a client uses one of the objects provided by a component, the operation crosses from the client's process space to the component's — hence the name. Cross-process components offer some valuable features when compared to the other types:

  • Asynchronous operation ("threads").

  • Untrapped errors in the component won't cause the calling application to crash.

  • Interoperability between 16-bit and 32-bit applications.

Of these, the first and the last points are of particular interest from an optimization standpoint.

Because a cross-process component is a separate program, it can operate asynchronously with the component acting as a client. It has a separate "thread" that multitasks with the client program (technically speaking this is not a thread but a separate process; however, conceptually the two are equivalent). The two programs can communicate and share objects, but they run independently. This is particularly useful when your application needs to perform some operation that takes a long time. The client can call the component to perform the operation and then continue responding to the user.

Even if your application will run on a 32-bit system, you may not be able to make it 32-bit immediately if you rely on legacy 16-bit applications or components. However, if you segment your application using cross-process components, you can mix and match 16-bit and 32-bit components. This allows you to incrementally take advantage of 32-bit features and performance while preserving your investment in 16-bit components.

For all their strengths, cross-process components have a significant disadvantage: performance. This manifests itself in a couple of ways:

  • Startup speed

  • Cross-process call overhead

A cross-process component is an executable created with Visual Basic, so the same startup issues related to application startup also apply. The good news is that if you are calling a cross-process component written in Visual Basic from another Visual Basic program, almost all the support DLLs will already be loaded. This greatly reduces the time required to start the component. Many components are smaller than your average Visual Basic application, with few or no forms to load, which again improves load time. Nevertheless, a cross-process component will always be slower to start than an in-process component.

Once it is running, a cross-process component suffers from its very nature: Every interaction with the component is a cross-process call. Crossing process boundaries takes a lot of CPU cycles. So every reference to an object from the cross-process component is much more expensive than an equivalent reference to an object in the client application itself or an in-process component. Reducing the number of cross-process calls in your code can reduce the impact of the cross-process call overhead.

In-Process Components

An in-process component offers its services to other programs within their process space. Compared to cross-process components, in-process components offer two advantages:

  • Improved load time

  • No cross-process overhead

With an in-process component, no new process needs to be created and no run-time DLLs need to be loaded. This can make an in-process component considerably quicker to load compared to an equivalent cross-process component.

Because it is in-process, there is no cross-process overhead when referring to the methods or properties on an object supplied by the component. Objects from the component operate with the same efficiency as objects within the client application itself.

Remote Components

The Enterprise Edition of Visual Basic enables you to create remote components that execute on a separate machine elsewhere on the network. Although network overhead will inevitably exact a toll on application performance, you can make up for it by using the resources of additional CPUs. This is particularly true when you work with a remote component that is operating on data that is local to the machine containing the component. Since this data would have to be fetched across the network anyway, a component operating on it locally and returning only the results across the network can actually be more efficient.

For example, you might write an object in a component that can search for files matching a specified criteria on the local hard disk. By making this a remote component and placing a copy on each machine on the network, you could write a distributed file-finder program that searches all the network components in parallel, using all those CPU resources.


Optimizing Objects

As you use more and more objects in your Visual Basic applications, optimizing your use of those objects becomes more and more important. There are several key techniques to making the most efficient use of objects:

  • Use early binding.

  • Minimize the dots.

  • Use Set and With...End With.

  • Minimize cross-process calls.

In Visual Basic, referencing another application’s object in your code (by getting or setting an object’s property, or executing one of its methods) constitutes a cross-process call. Cross-process calls are expensive and you should try to avoid them if you are concerned about optimizing your application.

Early Binding vs. Late Binding

Visual Basic can use objects more efficiently if it can early bind them. An object can be early bound if you supply a reference to a type library containing the object, and you declare the type of the object:

Dim X As New MyObject

Or, equivalently:

Dim X As MyObject
Set X = New MyObject

Early binding enables Visual Basic to do most of the work of resolving the definition of the object at compile time rather than at run time, when it impacts performance. This also allows Visual Basic to check the syntax of properties and methods used with the object and report any errors.

If Visual Basic cannot bind an object early, it must bind it late. Late binding objects is expensive: At compile time you get no error checking, and each reference at run time requires at least 50% more work by Visual Basic.

Generally, you should always early bind objects if possible. The only times you should have to declare a variable As Object is if you do not have a type library for the object in question, or you need to be able to pass any kind of object as an argument to a procedure.

For More Information To learn more about early binding, see "Speeding Object References" in "Programming with Components."

Minimize the Dots

When referencing other applications' objects from Visual Basic, you use the dot syntax "." to navigate an object’s hierarchy of collections, objects, properties, and methods. It is not uncommon to create very lengthy navigation strings. For example:

' Refers to cell A1 on Sheet1 in the first workbook
' of an Microsoft Excel spreadsheet.
Application.Workbooks.Item(1).Worksheets.Item_
("Sheet1").Cells.Item(1,1)

In addition to being a rather lengthy string to type, this line of code is fairly difficult to read — and it is extremely inefficient.

When calling an object from Visual Basic, each "dot" requires Visual Basic to make multiple calls.

To write the most efficient applications, minimize the use of dots when referencing an object.

You can usually minimize the dots by analyzing the objects and methods available to you. For example, the above line of code can be shortened by removing the Item method (this is the default method for collections anyway, so you’ll rarely use it in code) and by using the more efficient Range method:

' Refers to cell A1 on Sheet1 in the first workbook
' of an Microsoft Excel spreadsheet.
Application.Workbooks(1).Worksheets("Sheet1")_
.Range("A1")

You can shorten this even further by rewriting the code so that it refers to the active sheet in the active workbook, instead of a specific sheet in a specific workbook:

' Refers to cell A1 on the active sheet in the
' active workbook.
Range("A1")

Of course, the above example assumes it’s OK to refer to cell A1 of any sheet that happens to be active.

Use Set and With...End With

Using the Set statement also allows you to shorten navigation strings and gives you a bit more control over your code. The following example uses the Dim and Set statements to create variables that refer to frequently used objects:

Dim xlRange As Object
Set xlRange = Application.ActiveSheet.Cells(1,1)
xlRange.Font.Bold = True
xlRange.Width = 40

Visual Basic provides the With...End With construct to set an implied object within code:

With Application.ActiveSheet.Cells(1,1)
   .Font.Bold = True
   .Width = 40
End With

Minimize Cross-Process Calls

If you are using a cross-process ActiveX component, you can't completely avoid making cross-process calls. However, there are several ways to minimize the number of cross-process calls you need to make. If possible, do not reference objects inside a For...Next loop. Cache values in variables and use the variables in loops. If you have to call a large number of methods on an object, you can greatly improve the performance of your application by moving the code into the component. For example, if the component is Word or Microsoft Excel, you can put a looping macro in a template in Word or a looping procedure into module in Microsoft Excel. You then call the macro or procedure from Visual Basic, which is a single call that launches a looping operation within the component.

If you are writing components, you can design the objects in the component to be efficient by reducing the cross-process calls required to perform an operation. For example, when you have several interrelated properties, implement a method with several arguments — one for each property. Calling the method requires a single cross-process call regardless of how many arguments it has, whereas setting each property requires a separate cross-process call. Likewise, if you anticipate that the component acting as a client will want to call your component in a loop (for example, to sum or average all the values in a list property), you can improve performance by providing methods that do the looping within your object and return the appropriate value.

For More Information Component creation is discussed in depth in the Component Tools Guide included with the Professional and Enterprise editions of Visual Basic.


Compiled vs. Interpreted Applications

By default, applications created in Visual Basic are compiled as interpreted or p-code executables. At run time, the instructions in the executables are translated or interpreted by a run-time dynamic-link library (DLL). The Professional and Enterprise editions of Visual Basic include the option to compile a native code .exe. In many cases, compiling to native code can provide substantial gains in speed over the interpreted versions of the same application; however, this is not always the case. The following are some general guidelines regarding native-code compilation.

  • Code that does a lot of primitive operations on hard-typed, nonstring variables will yield a maximum ratio of generated native code to displaced p-code operations. Complex financial calculations or fractal generation, therefore, would benefit from native code.

  • Computationally intensive programs, or programs that shuffle a lot of bits and bytes around within local data structures, will gain very visibly with native code.

  • For many programs, especially those doing a lot of Windows API calls, COM method calls, and string manipulations, native code will not be much faster than p-code.

  • Applications that consist primarily of functions from the Visual Basic for Applications run-time library are not going to see much if any advantage from native code, because the code in the Visual Basic for Applications run-time library is already highly optimized.

  • Code that involves a lot of subroutine calls relative to inline procedures is also unlikely to appear much faster with native code. This is because all the work of setting up stack frames, initializing variables, and cleaning up on exit takes the same time with both the p-code engine and generated native code.

Note that any calls to objects, DLLs or Visual Basic for Applications run-time functions will negate the performance benefits of native code. This is because relatively little time is spent executing code — the majority of time (usually around 90–95%) is spent inside forms, data objects, Windows .dlls, or the Visual Basic for Applications run time, including intrinsic string and variant handling.

In real-world tests, client applications typically spent about 5% of their total execution time executing the p-code. Hence, if native code was instantaneous, using native code for these programs would provide at most a 5% performance improvement.

What native code does is to enable programmers to write snippets of code or computationally intensive algorithms in Basic that were never possible before because of performance issues. Enabling these "snippets" to run much faster can also improve the responsiveness of certain portions of an application, which improves the perceived performance of the overall application.

For More Information To learn more about native-code compilation, see "Compiling Your Project to Native Code" in "More About Programming."


Compatibility with Other Microsoft Applications

Visual Basic is the senior member of the family of Visual Basic products that includes Visual Basic for Applications and Visual Basic, Scripting Edition (VBScript). While most of the code that you write in Visual Basic can be shared with applications written in Visual Basic for Applications or VBScript, there are some exceptions.

Compatibility with Visual Basic for Applications

Visual Basic for Applications is a single, common application scripting language and environment that users and developers can leverage across their Windows desktop. Visual Basic for Applications is included in Microsoft Office and other Microsoft applications. It is also licensed to other software vendors and included in a wide range of products.

Visual Basic for Applications, contained in Vba6.dll, is the underlying language engine for Visual Basic. This library contains all of the language elements that are shared by Visual Basic for Applications and Visual Basic. You can view the elements by selecting VBA from the Library listbox in the Object Browser. Code written in Visual Basic for Applications is portable to Visual Basic with the following limitations: Visual Basic for Applications code that refers to application-specific elements (such as an Microsoft Excel worksheet) may be ported, provided that they contain a fully qualified reference and provided that the referenced application exists on the target machine.

Elements specific to Visual Basic, such as forms and intrinsic controls, are contained in the type library Vb6.olb (which is also visible in the Object Browser). In general, code written in Visual Basic is portable to Visual Basic for Applications as long as it doesn’t reference these elements.

For More Information To learn more about Visual Basic for Applications, visit the Microsoft Web site at http://www.microsoft.com.To learn more about referencing objects, see "Creating a Reference to an Object" in "Programming with Components." To learn more about native-code compilation, see "Compiling Your Project to Native Code" in "More About Programming."

Compatibility with Visual Basic Scripting Edition

Visual Basic Scripting edition (VBScript) is designed to be hosted within an Internet browser, such as the Microsoft Internet Explorer or other, third-party browsers. VBScript is a lightweight and extremely fast language engine designed specifically for environments like the Internet, intranets, or the World Wide Web. VBScript leverages the strengths of Visual Basic and enables developers to use their Visual Basic development knowledge to quickly create solutions for the Internet or World Wide Web.

VBScript supports a subset of the Visual Basic for Applications language syntax. Visual Basic Scripting edition does not include an IDE like that found in Microsoft Visual Basic, because it is designed to be a lightweight language engine that can be shared across different platforms. You can write VBScript code in Visual Basic’s Code Editor, but you can’t run or test the application in the Visual Basic IDE.

Because VBScript is a cross-platform development language, some of the elements of the Visual Basic for Applications language are not included. These include any file input/output functions, intrinsic constants, intrinsic data types, and so forth. When porting code from Visual Basic to VBScript, it’s important to review your code for unsupported elements.

For More Information To learn more about Visual Basic, Scripting Edition, including a list of supported language elements, visit the Microsoft Web site at http://www.microsoft.com.


International Issues

If you are planning to distribute your Visual Basic application to an international market, you can reduce the amount of time and code necessary to make your application as functional in its foreign market as it is in its domestic market. This chapter introduces key concepts and definitions for developing international applications with Visual Basic, presents a localization model, and emphasizes the advantages of designing software for an international market.

This chapter also discusses guidelines for writing Visual Basic code that results in a flexible, portable, and truly international application. A section is devoted to writing Visual Basic code that handles the specific aspects of the double-byte character set (DBCS) used on East Asian versions of Windows.

Topics

International Software Definitions

Defines basic terms related to localizing software.
Designing International Software

Explains the advantages of designing software with an international audience in mind and explains the best approach for localizing software.
Using Resource Files for Localization

Describes how to create, localize, and use a resource file for separating localizable information from your code.
Designing an International-Aware User Interface

Shows how to design messages, menus, dialog boxes, icons and bitmaps, and shortcut keys that will be easy to localize.
General Considerations When Writing International Code

Explains some important differences between English and other languages that affect the way you work with strings.
Writing International Code in Visual Basic

Explains how to write Visual Basic code that will work internationally.
Issues Specific to the Double-Byte Character Set (DBCS)

Provides information about working with Chinese, Japanese, Korean, and other East Asian languages.

Sample application

Atm.vbp

Some of the code examples in this chapter are taken from the Automated Teller Machine (Atm.vbp) sample which is listed in the Samples directory.


International Software Definitions

Before you start developing international software, you should know some fundamental terms.

International Software

International software is software that is marketable worldwide. A software product is international only if it is as functional in its foreign market as it is in its domestic market. For more information about how to localize your application, see "Designing International Software" later in this chapter.

Locale

A locale describes the user's environment — the local conventions, culture, and language of the user's geographical region. A locale is made up of a unique combination of a language and a country. Two examples of locales are: English/U.S. and French/Belgium.

A language might be spoken in more than one country; for instance, French is spoken in France, Belgium, Canada, and many African nations. While these countries share a common language, certain national conventions (such as currencies) vary among countries. Therefore, each country represents a unique locale. Similarly, one country might have more than one official language. Belgium has three — French, Dutch, and German. Therefore, Belgium has three distinct locales. For more information about locale-specific settings, see "General Considerations When Writing International Code" later in this chapter.

Localization

Localization is the process by which an application is adapted to a locale. It involves more than just literal, word-for-word translation of these resources — it is the meaning that must be communicated to the user. For more information about how to localize your application, see "Designing International Software" later in this chapter.

String Resources

String resources refers to all the text that appears in the application's user interface. They include, but are not limited to, menus, dialog boxes, and informational, alert, and error messages. If an application will be used in a locale other than the one in which it was developed, these resources will have to be translated, or localized.

For More Information For definitions of East Asian terminology, see "ANSI, DBCS, and Unicode: Definitions" later in this chapter. For more information about string resources and resource files, see "Using Resource Files for Localization" later in this chapter.


Localization Model

Designing International Software

It is a lot more efficient to design your application with localization in mind, following an approach that separates string resources from code, than to revise your finished application to make it international later in the development process.

Advantages of Designing International Software

There are four primary advantages to designing and implementing your Visual Basic application so that it is sensitive and appropriate to international conventions, foreign data, and format processing:

  • You can launch your Visual Basic application onto the market more rapidly. No additional international development is needed once the initial version of the product is complete.

  • You use resources more efficiently. Adding international support to your finished Visual Basic application may make it less stable. More development and testing resources would be required than if the same support had been implemented as part of the original development process.

  • If the international version of your Visual Basic application is built from the same set of sources as the version in which you originally developed your application, only isolated modules need international attention, thus making it easier and less expensive to maintain code while including international support. See "Using Resource Files for Localization" later in this chapter.

  • Developing an international version of your application becomes easy. For instance, you can develop an English-language version of your application that runs in a German operating environment without rewriting code. You only need to customize the user interface. See "Designing an International-Aware User Interface" later in this chapter.

Localization Model

Any application that will be localized represents two conceptual blocks: a code block and a data block. Figure 16.1 represents the data block as the "user interface component" and the code block as the "application component."

Figure 16.1 The data block and code block make up a localized product

The data block contains all the user-interface string resources but no code. Conversely, the code block contains only the application code that is run for all locales. This Visual Basic code handles the string resources and the locale-specific settings. "Writing International Code in Visual Basic" provides details on how to write Visual Basic code that handles locale-specific settings, such as dates, currencies, and numeric values and separators.

In theory, you can develop a localized version of your Visual Basic application by changing only the data block. The code block for all locales should be the same. Combining the data block with the code block results in a localized version of your application. The keys to successful international software design are the separation of the code and data blocks and the ability for data to be accurately read by your Visual Basic application, regardless of the locale.

Although it may be more work for the person writing the Visual Basic application, no user-interface elements should be present in the Visual Basic code. Instead, the string resources should be placed in a separate file, from which they will be loaded at run time. This file is called a resource file (.res), which is a file that contains all the strings, bitmaps, and icons that are localized. For more information about resource files, see "Using Resource Files for Localization" later in the chapter.

The teams working on localizing the application should work exclusively on the resource file to develop all the different language versions of the application. This approach has the following advantages:

  • Efficiency. Developing a new international version of the application only involves creating a new international resource file because each version has the same code block. This streamlines the creation of multiple language versions of your Visual Basic application.

  • Greater security. Whether you decide to localize your application in-house or to use an external company, you won't need to access source code to develop international versions of your application. This approach will also lower the amount of testing needed for the international version.

  • Better localization. By placing all string resources in one file, you ensure a more efficient localization process and reduce the chance of leaving some strings unlocalized.

The following table lists some factors to consider when designing your Visual Basic application.

Factor | Item
Language | Strings in the user interface (menus, dialog boxes, and error messages) Printed and online documentation
Locale-specific settings | Date/time formats (separators, order of day/month/year) Number formats (decimal and thousand separators) Currency formats (symbol and format) Sort order and string comparison

The first factor, language, is addressed primarily in the design phase of your Visual Basic application. See "Designing an International-Aware User Interface" for more information. The second factor, locale-specific settings, is discussed in "Writing International Code in Visual Basic" and "International Sort Order and String Comparison" later in this chapter.


Using Resource Files for Localization

A resource file is a useful mechanism for separating localizable information from code in Visual Basic.

Note You can have only one resource file in your project. If you attempt to add more than one resource file, Visual Basic generates an error message.

Advantages of Storing Strings in Resource Files

When you are writing Visual Basic code, you can use the LoadResString, LoadResPicture, and LoadResData functions in place of references to string literals, pictures, and data. Storing such elements in a resource file offers two benefits:

  • Performance and capacity are increased because strings, bitmaps, icons, and data can be loaded on demand from the resource file, instead of all being loaded at once when a form or a module is loaded.

  • The resources that need to be translated are isolated in one resource file. There is no need to access the source code or recompile the application.

To create a resource file

  1. Select Add New Resource File from the Project menu.

    Note This command is only available when the Resource Editor Add-In is loaded. To load the Resource Editor Add-In, select Add-In Manager from the Add-Ins menu. In the Add-In Manager dialog box, select VB6 Resource Editor and check the Loaded/Unloaded box.

  2. In the Open A Resource File dialog box, enter a name for the resource file. The resource file will be added to the Related Documents node in the Project Explorer.

Visual Basic recognizes resource files by the .res file name extension. If the resource file does not have the appropriate file name extension, Visual Basic won't load it. Conversely, if any file uses the .res file name extension, Visual Basic interprets that it is a resource file when adding it to the project. If the file does not follow the standard format for a resource file, Visual Basic generates an error message the first time you attempt to use the resource file support functions (LoadResString, LoadResPicture, and LoadResData), or when you try to make an .exe file. Visual Basic will generate the same error message if you try to add a 16-bit resource file to a project.

The .res file, before and after you compile the .exe file, is a standard Windows resource file, which means the resources contained in the file can be loaded in any standard Windows-based resource editor.

To edit a resource file

  1. Select Resource Editor from the Tools menu.

    Note This command is only available when the Resource Editor Add-In is loaded. To load the Resource Editor Add-In, select Add-In Manager from the Add-Ins menu. In the Add-In Manager dialog box, select VB6 Resource Editor and check the Loaded/Unloaded box.

  2. Select a button from the Resource Editor Toolbar to edit an existing resource or add a new one. To learn more about editing resources, see the Resource Editor Add-In documentation.

Locking Resource Files

Visual Basic uses file locking on the .res file to prevent problems with multiple applications trying to use the file at the same time. Visual Basic will lock the .res file whenever:

  • Visual Basic is in run mode or break mode.

  • You create an .exe file.

For More Information For an example of how a resource file can be used to create an application that works in several locales, see "The Automated Teller Machine Sample Application" later in this chapter. For background information about programming with resource files, see "Working with Resource Files" in "More About Programming."


The Automated Teller Machine Sample Application

This sample application has been designed to illustrate support for resource files in Visual Basic. The application contains three forms, a standard module, and a resource file. When you run the Automated Teller Machine (Atm.vbp) sample application, an opening screen lets you perform a bank transaction in one of several languages, including German, French, Italian, and Spanish.

The following code from the FrmInput.frm file loads resources stored in the Atm32.res file, which contains the localized strings for all languages.

Sub Form_Load()
   imgFlag = LoadResPicture(I, vbResBitmap)
   Caption = LoadResString(I)
   lblPINCode = LoadResString(1 + I)
   fraAccount = LoadResString(2 + I)
   optChecking.Caption = LoadResString(3 + I)
   optSavings.Caption = LoadResString(4 + I)
   lblAmount = LoadResString(5 + I)
   cmdOK.Caption = LoadResString(6 + I)
   SetCursor cmdOK
End Sub

Sub cmdOK_click()
   ' Display a process message.
   MsgBox LoadResString(7 + I)
   frmAmountWithdrawn.Show vbModal
   Unload Me
End Sub

At run time, this code reads the appropriate section of the resource file, based on an offset that is initialized when the user makes a language selection in the opening screen. The offset is a public variable declared in the standard module that indicates how far from a starting point a particular item is located. In the ATM sample application, the offset variable is I.

In the resource file, resource identifiers 16 through 47 are reserved for English, 48 through 79 are reserved for French, 80 through 111 are reserved for German, and so on. Each language contains the localized entries that make up the data block of the sample application. This block currently contains the eleven resources that are particular to each language.

This sample application, which contains several data blocks, introduces an alternative to a language-specific resource file using only one data block. Depending on the nature of the application you are developing, you may consider using one resource file per language version of your application or a single resource file containing all the localized data blocks.

The design of the Automated Teller Machine sample application presents several advantages beyond the ones outlined earlier in the chapter:

  • The application can grow in scope by providing service in more languages. Simply add the same data block to the resource file and localize it as needed. If you decide to add a language, you may have to add a button to the opening screen.

  • The application can grow in size if you want to extend your application by, for instance, allowing the ATM users to make deposits. Simply allow for wider identifier ranges (160 for example) for each language in the resource file. Currently, the identifiers range from 16 to 47, 48 to 79, and so on.

For More Information See "LoadResString Function," "LoadResPicture Function," and "LoadResData Function" in the Language Reference. For information on resource files, see "Working with Resource Files" in "More About Programming" and "Designing for Performance and Compatibility."


Designing an International-Aware User Interface

Because text tends to grow when you localize an application, you should pay special attention when designing the following user interface (UI) components:

  • Messages

  • Menus and dialog boxes

  • Icons and bitmaps

  • Access and shortcut keys

Messages

English text strings are usually shorter than localized text strings in other languages. The following table shows the additional average growth for strings, based on initial length. This data is drawn from past Visual Basic localization projects and describes an average growth rate.

English length (in characters) | Additional growth for localized strings
1 to 4 | 100%
5 to 10 | 80%
11 to 20 | 60%
21 to 30 | 40%
31 to 50 | 20%
Over 50 | 10%

When designing the interface, consider these growth ratios and allow for text to wrap to more lines as the messages get longer.

Menus and Dialog Boxes

As with messages, menus and dialog boxes may grow when the application is localized. Consider the two following identical dialog boxes in the Automated Teller Machine sample application. You can see that extra space was allocated in the dialog box to allow for text expansion. Figure 16.2 shows the English dialog box, while Figure 16.3 shows the Spanish dialog box. Knowing that text can grow, plan your interface so that controls don't have to be resized or other elements redesigned when localized.

Figure 16.2 English input dialog box in the ATM sample application

Figure 16.3 Spanish input dialog box in the ATM sample application

In menus and dialog boxes, avoid crowding status bars. Even abbreviations might be longer or simply not exist in other languages.

Icons and Bitmaps

Icons and bitmaps are usually used to depict a certain functionality without using text. Consider the following rules when working with icons and bitmaps:

  • Avoid using bitmaps that are not an international standard. The following bitmaps represent a mailbox in the United States, but many users from other locales will not recognize it.

  • Avoid using bitmaps that contain text. They take time to redraw, and text growth might also become an obstacle, as illustrated in the following icons.

  • Make sure that bitmaps or icons are culturally sensitive. What may be acceptable in one locale may be inappropriate or offensive in another.

Access and Shortcut Keys

Different locales have different keyboard layouts. Not all characters exist in all keyboard layouts. When developing your Visual Basic application, make sure all access-key and shortcut-key combinations you assign can be reproduced with international keyboards. One simple method to verify that your keyboard assignments work properly for the locales you are targeting is to choose the desired keyboard layout from your Windows Control Panel, along with keyboard layout pictures (which some reference manuals contain), and try the access-key and shortcut-key combinations.

Because certain access-key and shortcut-key combinations are not available for certain locales or because they are reserved for system use by some editions of Windows, it is best to avoid them when developing your Visual Basic application. Here are some examples of characters to avoid:

@ $ { } [ ] \ ~ | ^ ' < >

One way to work around this limitation is to use numbers and function keys (F1, F2, etc.) instead of letters in shortcut-key combinations. These may be less intuitive but they will not require any localization, because virtually all keyboard layouts include numbers and function keys.

Note DBCS characters cannot be used as access or shortcut keys.


General Considerations When Writing International Code

When you're developing an application that will be localized — whether you're programming with Visual Basic or another tool — you must take into account differences between languages. You should identify the strings that need to be localized, allow for strings to grow, and avoid the pitfalls of string concatenation.

Hard-Coded Localizable Strings

The localization model presented in "Designing International Software" introduced the concepts of data block and code block. When building the resource files containing all the localizable strings, it is important to include only the strings that need to be localized. Any item that does not need to be partially or entirely localized can be hard-coded. Conversely, it is also fundamental to make sure all the resources that need to be localized are actually present in these resource files.

Buffer Sizes

If you are declaring a buffer size based on the expected length of a string or word, make sure this buffer can accommodate larger words and strings. See "Designing an International-Aware User Interface" for average growth rates of translated strings. The buffer size you declare in your Visual Basic code must account for this increase.

Consider the following example. Your Visual Basic declares a 2-byte buffer size for the word "OK." In Spanish, however, the same word is translated as "Aceptar," which would cause your program to overflow. Identical considerations apply for double-byte characters. Refer to "Issues Specific to the Double-Byte Character Set (DBCS)" later in this chapter for more information about DBCS.

String Concatenation

When you try to reduce the size of a string, one possible approach is string concatenation. This method allows you to use the same resource in several strings. However, there are some dangers when using this approach. Consider the following example:

English | French
String1: one after the other | String1: un après l'autre
String2: The controls will be deleted. | String2: Les contrôles seront supprimés.
String3: The forms will be deleted. | String3: Les feuilles seront supprimées.

Taken separately, String1, String2, and String3 can be easily localized. If your code performs String2 + String1 or String3 + String1, the resulting English string would look fine. The localized strings, however, are likely to be wrong. In the French column, for instance, String3 + String1 would be wrong because in French grammar, forms (feuilles) is a feminine word, thus String1 should be "une après l'autre" and not "un après l'autre." The same situation will be true in many other foreign languages. The only way to avoid this is to keep String2 and String1, and String3 and String1, together in the resource file.

In the above example, the order of the words that make up the sentence is the same in English and in French. However, the order is generally not the same in these two languages, or many other foreign languages. (For example, in both German and Japanese the verb generally appears at the end of the sentence.) The following example illustrates this situation:

English | French
String1: DLL | String1: DLL
String2: Missing Data Access | String2: Accès aux données manquante
String3: Missing OLE | String3: OLE manquante

If your code performs String2 + String1 and String3 + String1, localized versions will be broken because the order of the two strings produces a message that does not make any sense. One possible solution is to simply add String1 to String2 and String3 directly in the resource file and remove String1.

Another possible solution is presented in the following table:

English | French
String2: Missing Data Access '|1' | String2: '|1' d'accès aux données manquant
String3: Missing OLE '|1' | String3: '|1' OLE manquant

In this case, the localizer can identify '|1' as a placeholder and make the necessary changes in the resource file to reflect the appropriate way to build a sentence for the localized language.

Finally, it is also important to know that words or sentences that appear identical in English may need to be translated into different words or sentences when localized. Consider the following example:

English | French
String1: Setup program | String1: Programme d'installation
String2: String1 did not complete successfully. | String2: String1 a échoué.

In the English version, String1 is used as the setup program banner. It is also used as part of an error message in String2. In the French version, String1 worked perfectly as the stand-alone banner string. However, it needed to become "Le programme d'installation" to be used with String2.


Special Condsiderations When Writing International Code

Writing International Code in Visual Basic

Preparing a product for use in other locales implies more than just translating text messages. The product must support national conventions and provide country-specific support for numbers. In order to know how to work with different dates, currencies, and numeric values and separators, you have to understand the distinction Visual Basic makes between system locale and code locale.

System Locale vs. Code Locale

The system locale is the locale of the user who runs your program — it is used as a reference for user input and output and uses Control Panel settings provided by the operating system. The code locale is always English/U.S. in Visual Basic, regardless of which international version you use. Code locale determines the programming language and all the locale-specific settings.

Date

In Visual Basic, never type dates as strings in your code. Entering dates in code in the format #month/day/year# ensures that the date will be interpreted correctly in any system locale. Because Visual Basic allows only English/U.S. as a programming locale, the date will be the same to a user wherever your application is run.

For example, if a user enters 8/2/97 in an input dialog box,

CDate ("8/2/97")

returns the following results, based on the system locale:

Operating system | Output
French/France | 08/02/97 (= February 8, 1997)
English/U.S. | 8/2/97 (= August 2, 1997)

Conversely, if you enter 8/2/97 in code,

CDate (#8/2/97#)

returns the results in the following table, based on the code locale:

Operating system | Output
French/France | 02/08/97 (= August 2, 1997)
English/U.S. | 8/2/97 (= August 2, 1997)

If the user is in France and enters 8/2/97, the application will interpret this date as February 8, 1997, because the date format in France is day/month/year. If a user in the United States enters the same string, the application will understand August 2, 1997, because the date format is month/day/year.

Currency

Avoid typing currencies as strings in your code. For example, the following code does not run in any locale except those where the dollar sign ($) is the currency symbol.

Money = "$1.22"
NewMoney = CCur(Money)

If you run this code example in the French/France locale, where "F" is the currency symbol, Visual Basic will generate a "Type mismatch" error message, because $ is not recognized as a currency symbol in that locale. Instead, simply use numbers, as shown in the following example. Use a period as a decimal separator, because the code locale for Visual Basic is always English/U.S. The following code will run correctly, regardless of the user's locale.

Money = 1.22
NewMoney = CCur(Money)

Numeric Values and Separators

In the United States, the period (.) is used as the decimal separator. In several European countries, however, the comma (,) is used as the decimal separator. Similarly, in the United States, a comma is used as the thousands separator to isolate groups of three digits to the left of the decimal separator. In several European countries, a period or a space is used for this purpose. The following table lists some examples of different number formats:

Countries | Number formats
U.S. | 1,234,567.89 1,234.56 .123
France | 1 234 567,89 1 234,56 0,123
Italy | 1.234.567,89 1.234,56 0,123

Note In Visual Basic, the Str and Val functions always assume a period is the decimal separator. In a majority of locales, this assumption is not valid. Instead, use the CStr, CDbl, CSng, CInt, and CLng functions to provide international conversions from any other data type to the data type you need. These functions use the system locale to determine the decimal separator.

For More Information See "Locale-Aware Functions" later in this chapter for more information about the Print and Format functions. See "CStr Function," "CDbl Function," "CSng Function," "CInt Function," "CLng Function," "CDate Function," and "CCur Function" in the Language Reference. See also "Variables, Constants, and Data Types."


Locale-Aware Functions

Each locale has different conventions for displaying dates, time, numbers, currency, and other information. It is not necessary to know all the conventions of your users' locales. In Visual Basic, many functions use the user's system locale, which uses the Control Panel settings provided by the operating system to automatically determine the conventions at run time. These functions are called locale-aware functions.

Print Method

Even though the Print method provides little flexibility for different output formats, it does use the user's system locale. In the following example, dates are printed using the correct short date format, numbers are printed with the correct decimal separator, and currencies are printed with the correct symbol:

MyDate = #11/24/1997#
MyNumber = 26.5
Money = 1636.32
MyMoney = Format(Money, "###,###.##")
Debug.Print MyDate, MyNumber, MyMoney

When this code is run in an English/U.S. locale, the following output appears in the Immediate window:

11/24/1997   26.5   1,636.32

When this code is run in a German/Germany locale, the following output appears in the Immediate window:

24/11/1997   26,5   1.632,32

For More Information See "Print Method" in the Language Reference.

Format Function

The Format function can accept format codes, but format codes always produce the same type of output regardless of the user's locale. For example, the format code "mm-dd-yy" is not appropriate for a user in Belgium, where the day precedes the month.

For more flexibility, the Format function also provides named formats that will automatically determine which conventions to use at run time, including General Date, Long Date, Short Date, and Long Time. Using named formats produces output that is based on the user's system locale. The named formats can even generate output in the user's native language, including the names of months and days of the week. The following example illustrates this:

MyDate = #8/22/1997 5:22:20 PM#
NewDate1 = Format(MyDate, "Medium Date")
NewDate2 = Format(MyDate, "Short Date")
NewDate3 = Format(MyDate, "Long Date")
NewDate4 = Format(MyDate, "General Date")
Debug.Print NewDate1, NewDate2, NewDate3, NewDate4

When this code is run in an English/U.S. locale, the following output appears in the Immediate window:

22-Aug-97 8/22/97      Monday, August 22, 1997   8/22/97 5:22:20 PM

When this code is run in a French/France locale, the following output appears in the Immediate window:

22-août-97   22/08/97      lundi 22 août 1997      22/08/97 17:22:20f

For More Information See "Format Function" in the Language Reference.


International Sort Order and String Comparison

String comparison is widely used in Visual Basic. Using this functionality, however, may yield incorrect results if you overlook certain programming requirements.

Sorting Text

Sorting text means ordering text according to language conventions. Format and font are irrelevant to the sorting process because both involve presentation rather than content. At first glance, sorting text looks simple: a precedes b, b precedes c, and so on. However, there are many languages that have more complex rules for sorting. Correct international sorting is not always a simple extension of sorting English text, and it requires a different understanding of the sorting process.

Correct international sorting can imply context-sensitive sorting. Character contraction and expansion are the two important areas of context-sensitive sorting.

  • Character contraction occurs when a two-character combination is treated as a single, unique letter. For example, in Spanish the two-character combination ch is a single, unique letter and sorts between c and d.

  • Character expansion occurs in cases where one letter represents one character, but that one character sorts as if it were two. For example, ß (eszett) is equivalent to ss in both German/Germany and German/Switzerland locales. However, ß is equivalent to sz in the German/Austria locale.

Before implementing the sorting order, you must consider code pages. A code page is an ordered character set that has a numeric index (code point) associated with each character. Because there are various code pages, a single code point might represent different characters in different code pages. While most code pages share the code points 32 through 127 (ASCII character set), they differ beyond that. Typically, the ordering of any additional letters in these code pages is not alphabetic.

For More Information See "DBCS Sort Order and String Comparison" later in this chapter for more information about working with East Asian languages.

String Comparison in Visual Basic

String comparison rules are different for each locale. Visual Basic provides a number of tools, such as Like and StrComp, which are locale-aware. To use these effectively, however, the Option Compare statement must first be clearly understood.

Comparing Strings with the Option Compare Statement

When using this statement, you must specify a string comparison method: either Binary or Text for a given module. If you specify Binary, comparisons are done according to a sort order derived from the internal binary representations of the characters. If you specify Text, comparisons are done according to the case-insensitive textual sort order determined by the user's system locale. The default text comparison method is Binary.

In the following code example, the user enters information into two input boxes. The information is then compared and sorted in the appropriate alphabetic order.

Private Sub Form_Click ()
Dim name1 As String, name2 As String
   name1 = InputBox("Enter 1st hardware name here:")
   name2 = InputBox("Enter 2nd hardware name here:")
If name1 < name2 Then
   msg = " ' " & name1 & " ' comes before ' " & _
   name2 & " ' "
Else
   msg = " ' " & name2 & " ' comes before ' " & _
   name1 & " ' "
End If
   MsgBox msg
End Sub

If this code is run in an English/U.S. locale, the message box will contain the following output if the user enters printer and Screen:

'Screen' comes before 'printer'

This result is based on the fact that the default text-comparison method is Binary. Because the internal binary representation of uppercase S is smaller than the one for lowercase p, the conditional statement Screen < printer is verified. When you add the Option Compare Text statement in the Declarations section of a module, Visual Basic compares the two strings on a case-insensitive basis, resulting in the following output:

'printer' comes before 'Screen'

If this code is run in a French/Canada locale, the message box will contain the following output if the user enters imprimante and écran:

'imprimante' comes before 'écran'

Similarly, if you add the Option Compare Text statement to your code, the two terms will appear in the right order — that is, écran will precede imprimante. In addition to being case insensitive, the comparison takes into account the accented characters, such as é in French, and places it right after its standard character — in this case, e, in the sorting order.

If the user had entered ecran and écran, the output would be:

'ecran' comes before 'écran'

For More Information See "Option Compare Statement" in the Language Reference.

Comparing Strings with the Like Operator

You can use the Like operator to compare two strings. You can also use its pattern-matching capabilities. When you write international software, you must be aware of pattern-matching functions. When character ranges are used with Like, the specified pattern indicates a range of the sort ordering. For example, under the Binary method for string comparison (by default or by adding Option Compare Binary to your code), the range [A – C] would miss both uppercase accented a characters and all lower-case characters. Only strings starting with A, B, and C would match. This would not be acceptable in many languages. In German, for instance, the range would miss all the strings beginning with Ä. In French, none of the strings starting with À would be included.

Under the Text method for string comparison, all the accented A and a characters would be included in the interval. In the French/France locale, however, strings starting with Ç or ç would not be included, since Ç and ç appear after C and c in the sort order.

Using the [A – Z] range to check for all strings beginning with an alphabetic character is not a valid approach in certain locales. Under the Text method for string comparison, strings beginning with Ø and ø would not be included in the range if your application is running in a Danish/Denmark locale. Those two characters are part of the Danish alphabet, but they appear after Z. Therefore, you would need to add the letters after Z. For example, Print "øl" Like "[A-Z]*" would return False, but Print "øl" Like "[A-ZØ]*" would return True with the Option Compare Text statement.

Comparing Strings with the StrComp Function

The StrComp function is useful when you want to compare strings. It returns a value that tells you whether one string is less than, equal to, or greater than another string. The return value is also based on the string comparison method (Binary or Text) you defined with the Option Compare statement. StrComp may give different results on the strings you compare, depending on the string comparison method you define.

For More Information See "DBCS Sort Order and String Comparison" later in this chapter for more information about comparing strings in East Asian languages. See also "StrComp Function" in the Language Reference.


International File Input/Output

Locale is also an important consideration when working with file input and output in Visual Basic. Both the Print # and Write # statements can be used to work with data files, but they have distinct purposes.

Print #

The Print # statement puts data into a file as the data is displayed on the screen, in a locale-aware format. For instance, date output uses the system Short Date format, and numeric values use the system decimal separator.

The Input # statement cannot read locale-aware data in Visual Basic that has been written to a file with the Print # statement. To write locale-independent data that can be read by Visual Basic in any locale, use the Write # statement instead of the Print # statement.

Write #

Like the Print # statement, the Write # statement puts data into a file in a fixed format, which ensures that the data can be read from the file in any locale when using the Input # statement. For instance, dates are written to the file using the universal date format, and numeric values are written to the file using the period as the decimal separator. In the following code example, a date and a numeric value are written to a file with the Write # statement. The same file is reopened later, its content is read with the Input # statement, and the results are printed in the Immediate window. The Long Date information is drawn from the system locale:

Dim MyDate As Date, NewDate As Date
Dim MyNumber As Variant
   MyDate = #8/2/67#
   MyNumber = 123.45
Open "Testfile" for Output As #1
   Write #1, MyDate, MyNumber
Close #1

Open "Testfile" for Input As #1
   Input #1, MyDate, MyNumber
   NewDate = Format(Mydate, "Long Date")
Debug.Print NewDate, MyNumber
Close #1

When you run this code in an English/U.S. locale, the following output appears in the Immediate window:

Wednesday, August 02, 1967      123.45

When you run this code in a French/France locale, the following output appears in the Immediate window:

mercredi 2 août 1967            123,45

In both locales, the output is accurate — that is, the information was stored and retrieved properly using the Write # and Input # statements.

For More Information For background information on processing files, see "Working with Files" in "Processing Drives, Folders, and Files." See also "Print # Statement" or "Write # Statement" in the Language Reference.


Locale-Aware SQL Queries Based on Dates

As explained in "Writing International Code in Visual Basic," different countries have different date formats. If your application performs a comparison between two dates, date literals must be stored in a unique format to ensure a reliable comparison, regardless of a user's locale. In Visual Basic, the database engine stores a date/time value as a DateSerial value, which is represented by an 8-byte floating-point number, with the date as the integral portion and the time as the fractional portion. This approach is completely locale-independent and will let you perform date/time comparisons using the international date/time formats.

Structured Query Language (SQL) is an ANSI standard with which Visual Basic complies. Dates are saved in tables and databases using the English/U.S. format (month/day/year). This format was also adopted for the Microsoft Jet database engine. Queries that use these fields may return the wrong records or no records at all if a non-U.S. date format is used.

This constraint also applies to the Filter property, to the FindFirst, FindNext, FindPrevious, and FindLast methods of the Recordset object, and to the WHERE clause of an SQL statement.

Using DateSerial and DateValue

There are two functions you can use to handle the limitations of the SQL standard. Avoid using date/time literals in your code. Instead, consider using the DateValue or the DateSerial functions to generate the date you want. The DateValue function uses the system's Short Date setting to interpret the string you supply; the DateSerial function uses a set of arguments that will run in any locale. If you are using date/time literals in your SQL query or with the Filter property, you have no choice but to use the English/U.S. format for date and time.

The following examples illustrate how to perform a query based on a date. In the first example, a non-U.S. date format is used. The Recordset returned is empty because there is a syntax error in the date expression:

Dim mydb As Database
Dim myds As Recordset

Set mydb = OpenDatabase("MyDatabase.mdb")
' Table that contains the date/time field.
Set myds = mydb.OpenRecordset("MyTable,dbopenDynaset")
' The date format is dd/mm/yy.
myds.FindFirst "DateFiled > #30/03/97#"
' A data control is connected to mydb.
Data1.Recordset.Filter = "DateFiled = #30/03/97#"

mydb.Close
myds.Close

The following example, however, will work adequately in any locale because the date is in the appropriate format:

Dim mydb As Database
Dim myds As Recordset

Set mydb = OpenDatabase("MyDatabase.mdb")
' Table that contains the date/time field.
Set myds = mydb.OpenRecordset("MyTable, dbopenDynaset")

myds.FindFirst "DateFiled > #03/30/97#"   ' Date format
                                    ' is mm/dd/yy.

' A data control is connected to mydb.
Data1.Recordset.Filter = "DateFiled = _
DateValue(""" & DateString & """)"

mydb.Close
myds.Close


Issues Specific to the Double-Byte Character Set (DBCS)

The double-byte character set (DBCS) was created to handle East Asian languages that use ideographic characters, which require more than the 256 characters supported by ANSI. Characters in DBCS are addressed using a 16-bit notation, using 2 bytes. With 16-bit notation you can represent 65,536 characters, although far fewer characters are defined for the East Asian languages. For instance, Japanese character sets today define about 12,000 characters.

In locales where DBCS is used — including China, Japan, and Korea — both single-byte and double-byte characters are included in the character set. The single-byte characters used in these locales conform to the 8-bit national standards for each country and correspond closely to the ASCII character set. Certain ranges of codes in these single-byte character sets (SBCS) are designated as lead bytes for DBCS characters. A consecutive pair made of a lead byte and a trail byte represents one double-byte character. The code range used for the lead byte depends on the locale.

Note DBCS is a different character set from Unicode. Because Visual Basic represents all strings internally in Unicode format, both ANSI characters and DBCS characters are converted to Unicode and Unicode characters are converted to ANSI characters or DBCS characters automatically whenever the conversion is needed. You can also convert between Unicode and ANSI/DBCS characters manually. For more information about conversion between different character sets, see "DBCS String Manipulation Functions."

When developing a DBCS-enabled application with Visual Basic, you should consider:

  • Differences between Unicode, ANSI, and DBCS.

  • DBCS sort orders and string comparison.

  • DBCS string manipulation functions.

  • DBCS string conversion.

  • How to display and print fonts correctly in a DBCS environment.

  • How to process files that include double-byte characters.

  • DBCS identifiers.

  • DBCS-enabled events.

  • How to call Windows APIs.

Tip Developing a DBCS-enabled application is good practice, whether or not the application is run in a locale where DBCS is used. This approach will help you develop a flexible, portable, and truly international application. None of the DBCS-enabling features in Visual Basic will interfere with the behavior of your application in environments using exclusively single-byte character sets (SBCS), and the size of your application will not increase because both DBCS and SBCS use Unicode internally.

For More Information For limitations on using DBCS for access and shortcut keys, see "Designing an International-Aware User Interface."


ANSI, DBCS, and Unicode: Definitions

Visual Basic uses Unicode to store and manipulate strings. Unicode is a character set where 2 bytes are used to represent each character. Some other programs, such as the Windows 95 API, use ANSI (American National Standards Institute) or DBCS to store and manipulate strings. When you move strings outside of Visual Basic, you may encounter differences between Unicode and ANSI/DBCS. The following table shows the ANSI, DBCS, and Unicode character sets in different environments.

Environment | Character set(s) used
Visual Basic | Unicode
32-bit object libraries | Unicode
16-bit object libraries | ANSI and DBCS
Windows NT API | Unicode
Automation in Windows NT | Unicode
Windows 95 API | ANSI and DBCS
Automation in Windows 95 | Unicode

ANSI

ANSI is the most popular character standard used by personal computers. Because the ANSI standard uses only a single byte to represent each character, it is limited to a maximum of 256 character and punctuation codes. Although this is adequate for English, it doesn't fully support many other languages.

DBCS

DBCS is used in Microsoft Windows systems that are distributed in most parts of Asia. It provides support for many different East Asian language alphabets, such as Chinese, Japanese, and Korean. DBCS uses the numbers 0 – 128 to represent the ASCII character set. Some numbers greater than 128 function as lead-byte characters, which are not really characters but simply indicators that the next value is a character from a non-Latin character set. In DBCS, ASCII characters are only 1 byte in length, whereas Japanese, Korean, and other East Asian characters are 2 bytes in length.

Unicode

Unicode is a character-encoding scheme that uses 2 bytes for every character. The International Standards Organization (ISO) defines a number in the range of 0 to 65,535 (216 – 1) for just about every character and symbol in every language (plus some empty spaces for future growth). On all 32-bit versions of Windows, Unicode is used by the Component Object Model (COM), the basis for OLE and ActiveX technologies. Unicode is fully supported by Windows NT. Although both Unicode and DBCS have double-byte characters, the encoding schemes are completely different.

Character Code Examples

Figure 16.4 shows an example of the character code in each character set. Note the different codes in each byte of the double-byte characters.

Figure 16.4 Character codes for "A" in ANSI, Unicode, and DBCS


DBCS Sort Order and String Comparison

You need to be aware of the issues when sorting and comparing DBCS text, because the Option Compare Text statement has a special behavior when used on DBCS strings. When you use the Option Compare Binary statement, comparisons are made according to a sort order derived from the internal binary representations of the characters. When you use Option Compare Text statement, comparisons are made according to the case-insensitive textual sort order determined by the user's system locale.

In English "case-insensitive" means ignoring the differences between uppercase and lowercase. In a DBCS environment, this has additional implications. For example, some DBCS character sets (including Japanese, Traditional Chinese, and Korean) have two representations for the same character: a narrow-width letter and a wide-width letter. For example, there is a single-byte "A" and a double-byte "A." Although they are displayed with different character widths, Option Compare Text treats them as the same character. There are similar rules for each DBCS character set.

You need to be careful when you compare two strings. Even if the two strings are evaluated as the same using Like or StrComp, the exact characters in the strings can be different and the string length can be different, too.

For More Information For general information about comparing strings with the Option Compare statement, see "International Sort Order and String Comparison."


DBCS String Manipulation Functions

Although a double-byte character consists of a lead byte and a trail byte and requires two consecutive storage bytes, it must be treated as a single unit in any operation involving characters and strings. Several string manipulation functions properly handle all strings, including DBCS characters, on a character basis.

These functions have an ANSI/DBCS version and a binary version and/or Unicode version, as shown in the following table. Use the appropriate functions, depending on the purpose of string manipulation.

The "B" versions of the functions in the following table are intended especially for use with strings of binary data. The "W" versions are intended for use with Unicode strings.

Function | Description
Asc | Returns the ANSI or DBCS character code for the first character of a string.
AscB | Returns the value of the first byte in the given string containing binary data.
AscW | Returns the Unicode character code for the first character of a string.
Chr | Returns a string containing a specific ANSI or DBCS character code.
ChrB | Returns a binary string containing a specific byte.
ChrW | Returns a string containing a specific Unicode character code.
Input | Returns a specified number of ANSI or DBCS characters from a file.
InputB | Returns a specified number of bytes from a file.
InStr | Returns the first occurrence of one string within another.
InStrB | Returns the first occurrence of a byte in a binary string.
Left, Right | Returns a specified number of characters from the right or left sides of a string.
LeftB, RightB | Returns a specified number of bytes from the left or right side of a binary string.
Len | Returns the length of the string in number of characters.
LenB | Returns the length of the string in number of bytes.
Mid | Returns a specified number of characters from a string.
MidB | Returns the specified number of bytes from a binary string.

The functions without a "B" or "W" in this table correctly handle DBCS and ANSI characters. In addition to the functions above, the String function handles DBCS characters. This means that all these functions consider a DBCS character as one character even if that character consists of 2 bytes.

The behavior of these functions is different when they're handling SBCS and DBCS characters. For instance, the Mid function is used in Visual Basic to return a specified number of characters from a string. In locales using DBCS, the number of characters and the number of bytes are not necessarily the same. Mid would only return the number of characters, not bytes.

In most cases, use the character-based functions when you handle string data because these functions can properly handle ANSI strings, DBCS strings, and Unicode strings.

The byte-based string manipulation functions, such as LenB and LeftB, are provided to handle the string data as binary data. When you store the characters to a String variable or get the characters from a String variable, Visual Basic automatically converts between Unicode and ANSI characters. When you handle the binary data, use the Byte array instead of the String variable and the byte-based string manipulation functions.

For More Information See the Language Reference for the appropriate function.

If you want to handle strings of binary data, you can map the characters in a string to a Byte array by using the following code:

Dim MyByteString() As Byte
' Map the string to a Byte array.
MyByteString = "ABC"
' Display the binary data.
For i = LBound(MyByteString) to UBound(MyByteString)
   Print Right(" " + Hex(MyByteString(i)),2) + " ,";
Next
Print

DBCS String Conversion

Visual Basic provides several string conversion functions that are useful for DBCS characters: StrConv, UCase, and LCase.

StrConv Function

The global options of the StrConv function are converting uppercase to lowercase, and vice versa. In addition to those options, the function has several DBCS-specific options. For example, you can convert narrow letters to wide letters by specifying vbWide in the second argument of this function. You can convert one character type to another, such as hiragana to katakana in Japanese. StrConv enables you to specify a LocaleID for the string, if different than the system's LocaleID.

You can also use the StrConv function to convert Unicode characters to ANSI/DBCS characters, and vice versa. Usually, a string in Visual Basic consists of Unicode characters. When you need to handle strings in ANSI/DBCS (for example, to calculate the number of bytes in a string before writing the string into a file), you can use this functionality of the StrConv function.

Case Conversion in Wide-Width Letters

You can convert the case of letters by using the StrConv function with vbUpperCase or vbLowerCase, or by using the UCase or LCase functions. When you use these functions, the case of English wide-width letters in DBCS are converted as well as ANSI characters.


Font, Display, and Print Considerations in a DBCS Environment

When you use a font designed only for SBCS characters, DBCS characters may not be displayed correctly in the DBCS version of Windows. You need to change the Font object's Name property when developing a DBCS-enabled application with the English version of Visual Basic or any other SBCS-language version. The Name property determines the font used to display text in a control, in a run-time drawing, or during a print operation. The default setting for this property is MS Sans Serif in the English version of Visual Basic. To display text correctly in a DBCS environment, you have to change the setting to an appropriate font for the DBCS environment where your application will run. You may also need to change the font size by changing the Size property of the Font object. Usually, the text in your application will be displayed best in a 9-point font on most East Asian platforms, whereas an 8-point font is typical on European platforms.

These considerations apply to printing DBCS characters with your application as well.

How to Avoid Changing Font Settings

If you do not have any DBCS-enabled font or do not know which font is appropriate for the target platform, there are several options for you to work around the font issues.

In the Traditional Chinese, Simplified Chinese, and Korean versions of Windows, there is a system capability called Font Association. With Korean Windows, for example, Font Association automatically maps any English fonts in your application to a Korean font. Therefore, you can still see Korean characters displayed, even if your application uses English fonts. The associated font is determined by the setting in \HKEY_LOCAL_MACHINE\System\CurrentControlSet\control\fontassoc
\Associated DefaultFonts in the system registry of the run-time platform. With Font Association supported by the system, you can run your English application on a Chinese or Korean platform without changing any font settings. Font Association is not available on other platforms, such as Japanese Windows.

Another option is to use the System or FixedSys font. These fonts are available on every platform. Note that the System and FixedSys fonts have few variations in size. If the font size you set at design time (with the Size property of the Font object) for either of these fonts does not match the size of the font on the user's machine, the setting may be ignored and the displayed text truncated.

How to Change the Font at Run Time

Even though you have the options above, these solutions have restrictions. Here is an example of a global solution to changing the font in your application at run time. The following code, which works in any language version of Windows, applies the proper font to the Font object specified in the argument.

Private Const DEFAULT_CHARSET = 1
Private Const SYMBOL_CHARSET = 2
Private Const SHIFTJIS_CHARSET = 128
Private Const HANGEUL_CHARSET = 129
Private Const CHINESEBIG5_CHARSET = 136
Private Const CHINESESIMPLIFIED_CHARSET = 134
Private Declare Function GetUserDefaultLCID Lib "kernel32" () As Long

Public Sub SetProperFont(obj As Object)
    On Error GoTo ErrorSetProperFont
    Select Case GetUserDefaultLCID
    Case &H404 ' Traditional Chinese
        obj.Charset = CHINESEBIG5_CHARSET
        obj.Name = ChrW(&H65B0) + ChrW(&H7D30) + ChrW(&H660E) _
         + ChrW(&H9AD4)   'New Ming-Li
        obj.Size = 9
    Case &H411 ' Japan
        obj.Charset = SHIFTJIS_CHARSET
        obj.Name = ChrW(&HFF2D) + ChrW(&HFF33) + ChrW(&H20) + _
         ChrW(&HFF30) + ChrW(&H30B4) + ChrW(&H30B7) + ChrW(&H30C3) + _
         ChrW(&H30AF)
        obj.Size = 9
    Case &H412 'Korea UserLCID
        obj.Charset = HANGEUL_CHARSET
        obj.Name = ChrW(&HAD74) + ChrW(&HB9BC)
        obj.Size = 9
    Case &H804 ' Simplified Chinese
        obj.Charset = CHINESESIMPLIFIED_CHARSET
        obj.Name = ChrW(&H5B8B) + ChrW(&H4F53)
        obj.Size = 9
    Case Else   ' The other countries
        obj.Charset = DEFAULT_CHARSET
        obj.Name = ""   ' Get the default UI font.
        obj.Size = 8
    End Select
    Exit Sub
ErrorSetProperFont:
    Err.Number = Err
End Sub

You can modify this sample code to make the font apply to other font settings, such as printing options.


Processing Files That Use Double-Byte Characters

In locales where DBCS is used, a file may include both double-byte and single-byte characters. Because a DBCS character is represented by two bytes, your Visual Basic code must avoid splitting it. In the following example, assume Testfile is a text file containing DBCS characters.

' Open file for input.
Open "TESTFILE" For Input As #1

' Read all characters in the file.
Do While Not EOF(1)
   MyChar = Input(1, #1)   ' Read a character.
   ' Perform an operation using Mychar.
Loop
Close #1                     ' Close file.

When you read a fixed length of bytes from a binary file, use a Byte array instead of a String variable to prevent the ANSI-to-Unicode conversion in Visual Basic.

Dim MyByteString(0 to 4) As Byte

Get #1,, MyByteString

When you use a String variable with Input or InputB to read bytes from a binary file, Unicode conversion occurs and the result is incorrect.

Keep in mind that the names of files and directories may also include DBCS characters.

For More Information For background information on file processing, see "Working with Files" in "Processing Drives, Folders, and Files." For information on the Byte data type, see "Data Types" in "Programming Fundamentals."


Identifiers in a DBCS Environment

DBCS characters are not supported in any of the following identifiers:

  • Public procedure names

  • Public variables

  • Public constants

  • Project name (as specified in the Project Properties dialog box)

  • Class names (Name property of a class module, a user control, a property page, or a user document)


DBCS-Enabled KeyPress Event

The KeyPress event can process a double-byte character code as one event. The higher byte of the keyascii argument represents the lead byte of a double-byte character, and the lower byte represents the trail byte.

In the following example, you can pass a KeyPress event to a text box, whether the character you input is single-byte or double-byte.

Sub Text1_KeyPress (KeyAscii As Integer)
   Mychar = Chr(KeyAscii)
   ' Perform an operation using Mychar.
End Sub

For More Information See "KeyPress Event" in the Language Reference.


Calling Windows API Functions

Many Windows API and DLL functions return size in bytes. This return value represents the size of the returned string. Visual Basic converts the returned string into Unicode even though the return value still represents the size of the ANSI or DBCS string. Therefore, you may not be able to use this returned size as the string's size. The following code gets the returned string correctly:

buffer = String(145, Chr(" "))
   ret = GetPrivateProfileString(section, _
   entry, default, buffer, Len(buffer)-1, filename)
   retstring = Left(buffer, Instr(buffer, Chr(0))-1))

For More Information For more information, see "Accessing the Microsoft Windows API" in "Accessing DLLs and the Windows API" of the Component Tools Guide, available in the Professional and Enterprise editions.


Visual Basic Bidirectional Features

Visual Basic is bidirectional (also known as "BiDi")-enabled. Bidirectional is a generic term used to describe software products that support Arabic and other languages which are written right-to-left. More specifically, bidirectional refers to the product ability to manipulate and display text for both left-to-right and right-to-left languages. For example, displaying a sentence containing words written in both English and Arabic requires bidirectional capability.

Microsoft Visual Basic includes standard features to create and run Windows applications with full bidirectional language functionality. However, these features are operational only when Microsoft Visual Basic is installed in a bidirectional 32-bit Microsoft Windows environment, such as Arabic Microsoft Windows 95. Other bidirectional 32-bit Microsoft Windows environments are available as well.

The RightToLeft property has been added to forms, controls, and other Visual Basic objects to provide an easy mechanism for creating objects with bidirectional characteristics. Although RightToLeft is a part of every Microsoft Visual Basic installation, it is operational only when Microsoft Visual Basic is installed in a bidirectional 32-bit Microsoft Windows environment.

Bidirectional Features documentation describes all Microsoft Visual Basic bidirectional features. You can find this information under Additional Information under Reference in the Visual Basic table of contents. Click the See Also under the title of this topic to go directly to the overview topic.

For compatibility with Microsoft Visual Basic 4.0, two versions of the 32-bit Grid control (Grid32.ocx) are included with Microsoft Visual Basic 6 but not installed. Both are located in the \Tools folder of the product media. The standard and bidirectional versions are located in the \Controls and \Controls\Bidi subfolders, respectively.


Distributing Your Applications

After you create a Visual Basic application, you may want to distribute it to others. You can freely distribute any application you create with Visual Basic to anyone who uses Microsoft Windows. You can distribute your applications on disk, on CDs, across networks, or over an intranet or the Internet.

When you distribute an application, there are two steps you must go through:

  • Packaging — you must package your application files into one or more .cab files that can be deployed to the location you choose, and you must create setup programs for certain types of packages. A .cab file is a compressed file that is well suited to distribution on either disks or the Internet.

  • Deployment — you must move your packaged application to the location users can install it from. This may mean copying the package to floppy disks or to a local or network drive, or deploying the package to a Web site.

You can use two tools to package and distribute your applications: the Package and Deployment Wizard (formerly the Setup Wizard), or the Setup Toolkit provided with your Visual Basic installation. The Package and Deployment Wizard automates many of the steps involved in distributing applications by presenting you with choices about how you want to configure your .cab files. The Setup Toolkit lets you customize some of what happens during the installation process.

Topics

The Package and Deployment Wizard

Introduces the Package and Deployment Wizard and explains the three main tasks you can perform using it. Also compares the wizard to the Setup Toolkit project.
Application Packaging with the Wizard

Explains the process of packaging your applications in preparation for deployment. Describes the three types of packages: standard packages, Internet packages, and dependency files.

Application Deployment with the Wizard

Explains the process of deploying your packaged applications to floppy disks, a location on a local or network drive, or to a site on the Internet or an intranet.
Managing Wizard Scripts

Explains packaging and deployment scripts and how they are used. Includes information on how to rename, copy, and delete the scripts you have created in previous sessions in the Package and Deployment Wizard.
The Setup Toolkit

Describes the Setup Toolkit project and how you can use it to add functionality and customization to the setup programs for your packages.
Manually Editing a Setup.lst File

Explains the function of a Setup.lst file, the sections that occur in it, and the proper format for each section of the file.
Manually Creating Distribution Media

Explains the process of manually creating your distribution media if you are not using the Package and Deployment Wizard to deploy your application.
Using the Package and Deployment Wizard with the Setup Toolkit

Explains how to modify the Setup Toolkit project and still use the Package and Deployment Wizard to create your setup program.
Testing Your Setup Program

Describes simple techniques for testing your application's installation.
Allowing the User to Remove Your Application

Describes how your application can be uninstalled from the user's machine and lists common problems that may be encountered.
Deploying Localized ActiveX Controls

Presents information on how to troubleshoot a problem you may encounter when deploying internationalized ActiveX controls with the Package and Deployment Wizard.


The Package and Deployment Wizard

The Visual Basic Package and Deployment Wizard helps you create .cab files for your application, group them into a unit, or package, that contains all information needed for installation, and deliver those packages to end users. You can use Visual Basic's Package and Deployment Wizard to create packages that are distributed on floppy disks, CDs, a local or network drive, or the Web. The Package and Deployment Wizard automates much of the work involved in creating and deploying these files.

The Package and Deployment Wizard offers three options

  • The Package option helps you package a project's files into a .cab file that can then be deployed, and in some cases creates a setup program that installs the .cab files. The wizard determines the files you need to package and leads you through all the choices that must be made in order to create one or more .cab files for your project.

  • The Deploy option helps you deliver your packaged applications to the appropriate distribution media, such as floppies, a network share, or a Web site.

  • The Manage Scripts option lets you view and manipulate the scripts you have saved from previous packaging and deployment sessions in the wizard. Each time you use the wizard, you save a script that contains all the choices you made. You can reuse these scripts in later sessions if you want to use similar settings and make the same choices as you did previously.

For More Information See "Application Packaging with the Wizard" later in this chapter for more information about packaging your projects. See "Application Packaging with the Wizard" later in this chapter for more information on deploying an application to the Web or another destination. See "Managing Wizard Scripts" for more information on how to create and use scripts.

The Package and Deployment Wizard vs. The Setup Toolkit Project

The Package and Deployment Wizard walks you through creating and distributing professional setup programs for your Visual Basic applications. In addition to creating .cab files for your application, the wizard also creates the application's setup program by compiling the Setup Toolkit project installed with Visual Basic. The setup program is called setup1.exe.

In most cases, the Package and Deployment Wizard is the best way to create and distribute your application's setup1.exe program. However, if you want your application's setup program to use features not provided by the Package and Deployment Wizard, you can do so by modifying the Setup Toolkit project. Like any other Visual Basic project, the forms, code, and functionality of this project can be modified or enhanced.

Note The Package and Deployment Wizard and the Setup Toolkit create setup programs and distribution media only for Visual Basic applications. To create setup programs for other Windows–based applications, use the setup toolkit provided with that development product or in the Microsoft Windows SDK.

For More Information For more information on using the toolkit project to package and deploy your applications, see "The Setup Toolkit" later in this chapter.


Starting the Visual Basic Package and Deployment Wizard

Starting the Package and Deployment Wizard

The Visual Basic Package and Deployment Wizard makes it easy for you to create the necessary .cab files and setup programs for your application. Like other wizards, the Package and Deployment Wizard prompts you for information so that it can create the exact configuration you want.

There are three ways you can start the Package and Deployment Wizard:

  • You can run it from within Visual Basic as an add-in. If you run the wizard as an add-in, you must first set the necessary references in the Add-In Manager to load the wizard. When you use the wizard as an add-in, Visual Basic assumes that you want to work with the project you currently have open. If you want to work with another project, you must either open that project before starting the add-in, or use the wizard as a stand-alone component.

  • You can run it as a stand-alone component from outside the development environment. When you run the wizard as a stand-alone component, you are prompted to choose the project on which you want to work.

  • You can start it in silent mode by launching it from a command prompt. See "Running the Wizard in Silent Mode" in this topic for more information.

After you start the wizard, a series of screens prompt you for information about your project and let you choose options for the package. Each screen explains how it is to be used, including which information is optional, and what information must be entered before you can move to the next screen. If you find that you need more information on any screen, press F1 or click the Help button.

Note You should save and compile your project before running the Package and Deployment Wizard.

In most cases, the Package and Deployment Wizard is all you need to create a package that is ready for deployment. However, if you want to customize your packaging process further or provide functionality not supported by the Package and Deployment Wizard, you can modify the Setup Toolkit Project.

To start the Package and Deployment Wizard from within Visual Basic

  1. Open the project you want to package or deploy using the wizard.

    Note If you are working in a project group or have multiple projects loaded, make sure that the project you want to package or deploy is the current project before starting the wizard.

  2. Use the Add-In Manager to load the Package and Deployment Wizard, if necessary: Select Add-In Manager from the Add-Ins menu, select Package and Deployment Wizard from the list, then click OK.

  3. Select Package and Deployment Wizard from the Add-Ins menu to launch the wizard.

  4. On the main screen, select one of the following options:
    • If you want to create a standard package, Internet package, or dependency file for the project, click Package.

    • If you want to deploy the project, click Deploy.

    • If you want to view, edit, or delete scripts, click Manage Scripts.

    For an introduction to these options, see "The Package and Deployment Wizard."

  5. Proceed through the wizard screens.

To start the Package and Deployment Wizard as a stand-alone component

  1. If the project you want to package is open, save it and close Visual Basic.

  2. Click the Start button, and then click Package and Deployment Wizard from the Visual Basic submenu.

  3. In the Project list on the initial screen, choose the project you want to package.

    Note You can click Browse if your project is not in the list.

  4. On the main screen, select one of the following options:
    • If you want to create a standard package, Internet package, or dependency file for the project, click Package.

    • If you want to deploy the project, click Deploy.

    • If you want to view, edit, or delete scripts, click Manage Scripts.
  5. Proceed through the wizard screens.

Running the Wizard in Silent Mode

Using scripts, you may package and deploy your project files in silent mode. In silent mode, the wizard runs without your having to attend it to make choices and move through screens. The wizard packages and deploys your project using the settings contained in a script.

Silent mode is especially useful if you are packaging and deploying as part of a batch process. For example, early in the development of your project, you may use the Package and Deployment Wizard to package your project and deploy it to a test location. You can later create a batch file to perform the same packaging and deployment steps periodically as you update your project.

To package and deploy in silent mode

  1. Open an MS-DOS prompt.

  2. Type the name of the wizard executable, pdcmdln.exe, followed by the path and file name of your Visual Basic project, and the appropriate command line arguments, as shown in the following example:
    PDCmdLn.exe C:\Project1\Project1.vbp /p "Internet Package"
   /d Deployment1 /l "C:\Project1\Silent Mode.log"

    Note You can perform packaging and deployment in a single silent session by specifying both the /p and the /d arguments, as shown in the example above. Otherwise, use either /p or /d.

    Argument | Description
    /p packagingscript | Type /p followed by the name of a previously saved packaging script to package the project silently according to the specified script.
    /d deploymentscript | Type /d followed by the name of a previously saved deployment script to deploy the project silently according to the specified script.
    /l path Specifies that the wizard should store all output from the wizard, such as error messages and success reports, to a file rather than displaying them on the screen.

    Type /l followed by the path and file name of a file in which output should be stored. If the file does not exist, the wizard creates it.

Note Any file or script name that includes spaces should be enclosed in quotation marks, as shown in the example above.

For More Information See "Application Packaging with the Wizard" for instructions on how to use the wizard to package your project. See "Application Deployment with the Wizard" for instructions on how to use the wizard to deploy your projects. See "Modifying the Setup Project" for more information on customizing the installation process.


Application Packaging in the Wizard

Application Packaging with the Wizard

Application packaging is the act of creating a package that can install your application onto the user's computer. A package consists of the .cab file or files that contain your compressed project files and any other necessary files the user needs to install and run your application. These files may include setup programs, secondary .cab files, or other needed files. The additional files vary based on the type of package you create.

You can create two kinds of packages — standard packages or Internet packages. If you plan to distribute on disk, floppy, or via a network share, you should create a standard package for your application. If you plan to distribute via an intranet or Internet site, you should create an Internet package.

In most cases, you will package your applications using the Package and Deployment Wizard, which is provided with Visual Basic. You can package applications manually, but the wizard provides valuable shortcuts and automates some of the tasks you would have to perform yourself in a manual packaging session.

Note In addition, you can use the Setup Toolkit and the Package and Deployment Wizard together. You can modify the Setup Toolkit project to customize your setup programs and add features that that Package and Deployment Wizard does not provide, then use the wizard to package and deploy the application.

In addition to creating standard and Internet packages, you can also use the packaging portion of the Package and Deployment Wizard to create dependency files. Dependency files list the run-time components that must be distributed with your application's project files.

Important Any time you create a package, you should be sure that the version number for your project has been set on the Make tab of the Project Properties dialog box. This is especially important if you are distributing a new version of an existing application: Without the appropriate change in version numbers, the end user's computer may determine that critical files do not need to be updated.

For More Information For a more detailed explanation of standard packages and their contents, see "Standard Packages" later in this chapter. See "Internet Packages" for more information about Internet packages and their contents. See "Using the Setup Toolkit Project with the Package and Deployment Wizard" later in this chapter for more information on using the two tools together to create customized setup programs. See "Dependency Files" for an explanation of the contents of a dependency file.

Files You Are Allowed to Distribute

You can freely distribute any application or component that you create with Visual Basic. In addition to an executable (.exe) file, your application might require other files, such as DLLs, ActiveX controls (.ocx files), or bitmaps (.bmp files).

You can legally distribute sample application files and any files that were originally copied to the \Icons subdirectory of the \Visual Studio\Common\Graphics directory when you first installed Visual Basic on your system. Microsoft makes no warranty, express or implied, regarding the merchantability or fitness of these applications, nor does Microsoft assume any obligation or liability for their use.

If you have purchased the Professional or Enterprise Edition of Visual Basic, you can also distribute any files originally copied to the \Visual Studio\Common\Graphics and \Program Files\Common Files\ODBC subdirectories .

Note You may also be able to distribute other ActiveX controls, .exe files, and DLLs that you have purchased. Consult the manufacturer's license agreement for each of the files you plan to distribute to determine whether you have the right to distribute the file with your application.

Overall Steps in the Packaging Process

Regardless of the type of package you create or the tool you use to create it, there are certain steps that must be taken.

Note The Package and Deployment Wizard performs many of these steps for you automatically.

  1. Determine the type of package you want to create. You can create a standard package for Windows-based programs that will be distributed on disk, on CD, or over a network; or you can create an Internet package for programs that will be distributed on the Web. You can also choose to create only a dependency file.

  2. Determine the files you need to distribute. The wizard must determine the project files and dependent files for your application before it can create the package. Project files are the files included in the project itself — for example, the .vbp file and its contents. Dependent files are run-time files or components your application requires to run. Dependency information is stored in the vb6dep.ini file, or in various .dep files corresponding to the components in your project.

  3. Determine where to install files on the user's machine. Program and setup files are usually installed into a subdirectory of the Program Files directory, while system and dependent files are usually installed into the \Windows\System or \Winnt\System32 directory. Your setup program must take this into account and determine where to install each file.

  4. Create your package. The wizard creates the package and the setup program (setup1.exe) for it, referencing all necessary files. The end result of this step is one or more .cab files and any necessary setup files.

  5. Deploy your package. The deployment process involves creating your distribution media and copying all necessary files to the location from which users can access it. For information on deployment, see "Application Deployment with the Wizard" later in this chapter.

For More Information For a full list of common run-time, setup, and dependency files, see "Files You Need to Distribute" later in this chapter. See "Dependency Files" later in this chapter for more information on creating a .dep file.


Packaging Features

Using the Package and Deployment Wizard, you can easily create a professional setup program for your application or deploy an Internet application to the Web. The wizard performs these steps during the packaging process:

  • Automatic inclusion of your application's main setup program (setup1.exe). The wizard adds the Setup Toolkit application, Setup1.exe, to the package. This file is the main installation program for your application.

  • Automatic creation of your application's .cab files. The Package and Deployment Wizard can create a single .cab file or multiple .cab files for your application.

  • Script-based sessions. You can select a script from another packaging session with the same project if you want to use the same or very similar settings as you move through the wizard. This can save you significant time. In addition, you can use a previously saved script to package a project in silent mode. This is especially useful as part of a batch compilation process.

  • Optional creation of dependency files. Dependency files identify the run-time files that must be included with your application when it is distributed.

  • Automatic support for data access, Remote Automation, and DCOM features. The wizard automatically determines whether your project includes functionality that changes the setup process. For example, if you include certain types of data access, Remote Automation, or DCOM features, you may need to include drivers or other files in your package. The wizard checks your projects and displays screens that allow you to specify the appropriate options in these cases.

  • Shared file capability. The wizard allows you to install some files as shared files. This means that the files will not be removed from the system during an uninstall if other applications are using them.

  • Alternate file locations for Internet packages. In Internet packages, the wizard allows you to specify whether dependency files should be included in the setup program or downloaded from an alternate Web site.

  • Safety settings for Internet packages. If you do not use the IObjectSafety interface in your project, the Package and Deployment Wizard lets you mark components in your application as safe.

  • Custom destination locations for each file in the project. Most files have a default location to which they are installed, depending on whether they are project files or system files. You can change these locations if you want to install the files to a different location.

For More Information See "Deployment Features" later in this chapter for more information on features of the Package and Deployment Wizard.


Standard Packages

A standard package is a package that is designed to be installed by a setup.exe program, rather than through the downloading of a .cab file from a Web site. You create standard packages for Windows-based applications that will be distributed through disks, CD, or a network share.

When you create a standard package, you must carefully consider the distribution method you plan to use prior to creating your package. If you plan to use floppies, you must usually create multiple .cab files that can be placed onto several disks, rather than one large .cab file. An option in the Package and Deployment Wizard lets you specify whether you want one or multiple .cab files and the .cab size to use (1.44 MB, 1.2 MB, etc.). If you select multiple .cab files, the wizard splits your application files into several sets that do not exceed the indicated size.

Important Even if the application you plan to distribute on floppy disks is small enough to fit on a single disk when packaged into one large .cab file, you should still choose the multiple .cab option so that you have access to the floppy disk deployment process later in the wizard. In this case, only one .cab file will be created.

If you're planning to deploy to a network or local share, to CDs, or to a Web site, you can create either one large .cab file or multiple smaller .cab files.

Parts of a Standard Package

There are several files that are always part of your standard packages. These include:

  • The setup.exe file. Setup.exe acts as a pre-installation executable. Setup.exe is the first thing run on the user's machine in the installation process, and it performs necessary processing that must occur before the main installation takes place.

  • The setup1.exe file. Setup1.exe acts as the main setup program for your application.

  • All required support files. Support files are stored in the \Support subdirectory, beneath the directory in which the package was created. In addition to the setup.exe and setup1.exe files, this directory contains the files necessary to customize the .cab files for the application if the users so desire.

  • The .cab files for your application. Both Internet and Windows-based applications are packaged into .cab files prior to distribution. A .cab file takes the place of what was, in previous versions of Visual Basic, a long list of compressed application files. All of those files are now contained within the .cab. You can have a single .cab for your application, or you can create multiple .cabs for floppy disk delivery.

Note If your application will be run on a bidirectional (BiDi) operating system, you will need to manually include the vbame.dll file in the Setup.lst created by the Package and Deployment Wizard. You can do this by adding the file at the Included Files screen when running the Package and Deployment Wizard, by editing the Setup.lst directly, or by adding an entry for vbame.dll to the vb6dep.ini file so that it will be automatically added to the Setup.lst whenever you run the Package and Deployment Wizard.

Data Access Features

If your application uses one of Visual Basic's data access technologies, such as Data Access Objects (DAO), ActiveX Data Objects (ADO), or Remote Data Objects (RDO), the Package and Deployment Wizard performs two additional steps during the packaging process:

  • If your application uses ADO, OLEDB, or ODBC components, the wizard automatically adds a file called mdac_typ.exe to the list of files to include in your package. Mdac_type.exe is a self-extracting executable that installs all of the necessary components you need for your data access technology.

  • The wizard prompts you to choose the appropriate data access option when your application includes DAO features. You choose the appropriate method — ISAM-based, ODBCDirect, ODBC through Jet, etc.

Remote Automation and DCOM Features

If your application utilizes remote code components (formerly called OLE servers), you need to create two packages for the application: a setup program for the client, and one for the server. You can use the Package and Deployment Wizard to package the application, simply by running it twice on the same project group — once on the client project, and once on the server project.

Before you package either the client or the server, you must make sure that you have created the necessary remote support (.vbr) files for the project and placed them in the same directory where the .vbp file for the project is located.

To create support for Remote Automation or DCOM

  1. Open the project group in Visual Basic and select the project that will act as the server.

  2. Select Project Properties from the Project menu. Select the Components tab of the Project Properties dialog box and check the Remote Server Files option.

    When you compile the project with this option selected, the .vbr file is created automatically.

Registry Files

If your project references any .reg or .vbl files, you will see an additional screen in the wizard where you can specify how this registry information should be treated. You can choose to simply copy the registry files to the end user's computer, or you can have the system store the information in the registry and automatically register it on the end user's computer.

Missing or Outdated Files

As you move through the wizard, a series of dialogs may appear if any files needed by your application are missing or if any files have missing or outdated dependency information. You can either choose to proceed without the dependency information for the component, locate the missing files, or permanently mark a file as requiring no dependencies.


Internet Packages

Internet packages are .cab-based setup programs that are designed to be downloaded from a Web site. Internet Explorer uses a process known as Internet Component Download to install your Internet application. The Package and Deployment Wizard automatically includes information needed for this process in the packages it creates.

There are several types of Visual Basic applications or components that can be packaged for Internet deployment, including:

  • ActiveX controls (.ocx files) that are displayed on a Web page.

  • ActiveX .exe or .dll files, designed to run on the client or the Web server.

  • ActiveX documents, that are displayed in place of a Web page.

  • DHTML applications, client-based applications that link HTML pages to Visual Basic code through the use of Dynamic HTML.

  • IIS applications, server-based applications that link HTML pages to an object called a webclass. The webclass intercepts server requests from the browser and responds to them with Visual Basic code.

For More Information Extensive information about the Internet Component Download process can be found in "Downloading ActiveX Components" in Building Internet Applications in the Component Tools Guide.

Parts of an Internet Package

There are several files that are always part of your Internet packages. These include:

  • The primary .cab file for your application. The primary .cab file for Internet packages is used as the setup program for your application. The primary .cab file includes project components, such as the executable or DLL for your application or your .ocx file for controls, an .inf file referencing secondary cabs and containing safety and registry information, and all required dependency files that are not in secondary .cabs.

  • All required support files. Support files for an Internet application may include HTML files, Active Server Pages (.asp) files, graphics files in a variety of formats, or other files your application must access to run.

  • Any secondary .cab files for your application. In addition to project files, applications often reference several run-time components, such as the Visual Basic run-time DLL, individual ActiveX controls, and data access objects. If these components are available online in prepackaged .cab files, you can reference those .cab files in your primary .cab, rather than shipping the files yourself.

Secondary .cab files provide an efficient way to ensure that the user has the most current version of components. If a newer version of a component in a secondary .cab file becomes available on the external Web site, users who download your application will receive the updated version automatically.

Note If you cannot or do not want your application setup to require a connection to the Internet, you may place the secondary .cab files on a server within your intranet. An intranet server often provides for faster downloading and allows users to download from a secure network.

How Internet Component Download Works

After you package your Internet application or component for download, you deploy it to a specific location on a Web server, from which users can access it. Usually, your package is referenced as part of an existing Web page — that is, your control or other component is hosted by a Web page.

When a user accesses the Web page that hosts your package, the system downloads your package to the user's computer. The package is verified for safety, unpacked, registered, installed, and then activated. All of this occurs in the background and is controlled by the browser.

The Package and Deployment Wizard plays two parts in the process described above:

  1. It packages your component and its associated files into a compressed (.cab) file that the browser uses to download your component. The Package and Deployment Wizard determines which files your project needs to run, gathers those files, compresses them into a .cab file, and generates the HTML that points to your component.

  2. It deploys your packaged files to the Web server location of your choice. For more information on deploying your Internet component download package, see "Application Deployment with the Wizard" later in this chapter.

Safety Issues

When you prepare Internet applications and components for download, you must package them into a file that can be delivered to the user through a browser. In addition, you must perform a few precautionary steps to ensure users that your application will not harm their computers. These steps can include:

  • Digitally signing your components so that users can verify the contents of the component and identify you as the software's source.

  • Setting safety levels to vouch that your components will not damage users' computers or corrupt their data.

  • Arranging for licensing of any components that require it. When you add an ActiveX control to a Web page, you are distributing it to any users who download the control from the page. Unless you license the control, there is little to prevent an end user from taking your control and using it in their own applications. The license acts as a kind of copyright for your control, preventing unauthorized use.

Safety settings can be made within the Package and Deployment Wizard. When you work with an Internet package, a screen in the wizard asks you to verify safety settings. This screen appears lists only the objects in your project that do not implement a safety interface called IObjectSafety.

Note Signing and licensing must be done outside of the packaging process. You should arrange licensing for any components before you package the component. Digital signing can be done after you package the application — the Package and Deployment Wizard reserves space within the .cab file for information about the digital signature.

For More Information See "Steps to Prepare your Component for Download" in the Building Internet Applications book of the Component Tools Guide for detailed explanations of component safety, licensing, and signing. See "Setting Safety Levels for ActiveX Components" in "Downloading ActiveX Components" in Building Internet Applications in the Component Tools Guide for more information on the IObjectSafety interface.


Creating Dependency Files

Dependency Files

A dependency (.dep) file contains information about the run-time requirements of an application or component — for example, which files are needed, how they are to be registered, and where on the user's machine they should be installed. You can create .dep files for standard projects in all versions of Visual Basic. If you have the Professional or Enterprise edition of Visual Basic, you can create .dep files for ActiveX controls, ActiveX documents, and other ActiveX components.

The Package and Deployment Wizard uses .dep files when it packages your applications. It scans all available dependency information for the application to build a comprehensive list of information about the run-time files the application needs, then builds installation information from that list. For a standard package, the information from the .dep files is written to a Setup.lst file that is stored outside the packaged .cab file. For an Internet package, the .dep file information is written to an .inf file that is stored within the packaged .cab file.

When you package a component, you have the option of creating a .dep file to accompany it when it is deployed. You would do this if you have created a component you want to distribute with dependency information. It is recommended that you package and deploy your component before you package and deploy your dependency file, so that the packaging portion of the wizard knows the source location of the component that the dependency file references.

Types of Dependency Files

In Visual Basic, dependency information is stored in files generated by the Package and Deployment Wizard or created manually by you. There are two types of files that can contain dependency information:

  • Component .dep files — a .dep file lists files needed by a particular control or component. The Package and Deployment Wizard uses this file when it creates the setup program. In addition, the wizard can create this type of .dep file for you.

  • The VB6dep.ini file — a list of dependency files for the entire Visual Basic development environment.

When you run the Package and Deployment Wizard, it looks for dependency information in .dep files and in vb6dep.ini. If dependency information cannot be found for a component in either location, the wizard notifies you of the missing dependency information. You can ignore this omission or correct the problem by creating the appropriate dependency files.

Note If you ignore the omission, your program may not function properly after installation. If, however, you are certain that a dependent file will already be loaded on the user's machine, you may ignore the warning and proceed.

Component Dependency Files

A .dep file lists all the files required by a particular component. When you purchase or use a component from a vendor, you receive a .dep file from them. For example, all of the ActiveX controls shipped with Visual Basic have a companion .dep file. These .dep files list all of the dependent files used by the control, plus version and registry information.

You should generate a .dep file for any component that you create in Visual Basic if that component may be used in another project. The information from the .dep file for each component in a project is combined to form the project's dependency information. If you do not create a .dep file for your component, the dependency information for any projects in which it is used may be incorrect.

The VB6dep.ini File

The VB6dep.ini file provides the Package and Deployment Wizard with an all-purpose list of dependencies and references used by Visual Basic. This list is created when you install Visual Basic and resides in the \Wizards\PDWizard subdirectory of the main Visual Basic directory.

Missing Dependency Information

The Package and Deployment Wizard will inform you if dependency information is missing for a component in your project. There are three ways you can add the necessary dependency information:

  • Edit the vb6dep.ini file to manually add an entry for a particular component.

  • Create a .dep file for the component with the Package and Deployment Wizard.

  • Contact the component's vendor and request a .dep file.


Application Deployment with the Wizard

Application deployment is the act of moving your packaged application to either the distribution media you have chosen or to a Web site from which it can be downloaded. There are two ways you can deploy your Visual Basic application:

  • You can use the Deployment portion of the Package and Deployment Wizard to deploy your application to floppy disks, a local or network drive, or to a Web site.

  • You can manually copy files to disks or shares, or you can manually publish files to the appropriate Web location.

The Package and Deployment Wizard provides shortcuts and automatically performs some of the same tasks you would have to perform if you manually deployed your application.

Overall Steps in the Deployment Process

Whether you deploy your packages with the Package and Deployment Wizard by hand, there are certain steps that must be taken.

  1. Create a package for deployment. This can be either a single .cab file or a series of .cab files, depending on how you plan to distribute the application.

  2. Identify the package you want to deploy. You can choose any valid package for the selected project.

  3. Choose a deployment method. You can deploy your application to the Internet, to floppy disks, or to a directory on a local or network drive.

  4. Choose the files to deploy. If you are deploying to the Internet, you can add and remove files from the list of files to be deployed.

  5. Determine the destination for deployed files. For Internet deployment, this involves specifying a Web site to which the package should be deployed. For directory deployment, this means indicating the drive location to which the package should be deployed. For floppy disk deployment, this means choosing the appropriate floppy drive.

  6. Deploy your package. If you are using the Package and Deployment Wizard, the wizard handles this process for you. If not, you must copy the files to the appropriate locations on your shared or local drive, or publish your files to the Web.


Deployment Features

Using the Package and Deployment Wizard, you can easily copy your packaged applications to the appropriate location. The Package and Deployment Wizard performs these steps, with your input, during the deployment process:

  • Choice of deployment methods. You can choose to deploy to floppy disks, a local or network drive, or an intranet or Internet site.

  • Script-based sessions. You can select a script from another deployment session for the same project if you want to use the same or very similar settings as you move through the wizard. This can save you significant time.

  • Automatic access to Web Publishing technology. Web Publishing technology makes it easy to publish files to an intranet or Internet site.

For More Information For more information on Web publishing see "Internet Tools and Technologies" in the Internet Client SDK. For more features of the wizard, see "Packaging Features" earlier in this chapter.


Deploying Your Application

The wizard offers you a choice between deploying to the Web using Web Publishing technology, or deploying to floppies or a directory on a local or network drive.

Deploying to Floppy Disks, Directories, and CDs

You can deploy to floppy disks using the Package and Deployment Wizard only if you have created a standard package using the Multiple Cabs option. This options ensures that your package consists of multiple .cab files or of a single .cab that is smaller than the size of a disk. The system gives you the option of formatting each disk before you copy your .cab files to it. You do not have to format the disk, but you must use empty disks for this deployment process.

If you choose to deploy to a directory, the system prompts you to pick a local or network directory into which your files will be copied. You can then either direct your users to access the setup program for your application from that location, or you can move your files onto CD-ROMs.

Note If you have a writeable CD drive, you may be able to copy your files directly to that drive using the deployment portion of the wizard, rather than deploying to a directories and then copying the files to your CDs.

Deploying to the Web

You can deploy any package, whether it is a standard or Internet package, to the Web. When you choose Web Publishing as your deployment method, the system considers the project folder to be the local base folder for your deployment. The local base folder is used to determine how files and directories should be copied to the Web site you choose. Files and directories that are within the local base directory will be deployed to the Web server with the same directory structure as the base directory.

Note By default, the wizard does not deploy source files from within the project directory or the \Support subdirectory. The packaging portion of the wizard creates the \Support directory and places files in it that can be used to recreate your .cab files.


Managing Wizard Scripts

When you work in the Package and Deployment Wizard, you can create and store scripts. A script is a record of the selections you made in a packaging or deployment session. Creating a script retains these selections so that you can apply them to future sessions in the wizard for the same project. Using scripts can save you significant time in your packaging and deployment sessions. In addition, you can use scripts to package and deploy your applications in silent mode.

Each time you package or deploy a project, Visual Basic stores information about that session as a script. All scripts for a project are stored in a special file within the application's project directory. You can view the full list of scripts for the current project by using the Manage Scripts option in the Package and Deployment Wizard. This option lets you:

  • View a list of all packaging or deployment scripts.

  • Rename a script.

  • Create a copy of a script with a new name.

  • Delete a script that you no longer need.

Caution If you remove a packaging script, the deployment portion of the Package and Deployment Wizard will no longer recognize the package created from that script as one that it can deploy. You will then have to repackage the file in order to be able to deploy it. Delete only those scripts you are certain you no longer need.

To view a list of scripts

  1. Start the wizard, and then select Manage Scripts from the main screen.

    Important If you have started the wizard as a stand-alone application, you must choose the Visual Basic project you want before selecting Manage Scripts.

  2. Choose the appropriate panel for the scripts you want to view.


The Setup Toolkit

The Setup Toolkit is a project installed with Visual Basic that is used by the Package and Deployment Wizard when it creates a setup program. The Setup Toolkit project contains the forms and code that the application's setup program uses to install files onto the user's computer. When you use the Package and Deployment Wizard, the wizard includes the setup1.exe file that the Setup Toolkit project creates. This file is used as the application's main installation file.

Note There are two setup programs involved in the installation process — setup.exe and setup1.exe. The setup.exe program performs pre-installation processing on the user's computer, including installing the setup1.exe program and any other files needed for the main installation program to run. Only setup1.exe is customizable through the Setup Toolkit.

In addition to playing a supporting role in the process of creating a setup program, the Setup Toolkit can be used to modify the screens seen in the installation process, or to create a setup program directly. You might create a custom setup program if you need to add additional functionality not supported by the wizard to your installation sequence.

The Setup Toolkit project resides in the \Wizards\PDWizard\Setup1 subdirectory of the main Visual Basic directory.

Caution The files in this project are the same files used by the output of the Package and Deployment Wizard. Do not modify them without making a backup copy in another directory first. If you modify setup1.exe, subsequent setup programs created by the Package and Deployment Wizard will use the modified version.

You use the Setup Toolkit by loading the Setup1.vbp file into Visual Basic and making modifications to the appearance or functionality of the project. In doing so, you may need to manually go through the steps that the Package and Deployment Wizard would otherwise do for you. The following sections describe steps in the process and explain how to determine which files you need to include in your setup, how to create a Setup.lst, how to create distribution media, and how to test your setup.

Overall Steps to Modify the Package and Deployment Wizard

When you modify the Setup Toolkit with the intention of changing the output created by the Package and Deployment Wizard, you follow these steps:

  1. Modify the Setup Toolkit project to contain any new prompts, screens, functions, code, or other information you want to include. When you are finished, compile the project to create setup1.exe.

  2. Run the Package and Deployment Wizard, following the prompts on each screen, to create your distribution media.

Overall Steps to Create a Custom Setup Program

When you create a setup program manually using the Setup Toolkit rather than the Package and Deployment Wizard, you must follow these steps:

  1. If necessary, modify the Setup Toolkit project to contain any new prompts, screens, functions, code, or other information you want to include.

  2. Determine the files you want to distribute, including all run-time, setup, and dependency files.

  3. Determine where to install the files on the users' computers.

  4. Manually create your Setup.lst file to reflect the names and installation locations of all files that must be included for your project.

  5. Determine how you will be distributing files.

  6. Create the .cab files for your project using the Makecab utility.

Tip You can use the Package and Deployment Wizard to create your .cab files, then modify the .cab files manually. When the wizard creates your .cab files, it creates a .ddf file and a batch file in the \Support subdirectory of your project directory. To modify the .cab files, edit the .ddf file, then run the batch file provided. The batch file in turn will run Makecab.exe to recreate your .cab files.

  7. Create the setup1.exe for your project by compiling the Setup Toolkit project with your changes.

  8. Copy your files to the distribution media, or manually publish your files to the Web site using the Web Publishing Wizard, available in the ActiveX SDK.

For More Information For more information on using the Web Publishing Wizard, see "Internet Tools and Technologies" in the Internet Client SDK. See "Modifying the Setup Project" later in this chapter for more information on modifying the Setup Toolkit project. See "Files you Need to Distribute" and "Where to Install Files on the User's Machine" for more information on how to place files on the user's computer. See "Manually Creating Distribution Media" for more information on copying your files to the appropriate media.


Modifying the Setup Project

You can modify the Setup1.vbp project if you want to add new screens, prompts, or events to the installation sequence created by the Package and Deployment Wizard. You write code in the setup program just as you would in any other Visual Basic program. A number of function calls are available that are especially useful in setup routines.

Some examples of situations in which you might modify the Setup Toolkit project include:

  • You need to add special user prompts during installation.

  • You want to create a customized look and feel for your setup program.

  • You want to display billboards during installation. Billboards present information about your product's features, service and support, registration, and other related information.

  • You want to use your own compression utility to copy your application's files to the distribution media.

Important Because the Package and Deployment Wizard uses the files in the Setup Toolkit project, you should always make a backup of the project before making any changes. In addition, you should back up the full contents of the Setup1 directory.

To modify the Setup Toolkit project

  1. Create a backup of \Wizards\PDWizard\setup1.exe and all contents of the \Wizards\PDWizard\Setup1 directory before making any changes.

  2. Open the setup1.vbp project from the \Wizards\PDWizard\Setup1 directory.

  3. Make any changes to the code, forms, or modules in this project.

  4. Save the project and compile it to create setup1.exe.

  5. If you are using the Package and Deployment Wizard to package your application, launch the Package and Deployment Wizard and create a package for your application.

  6. If you are creating your own custom setup package, continue through the steps outlined in "The Setup Toolkit."

    Important Any time you create a package, using either the Package and Deployment Wizard or the Setup Toolkit project, you should be sure that the version numbers for your project have been set on the Make tab of the Project Properties dialog box in Visual Basic. This is especially important if you are distributing a new version of an existing application — without the appropriate change in version numbers, the end user's computer may determine that critical files do not need to be updated.


Files You Need to Distribute

The first step in creating a custom setup program is to determine which files to distribute. All Visual Basic applications need a minimum set of files, referred to as bootstrap files, that are needed before your application can be installed. In addition, all Visual Basic applications require application-specific files, such as an executable file (.exe), data files, ActiveX controls, or .dll files.

There are three main categories of files needed to run and distribute your application:

  • Run-time files

  • Setup files

  • Application-specific files

Run-Time Files

Run-time files are files your application must have in order to work correctly after installation. These files are needed by all Visual Basic applications. The following are the run-time files for Visual Basic projects:

  • Msvbvm60.dll

  • Stdole2.tlb

  • Oleaut32.dll

  • Olepro32.dll

  • Comcat.dll

  • Asyncfilt.dll

  • Ctl3d32.dll

While these files are needed by all Visual Basic applications, they may not be necessary for every type of installation package. For example, when creating an Internet package, the Package and Deployment Wizard assumes that any computer capable of performing an Internet download already has all of these files except for Msvbvm60.dll. Therefore this is the only run-time file that the wizard includes in an Internet package.

Note Run-time files can be further classified by their installation location. See "Where to Install Files on the User's Machine" for more information.

Setup Files for Standard Packages

Setup files are all of the files required to set up your standard application on the user's machine. These include the setup executables (setup.exe and setup1.exe), the setup file list (Setup.lst), and the uninstall program (st6unst.exe).

Visual Basic applications that are designed to be distributed on disk, on CD, or from a network location use the same setup files, regardless of whether you use the Package and Deployment Wizard or the Setup Toolkit to create your setup programs. These files are listed below.

File name | Description
setup.exe | Program that the user runs to pre-install the files that are needed for your application to be installed on the user's machine. For example, the setup.exe file installs the setup1.exe file, the Visual Basic run-time DLL, and other files without which the rest of the setup process cannot run.
setup1.exe | The setup program for your Visual Basic application. This executable file is generated by the Setup Toolkit and included in the package by the Package and Deployment Wizard. You can rename this file as long as the new name is reflected in the Setup.lst file.
Setup.lst | Text file that contains installation instructions and lists all the files to be installed on the user's machine.
Vb6stkit.dll | Library containing various functions used in Setup1.exe.
St6unst.exe | Application removal utility.

Note Applications designed to be delivered over the Internet generally do not use any of these files. See "Internet Packages" earlier in this chapter for more information on the files involved in Internet delivery.

Application Dependencies

In order to run your application, end users will need certain files in addition to the common run-time files and special setup files. Many of these files will be obvious to you: the executable file, any data files, and any ActiveX controls that you used. The less obvious files are your project's other dependent files. For example, some of the ActiveX controls used by your project may in turn require other files. One of the tasks of the Package and Deployment Wizard is to determine the complete list of such required files.

For More Information See "Dependency Files" earlier in this chapter for information on using the Package and Deployment Wizard to create dependency files for your application.


Where to Install Files on the User's Machine

Before writing your setup program, you must determine where to install all of the necessary files on the user's machine. You record this information in your Setup.lst file. See "Manually Editing a Setup.lst File" in this chapter for more information on how to record this information in the file.

The files required by your application can be divided into several categories.

  • Program-specific files — files your application requires to run and that are not used by other applications.

  • Shared files — files used by your application but also accessed by other applications on the user's machine.

  • Remote Automation server components - files necessary for Remote Automation or DCOM functionality.

Each type of file is best installed in a different location.

Program Files

Program files are files your application must have in order to run and that are useful only in the context of your application — for example, the application's .exe file and its required data files.

Program files should be installed in the application directory that the user specifies during installation. Code in Setup1.vbp demonstrates how to write files to this location. By default, the Setup Toolkit uses the \Program Files directory as the root location to install applications onto Windows 95 or later and Windows NT systems. For example, Setup1 suggests that Project1 be installed in the \Program Files\Project1 directory.

Caution When installing a file on the user's machine, you should not copy an older version of the file over a new version. The CopyFile function in Setup1.bas uses the VerInstallFile API function to copy files to the user's machine. VerInstallFile will not overwrite an existing file with an older version.

Shared Application Files

Shared application files are files that can be used by more than one application on the system. For example, several different vendors may ship applications that use the same ActiveX control. If you create an application that uses the control, you should indicate in your installation program that the control's .ocx file is designed to be shared.

Shared files must be installed in a location that allows other applications to access the them. In most cases, this is \Program Files\Common Files for Windows 95 and Windows NT 4.0 or later.

When an end user uninstalls your application, the system only removes a shared file if there are no other applications that could use the file.

Remote Automation Components

Install Remote Automation server components to the \Windows\System or \Winnt\System32 directory. This ensures that your applications use the most current Remote Automation server components.

Tip You can use the $(WinSysPath) installation macro to ensure that these files are installed in the correct directory.


Manually Creating a Setup.lst File

Manually Editing a Setup.lst File

If you use the Package and Deployment Wizard, the wizard creates the Setup.lst file automatically. You can edit the file manually after creation if you need to customize it.

The Setup.lst file describes all the files that must be installed on the user's machine for your application and contains crucial information for the setup process. For example, the Setup.lst file tells the system the name of each file, where to install it, and how it should be registered. There are five sections to the Setup.lst file:

  • The BootStrap section — lists the core information about the application, such as the name of the main setup program for the application, the temporary directory to use during the installation process, and the text in the startup window that appears during installation.

  • The BootStrap Files section — lists all files required by the main installation file. Normally, this includes just the Visual Basic run-time files.

  • The Setup1 Files section — lists all other files required by your application, such as .exe files, data, and text.

  • The Setup section — contains information needed by other files in the application.

  • The Icon Groups section — contains information about the groups your installation process will create. Each member of this section has a correlating section containing the icons to be created in that group.

For More Information See "Format of the Bootstrap and Setup1 Files Sections" for information on the syntax for these sections.

The BootStrap Section

The BootStrap section contains all the information the setup.exe file needs to set up and launch the main installation program for your application.

Note Remember that there are two setup programs for your installation: setup.exe, which is a pre-installation program, and setup1.exe, which is compiled from the Setup Toolkit. The BootStrap section provides instructions to the setup.exe file.

The BootStrap section contains the following components:

Component | Description
SetupTitle | The title to show in the dialog box that appears as setup.exe is copying files to your system.
SetupText | The text to show within the dialog box that appears as setup.exe is copying files to your system.
CabFile | The name of the .cab file for your application, or the name of the first .cab file for your application if your package has multiple .cab files.
Spawn | The name of the application to launch when setup.exe finishes processing. In most instances, this will be the setup1.exe file.
TmpDir | The location you wish to use for the temporary files generated during the installation process.
Uninstall | The name of the application to use as an uninstall program. In general, this is st6unst.exe, which is automatically packaged into all packages created with the wizard.

The BootStrap Files Section

The BootStrap Files section lists all the files that must be loaded on the user's machine before your application and dependency files can be loaded. These pre-install, or bootstrap, files include the core files required to run any Visual Basic application, such as the Visual Basic run-time DLL (Msvbvm60.dll). The setup program installs these files prior to installing and launching the main installation program.

The following example shows entries in a typical BootStrap Files section:

[Bootstrap Files]
File1=@Msvbvm60.dll,$(WinSysPathSysFile),$(DLLSelfRegister),1/23/98 9:43:25 AM,1457936,6.0.80.23

File2=@OleAut32.dll,$(WinSysPath),$(DLLSelfRegister),1/21/98 11:08:26 PM,571152,2.30.4248.1

File3=@OlePro32.dll,$(WinSysPathSysFile),$(DLLSelfRegister),1/21/98 11:08:27 PM,152336,5.0.4248.1

The Setup1 Files Section

The Setup1 Files section contains all the other files required by your application, such as your .exe file, data, text, and dependencies. The setup program installs these files after it installs the core files listed in the Bootstrap Files section.

The following example shows entries in a typical Setup1 Files section:

[Setup1 Files]
File1=@LotsAControls.exe,$(AppPath),$(EXESelfRegister),,1/26/98 3:43:48 PM,7168,1.0.0.0

File2=@mscomctl.ocx,$(AppPath),$(DLLSelfRegister),,1/23/98 9:43:40 AM,1011472,6.0.80.23

The Setup Section

The Setup section of the Setup.lst file is simply a list of information used by other parts of the installation process. The following table lists the information contained in the Setup section.

Component | Description
Title | The name of the application as it will appear in the splash screen during installation, in the program groups on the Start menu, and in the program item name.
DefaultDir | The default installation directory. The user can specify a different directory during the installation process.
ForceUseDefDir | If left blank, the user is prompted for an installation directory. If set to 1, the application is automatically installed to the directory specified by "DefaultDir" in Setup.lst.
AppToUninstal | The name you wish to see as your application in the Add/Remove Programs utility in Control Panel.
AppExe | The name of your application's executable file, such as Myapp.exe.

The IconGroups Section

The IconGroups section contains information about the Start menu program groups created by the installation process. Each program group to be created is first listed in the IconGroups section, then assigned an individual section (Group0, Group1, Group2, etc.) that contains information about icons and titles for that group. Groups are numbered sequentially starting at zero.

The following example shows entries in a typical IconGroups section and related subsections:

[IconGroups]
Group0=MyTestEXE
Group1=Group1

[MyTestExe]
Icon1=my.exe
Title1=MyTestExe

[Group1]
Icon1=ReadMe.txt
Title1=ReadMe
Icon2=my.hlp
Title2=Help


Format of the Bootstrap and Setup1 Files Sections

The Bootstrap Files and Setup1 Files sections of the Setup.lst file contain a complete list of the files that the setup programs (setup.exe and setup1.exe) need to install on the user's computer. Each file is listed individually, on its own line, and must use the following format:

Filex= file,install,path,register,shared,date,size[,version]

Part | Meaning
Filex | A keyword that must appear at the beginning of each line. X is a sequence number, starting at 1 in each section and moving in ascending order. You cannot skip values.
File | The name of the file as it will appear after installation on the user's computer. This is usually the same as the Install argument. If you want this file to be extracted from a cab, place an @ at the beginning of the name (for example, @my.exe).
Install | The name of the file as it appears on any distribution media.
Path | The directory to which the file should be installed. Either an actual directory path, a macro indicating a user-specified path, or a combination of the two. See "Path Argument Macros in Setup.lst Files" later in this chapter for more information on the available macros.
Register | A key that indicates how the file is to be included in the user's system registry. See "Registry Keys in Setup.lst Files" later in this chapter for more information.
Shared | Specifies that the file should be installed as shared.
Date | The last date on which the file was modified, as it would appear in Windows Explorer. This information helps you to verify that you have the correct versions of the files on the setup disks.
Size | The file size as it would appear in Windows Explorer. The setup program uses this information to calculate how much disk space your application requires on the user's machine.
Version | An optional internal version number of the file. Note that this is not necessarily the same number as the display version number you see by checking the file's properties.


Path Parameter Macros in Setup.lst Files

The Path argument in the Setup.lst file represents the location to which the file should be installed. The value used for this argument is either an actual path or a macro indicating a path specified by the user. The following tables list the macros that can be used in the installation.

Macro | Installs a file in this directory | Valid for this section
$(WinSysPath)
  • \Windows\System (Windows 95 or later)

  • \Winnt\System32 (Windows NT)
 Setup1 Files and Bootstrap Files
$(WinSysPathSysFile)
  • \Windows\System (Windows 95 or later)

    The file is installed as a system file and is not removed when the application is removed.

  • \Winnt\System32 (Windows NT)
 Setup1 Files only
$(WinPath)
  • \Windows (Windows 95 or later)

  • \Winnt (Windows NT)
 Setup1 Files and
BootStrap Files
$(AppPath) | The application directory specified by the user, or the DefaultDir value specified in the Setup section. | Setup1 Files only
$(AppPath)\Samples | \Samples, beneath the application directory. | Setup1 Files only
\path (for example, c:\) | The directory identified by path (not recommended). | Setup1 Files only
$(CommonFiles) \Program Files\Common Files\

Often combined with a subdirectory, as in $(CommonFiles)\My Company\My Application.

 Setup1 Files only
$(CommonFilesSys) | $(CommonFiles)\System | Setup1 Files only
$(ProgramFiles) | \Program Files | Setup1 Files only


Register Keys in Setup.lst Files

Registry Keys in Setup.lst Files

The Register key in the Setup.lst file indicates how the file should be registered on the user's computer. You can indicate that the file does not need to be registered, or indicate one of several registry options for files that do need registration.

The following table lists the possible keys.

Register Key | Meaning
(no key) | The file does not contain linked or embedded objects and does not need to be registered.
$(DLLSelfRegister) | The file is a self-registering .dll, .ocx, or any other .dll file with self-registering information.
$(EXESelfRegister) | The file is an ActiveX .exe component created in Visual Basic, or any other .exe file that supports the /RegServer and /UnRegServer command-line switches.
$(TLBRegister) | The file is a type library file and should be registered accordingly.
$(Remote) | The file is a remote support file (.vbr) and should be registered accordingly.
Filename.reg | The file is a component you distribute that needs to be registered but does not provide self-registration. This key indicates a .reg file that contains information that needs to be updated in the system registry. The .reg file must also be added to your Setup.lst file and installed.

The filename.reg key is not the recommended method of getting registration information into the registry. Registry entries added in this manner cannot be automatically uninstalled with the Application Removal program.


Specifying Remote Server Components in the Setup.lst File

If you use remote server components in your applications, you must place an entry in the Setup1 Files section marking a file as a remote server component and specifying connection information. This information is used by the Client Registration utility to register the Remote Automation server.

The following example shows how you specify remote server components:

File1=@LotsAControls.vbr,$(WinSysPath),$(Remote),,1/26/98 3:43:48 PM,1024,1.0.0.0
File2=@Server2.vbr,$(WinSysPath),$(Remote),,1/23/98 9:43:40 AM,1024,6.0.80.23
Remote1="Schweizer","ncacn_ip_tcp",1,RA
Remote2=,,1,DCOM

The Remotex entry consists of the server address, network protocol, and authentication information, separated by commas. You must also specify whether the component is to be used in a Remote Automation or DCOM environment using the RA or DCOM arguments.


Manually Creating Distribution Media

There are several ways you can distribute your application — on floppy or compact discs, over a network, or on a Web site. As part of the deployment process, you must copy your packaged files onto the distribution media you plan to use.

Note If you package and deploy your files using the Package and Deployment Wizard, these steps are not necessary because the wizard will create your disks, network location, or Internet component download distribution media for you automatically.

You can use any of the following methods when you manually create distribution media:

  • If you plan to distribute on floppies, you must copy files onto the disks in a specific order. You must place the setup.exe and Setup.lst files onto the first disk, followed by all the files in the Bootstrap Files section of your project's Setup.lst file, then place other .cab files onto the remaining disks.

  • To distribute on a network share, you simply copy your packaged files to the appropriate location, using Windows Explorer, a command prompt, or some other method.

  • To distribute on CD, you simply copy your files to the CDs.

  • To distribute on the Internet, you copy your Internet package to the appropriate Web site, using the Web Publishing Wizard, available in the ActiveX SDK.

For More Information For more information on creating a Web publishing site using the Web Publishing Wizard, see "Internet Tools and Technologies" in the Internet Client SDK.


Creating Distribution Media

You can create your distribution media after you have determined the files to include in your setup program, created the Setup.lst file, compressed any necessary files, and decided how to lay out files on the media:

  • If you are using floppy disks for your distribution, you need to copy the files to one or more disks.

  • If you are using another mechanism, such as CDs or a network location, you need to copy your files to the appropriate location or staging area for CD production.

  • For Internet or intranet delivery, you need to publish your files to the appropriate Web site, after formatting them for Internet component download.

Creating Distribution Disks

It is a good practice to label your distribution disks with the number and name of the disk and instructions for setting up your application. Repeat this installation message on each of the distribution disks. After your disks are labeled, you are ready to copy your files to the distribution disks.

To create distribution disks

  1. Copy the pre-installation files listed below onto the first disk:
    • Setup.exe

    • Setup.lst
  2. Copy the remaining bootstrap .cab files onto the rest of the first disk and any subsequent disks that are needed.

  3. After all bootstrap .cab files are copied, copy the rest of your .cab files onto the necessary disks.

Manually Deploying to a Web Site

If you want to manually deploy your application or component to a site on the Web, you can use the Web Publishing Wizard to deploy your packaged files to the appropriate location. The Web Publishing Wizard is available on the Microsoft Web site at www.microsoft.com, and is also installed with Internet Explorer 4.x.

For More Information See "Internet Tools and Technologies" in the Internet Client SDK for more information on the Web Publishing Wizard. See "Manually Creating Distribution Media" earlier in this chapter for information about deciding how to place your files on the disks.


Using the Package and Deployment Wizard with the Setup Toolkit

In addition to using the Setup Toolkit project to create your own custom setup project, you can use the Setup Toolkit project in conjunction with the Package and Deployment Wizard. In this case, you use the Setup Toolkit project to customize the screens or other parts of the installation sequence, then use the wizard to create and deploy the package for the application.

For example, you might use the Setup Toolkit and the Package and Deployment Wizard together to add dialog boxes to the installation program, prompting the user to specify whether to install optional features in your application. For example, you may have an online Help file that some users would rather not install. You can add as many installation options as you want.

To add an installation option to your setup program

  1. In the Setup1.vbp project, edit the code for the Form_Load event in the setup1.frm form. To add functionality, you add code after the code block calls the ShowBeginForm function (Sub ShowBeginForm).

    The following shows an example of how you would add a dialog box that asks if the user wants to install optional files:

    Dim LoadHelp As Integer
LoadHelp = MsgBox ("Do you want to install Help? ", vbYesNo)
If LoadHelp = vbYes Then
   CalcDiskSpace "Help"
EndIf
' Block of code containing cIcons = CountIcons(strINI FILES)
If LoadHelp = vbYes Then
   cIcons = CountIcons("Help")
EndIf
' Block of code containing CopySection strINI_FILES.
If LoadHelp = vbYes Then
   CopySection "Help"
EndIf
' Block of code containing CreateIcons, strINI FILES, strGroupName
  2. Close Setup1.frm, save the form and the Setup Toolkit project, and compile to create the Setup1.exe file.

  3. Run the Package and Deployment Wizard, and select Package from the main screen.

  4. Proceed through the wizard, making the appropriate choices. For the example shown above, you would make sure that all optional files the user could choose to install in your custom dialog box were listed in the Add and Remove screen.

  5. Once you are done with the Package and Deployment Wizard, generate the distribution media.

  6. Make any necessary changes to the Setup.lst file. In the example above, you would add a new section with a section you used in the CopySection section of your code. In this case, your section would look something like this:
    [Help]
File1=MyApp.HL1,MyApp.HLP,$(AppPath),,,10/12/96,2946967,0.0.0
  7. Deploy and test your package.

    8.

When the user runs the installation program for the example shown in this procedure, the setup program copies all the BootStrap files to the user's machine and then prompts the user to indicate whether to install the Help files. If the user chooses Yes, the CalcDiskSpace statement determines whether there is sufficient disk space on the user's machine for the Help files. The program then installs all of the files listed with the Setup1 Files section in Setup.lst.

Next, the program tests the LoadHelp flag again. If the user chose to install the Help files, Setup1.exe next executes the CopySection statement for the Help files, and installs the files listed in the [Help] section of Setup.lst.

For More Information   See "The Package and Deployment Wizard" earlier in this chapter for more information on features of the wizard.


Testing Your Setup Program

After you have completed the packaging process and produced distribution media for your application, you must test your setup program. Be sure to test your setup program on a machine that does not have Visual Basic or any of the ActiveX controls required by your application. You should also test your setup on all applicable operating systems.

To test your floppy disk-based or CD-based setup program

  1. Insert the first disk or the CD in the appropriate drive.

  2. In Windows 95 and Windows NT 4.0 or later, from the Start menu, choose Run, and type:

    drive:\setup
    -or-
    Double-click Setup.exe from the disk drive.

  3. When the installation finishes, run the installed program to be sure it behaves as expected.

To test your network drive-based setup program

  1. From another computer on the same network as the distribution server, connect to the server and directory containing your distribution files.

  2. In the distribution directory, double-click the Setup.exe file.

  3. When the installation finishes, run the installed program to be sure it behaves as expected.

To test your Web-based setup program

  1. Deploy your package to a Web server.

  2. Access the Web page from which your application's .cab file can be referenced. Download will begin automatically, and you will receive prompts asking you if you want to proceed.

  3. When the installation finishes, run the installed program to be sure it behaves as expected.


Allowing the User to Remove Your Application

When the user installs your application, the setup program copies the application-removal utility St6unst.exe to the \Windows or \Winnt directory. Each time you use the Visual Basic setup program to install an application, an application removal log file (St6unst.log) is generated in the application's installation directory. The .log file contains entries indicating:

  • Directories created during installation.

  • Files installed and their location. This list contains all of the files in the setup program, even if some files were not installed on the user's machine because a newer version of the same file already existed. The log file indicates whether the file was a shared file and if so, whether it replaced the existing file.

  • Registry entries created or modified.

  • Links and Start menu entries created with Windows 95 and Windows NT 4.0 or later.

  • Self-registered .dll, .ex, or .ocx files.

In Windows 95 and Windows NT 4.0 or later, the setup program adds the application removal utility to the list of registered applications displayed in the Add/Remove Programs section of Control Panel. End users should use Add/Remove Programs to uninstall the application.

Caution It is important to make sure that you have correctly set options for any files that should be shared, either by adding the files to the Shared Files screen in the Package and Deployment Wizard, or by indicating the installation location for the file as a shared file directory. If you accidentally install a file that should be shared without the correct settings, users will be able to remove it when they uninstall your application, which may cause problems for other applications on your system.

In the event of a failed or canceled installation, the application-removal utility automatically removes all of the directories, files, and registration entries that the setup program created in the installation attempt.

With Windows 95 or later and Windows NT, shared files are reference-counted in the registry. For example, a shared file used by three applications will have a reference count of three. When you remove an application that uses a shared file, the reference count for the shared file decreases by one. When the count for the file reaches zero, the user will be prompted for final removal of that item.

Situations in Which the Application Removal Utility Might Fail

For the application-removal utility to properly uninstall your application, the log file and registry entries created by the setup program must be accurate and unchanged from the time of installation.

The Application Removal utility might fail or work incorrectly if any of the following situations exists:

  • The end user copied shared files manually. In this case, reference counting is not updated in the registry, so the system cannot correctly judge when to remove the file.

  • The end user deletes installed files or an application directory rather than using the application-removal utility. This deletes the log file, so that application removal is impossible. In addition, it makes it impossible to remove system registry entries for program files, DLLs, or .ocx files in the application directory because these files must be run in order to be removed.

  • A setup program for an application that is not compliant with Windows 95 installs the same shared files as does your application.

  • A shared file is installed into a different directory than the one in which it already exists on the hard drive.

  • The end user installs the same Visual Basic application in two different directories. Not only will the first installation no longer work, but the application removal scenarios will clash. The end user should always remove the first installation before installing an application in a different directory.

  • The end user deletes the application setup log (St6unst.log). Without the application setup log file, the application-removal utility has no installation information and will fail.

Some of these scenarios could degrade the installed file-registry correspondence, cause the Application Removal utility to prematurely reach a zero-reference count for a particular file, and subsequently ask if this file could be deleted. If a file is prematurely deleted, it could cause other applications to cease functioning or function incorrectly due to missing file dependencies, missing components, and so on.


Distributing Localized ActiveX Controls

In most ways, distributing localized ActiveX controls is no different from distributing the original version. A problem may arise, however, if a new version of the control is released and distributed on the Web with another application.

ActiveX controls shipped with Visual Basic are automatically translated, or localized, into the correct language for the version of Visual Basic you own. For example, in the Japanese version of Visual Basic, controls are available in Japanese and are ready for distribution in that language. When you distribute these controls, the Package and Deployment Wizard must determine the necessary files to package with them.

Note This topic applies only to the ActiveX controls distributed with Visual Basic. It does not apply to user controls created with Visual Basic unless they contain a Visual Basic ActiveX control.

Visual Basic's localized ActiveX controls have two parts:

  • The .ocx file for the control, which acts as the code block for the control. The same .ocx file is used for every language version of Visual Basic.

  • The satellite DLL file, which contains the localized strings for the control. This acts as the data block for the control. The satellite DLL varies between versions, according to the language version of Visual Basic you have purchased.

The Package and Deployment Wizard automatically includes the correct satellite DLL when you package an application that contains a localized Visual Basic ActiveX control. When the end user downloads the application, the correct satellite DLL is installed on their computer.

If a new version of the control is distributed on the Internet, the .ocx file and the satellite DLL will be out of sync unless the new, localized satellite DLL is also downloaded with the second application. When that happens, the end user may suddenly find that the strings for the new control are either wrong or are in English, because the .ocx defaults to using the English strings (which are always available in the .ocx file) when the compatible satellite DLL is not found. To alleviate this problem, you should instruct end users to download the latest version of the satellite DLL from www.microsoft.com/vstudio/.

For More Information See "Designing International Software" in "International Issues" for an explanation of code blocks and data blocks.


Visual Basic Specifications, Limitations, and File Formats

This appendix describes the system requirements, limitations on a Visual Basic project, the types of files that my be included in your Visual Basic project, and descriptions of the form (.frm) and project (.vbp) files.

Note While many of these limitations are described in terms of a specific number, keep in mind that other conditions (such as available memory and system resources) may impose a restriction before the specified limit is reached.

Topics

System Requirements for Visual Basic Applications

System requirements for applications created in Visual Basic.
Project Limitations

Limitations that restrict how you will write code, how many controls you can use in an application, or how data input is managed.
Project File Formats

Design- and run-time files that my be included in your Visual Basic project.
Form Structures

Descriptions of the form (.frm) and project (.vbp) files.


System Requirements for Visual Basic Applications

The following hardware and software is required for Visual Basic applications:

  • Pentium® 90MHz or higher microprocessor.

  • VGA 640x480 or higher-resolution screen supported by Microsoft Windows.

  • 24MB RAM for Windows 95, 32MB for Windows NT.

  • Microsoft Windows NT 3.51 or later, or Microsoft Windows 95 or later.

  • Microsoft Internet Explorer version 4.01 or later (version 4.01 Service Pack 1 or later for DHTML application developers, and 4.x for end-users of these applications).

  • Disk space requirements:
    • Standard Edition: typical installation 48MB, full installation 80MB.

    • Professional Edition: typical installation 48MB, full installation 80MB.

    • Enterprise Edition: typical installation 128MB, full installation 147MB.

    • Additional components (if required): MSDN (for documentation): 67MB, Internet Explorer 4.x: approximately 66MB.
  • CD-ROM (no MS-DOS support assumed).


Project Limitations

A single project can contain up to 32,000 "identifiers" (any nonreserved keyword), which include, but are not limited to, forms, controls, modules, variables, constants, procedures, functions, and objects. Note that the actual number of identifiers is limited to available memory.

Variable names in Visual Basic can be no longer than 255 characters, and the names of forms, controls, modules, and classes cannot be longer than 40 characters. Visual Basic imposes no limit on the actual number of distinct objects in a project.


Control Limitations

Each nongraphical control (all the controls except shape, line, image, and label) uses a window. Each window uses system resources, limiting the total number of windows that can exist at one time. The exact limit depends on the available system resources and the type of controls used.

To reduce consumption of system resources, use the shape, line, label, and image controls instead of picture box controls to create or display graphics.

Total Number of Controls

The maximum number of controls allowed on a single form depends on the type of controls used and available system resources. However, there is a fixed limit of 254 control names per form. A control array counts only once toward this limit because all the controls in the array share a single control name.

The limit on control array indexes is 0 to 32,767 on all versions.

If you layer controls on top of each other, such as using several frame controls within other frames, Visual Basic will generally accept no more than 25 levels of nested controls.

Limitations for Particular Controls

The following table lists property limitations that apply to particular controls in Visual Basic.

Property | Applies to | Limitation
List and ListCount | List box and combo box controls | Maximum number of items is 32K; the limit on the size of each item is 1K (1024 bytes).
Text | Text box control | Limited to 64K.
Caption | Label control | Limited to 1024 bytes.
 | Command button, check box, frame, and option button controls | Limited to 255 characters. Any caption over these limits is truncated. Captions on custom control properties are limited to 32K.
 | Menu control | Limited to 235 characters.
Tag | All controls | Limited only by available memory.
Name | All controls | Limited to 40 characters.

Note In Visual Basic, control property names are limited to 30 characters.


Code Limitations

The amount of code that can be loaded into a form, class, or standard module is limited to 65,534 lines. A single line of code can consist of up to 1023 bytes. Up to 256 blank spaces can precede the actual text on a single line, and no more than twenty-five line-continuation characters ( _) can be included in a single logical line.

Procedures, Types, and Variables

There is no limit on the number of procedures per module. Each procedure can contain up to 64K of code. If a procedure or module exceeds this limit, Visual Basic generates a compile-time error. If you encounter this error, you can avoid it by breaking extremely large procedures into several smaller procedures, or by moving module-level declarations into another module.

Visual Basic uses tables to store the names of identifiers (variables, procedures, constants, and so on) in your code. Each table is limited to 64K.

DLL Declare Table

Each form and code module uses a table that contains a structure describing a DLL entry point. Each structure uses approximately 40 bytes, with a total restricted size of 64K, resulting in roughly 1,500 declarations allowed per module.

Project-Name Table

The entire application uses a single table that contains all names. These include:

  • Constant names

  • Variable names

  • User-defined — type definition names

  • Module names

  • DLL-procedure declaration names

The project name table is unlimited in total size, but is limited to a total of 32K case-sensitive unique entries. If the limit is reached, reuse private identifiers in different modules to limit the number of unique entries to 32K.

Import Table

Every reference to an identifier in a different module creates an entry in the Import Table. Each such entry is a minimum of 24 bytes and is restricted to 64K, resulting in roughly 2,000 references per module.

Module-Entries Table

This table accepts up to 125 bytes per module, with a total limit of 64K, resulting in about 400 modules per project.


Data Limitations

The following limitations apply to variables in the Visual Basic language.

Form, Standard, and Class Module Data

The data segment (that is, the data defined in the Declarations section) of the VBA module of any form or module in Visual Basic can be up to 64K. This data segment contains the following data:

  • Local variables declared with Static.

  • Module-level variables other than arrays and variable-length strings.

  • 4 bytes for each module-level array and variable-length string.

Procedures, Types, and Variables

If a procedure or module exceeds the 64K code limit, Visual Basic generates a compile-time error.

If you define a procedure that has more than 64K of local variables defined, you get the error "Too many local nonstatic variables."

If you define a module that has more than 64K of module-level variables defined, or if you define a User-Defined Type larger than 64K, you get the error "Fixed or static data can't be larger than 64K."

If you encounter this error, you can avoid it by breaking extremely large procedures into several smaller procedures, or by moving module-level declarations into another module.

An array declared as a variable doesn't contribute to the entire size of the array; only the array descriptor counts toward the 64K limit. So it is acceptable, for example, to have a declaration such as Dim x(1000000) As Byte either in a procedure or at module level. Out of memory problems occur, however, if you declare a large, fixed-size array in a record, then declare instances of those records as variables.

User-Defined Types

No variable of a user-defined type can exceed 64K, although the sum of variable-length strings in a user-defined type may exceed 64K (variable-length strings occupy only 4 bytes each in the user-defined type; the actual contents of a string are stored separately). User-defined types can be defined in terms of other user-defined types, but the total size of the types cannot exceed 64K.

Stack Space

Arguments and local variables in procedures take up stack space at run time. Module-level and static variables do not take up stack space because they are allocated in the data segment for forms or modules. Any DLL procedures you call use this stack while they are executing.

Visual Basic itself uses some of the stack for its own purposes, such as storing intermediate values when evaluating expressions.

Total available stack size for Visual Basic is one megabyte (1MB) per thread. A stack may grow beyond this, however, if there is adjacent free memory.

For More Information For tips on conserving stack space, see "Designing for Performance and Compatibility."


System Resource Limitations

Some limitations on Visual Basic, and the applications you create with it, are imposed by Microsoft Windows. These limitations may change when you install a different version of Microsoft Windows.

Windows Resources

Every open window uses some system resources (data areas used by Microsoft Windows). If you run out of system resources, the run-time error "Windows is running low on available resources" occurs. You can check the percentage of system resources remaining by choosing About from the Help menu in the Program Manager or File Manager in Windows NT 3.51, or, in Windows 95 and Windows NT 4.0, by choosing About in the Windows Explorer Help menu. Applications can also call the Windows API GetFreeSystemResources to reclaim system resources, close windows (such as open forms and Code windows, as well as windows in other applications), and exit running applications.


Project File Formats

Microsoft Visual Basic utilizes and creates a number of files at both design and run time. Which files will be required by your project or application depends upon its scope and functionality.

Project File Extensions

Visual Basic produces a number of files when you create and compile a project. These can be categorized as follows: design-time, miscellaneous development, and run-time.

Design time files are the building blocks of your project: basic modules (.bas) and form modules (.frm), for example.

Miscellaneous files are produced by various processes and functions of the Visual Basic development environment: Package and Deployment Wizard dependency files (.dep), for example.

Design-time and Miscellaneous Files

The following table lists all the design-time and miscellaneous other files that may be produced when you develop an application:

Extension | Description
.bas | Basic module
.cls | Class module
.ctl | User Control file
.ctx | User Control binary file
.dca | Active Designer cache
.ddf | Package and Deployment Wizard CAB information file
.dep | Package and Deployment Wizard dependency file
.dob | ActiveX document form file
.dox | ActiveX document binary form file
.dsr | Active Designer file
.dsx | Active Designer binary file
.dws | Deployment wizard script file
.frm | Form file
.frx | Binary form file
.log | Log file for load errors
.oca | Control TypeLib cache file
.pag | Property page file
.pgx | Binary property page file
.res | Resource file
.tlb | Remote Automation TypeLib file
.vbg | Visual Basic group project file
.vbl | Control licensing file
.vbp | Visual Basic project file
.vbr | Remote Automation registration file
.vbw | Visual Basic project workspace file
.vbz | Wizard launch file
.wct | WebClass HTML template

Run-Time Files

When you compile your application, all the necessary design-time files are included in the run-time executable files. Run-time files are listed in the following table:

Extension | Description
.dll | In-process ActiveX component
.exe | Executable file or ActiveX component
.ocx | ActiveX control
.vbd | ActiveX document state file
.wct | WebClass HTML template


Form Structures

While many of the files in a typical Visual Basic project are in a binary format and are readable only by specific processes and functions of Visual Basic or your application, the form (.frm) and project (.vbp) files are saved as ASCII text. These are readable in a text viewer (Notepad for instance).

The following sections describe the design- and run-time files in a typical Visual Basic project and the format of the form (.frm) and project (.vbp) files.

Visual Basic form (.frm) files are created and saved in ASCII format. The structure of a form consists of:

  • The version number of the file format.

  • A block of text containing the form description.

  • A set of form attributes.

  • The Basic code for the form.

The form description contains the property settings of the form. Blocks of text that define the properties of controls on the form are nested within the form. Controls contained within other controls have their properties nested within the text of the container. Figure A.1 illustrates the structure of the form description.

Figure A.1 Structure of the form description

Printing Form Descriptions

If you want to print a form description, you can do so without having to save the form.

To print a form description

  1. From the File menu, choose Print.

  2. Select the Form As Text check box, and choose OK to print the form description.


Version Number

The version number for forms created with Visual Basic is 6.00. If the version number is omitted from the form, an error is generated. When you load a Visual Basic application that has a version number less than 6.00, a warning dialog box appears informing you that the file will be saved in the new format.


Form Description

The form description starts with a Begin statement and ends with an End statement. The syntax of the Begin statement is:

Begin VB.{Form|MDIForm} formname

The End statement determines where the form description ends and the set of form attributes begins. Without the End statement, Visual Basic tries to read the attributes as if it were describing controls and properties of the form, thus producing errors.

Between the Begin Form and End statements are the properties of the form itself, followed by descriptions of each control on the form. Figure A.2 shows the nested structure of the form description in greater detail.

Figure A.2 Nested structure of the form description

Control Blocks

A control block consists of the text in the form description that defines the properties of an individual control. Like the form description itself, control blocks start with a Begin statement and end with an End statement. The syntax for a Begin statement of a control block is as follows:

Begin controlclass.controltype controlname

The properties for the control appear between the Begin statement and the End statement.

Control-Block Order

The order of the control blocks determines the z-order of the controls. Z-order is a relative ordering that determines how controls overlap each other on a form. The first control in the form description establishes the bottom of the z-order. Controls that appear later in the form description are higher in the z-order and therefore overlap controls that are lower in the z-order.

Embedded Control Blocks

Some controls can contain other controls. When a control is contained within another control, its control block is embedded in the control block of the container. You can embed control blocks inside:

  • Frames

  • Picture boxes

  • Menus

  • Custom controls, depending on their purpose

Embedded controls are commonly used to place option buttons inside a frame. Visual Basic must have all the information necessary for a container before adding any contained controls, so properties for a control must come before any embedded control blocks. Visual Basic ignores any properties within a control block that appear after embedded control blocks.

Menu Controls

Menu controls must appear together at the end of the form description, just before the Attributes section begins. When Visual Basic encounters a menu control during the loading of an ASCII form, it expects to find all the menu controls together. Once it detects a nonmenu control following one or more menu controls, Visual Basic assumes there are no more menu controls on the form and ignores any other menu controls it encounters during the loading of that form.

Shortcut Keys

Shortcut keys are keys you use to activate a menu control. The ASCII form uses the same syntax as the SendKeys statement to define key combinations: "+" = SHIFT, "^" = CTRL, and "{Fn}" = function key where n is the key number. Alphabetic characters represent themselves. Shortcut key syntax is:

Shortcut = ^{F4}' <CTRL><F4>

Note Top-level menus cannot have a shortcut key.

For More Information See "SendKeys Statement."

Comments in the Form Description

You can add comments to the form description. The single quotation mark (') is the comment delimiter.

Caution Comments and formatting in the form description are not retained when you save the form in Visual Basic. However, comments and indents in the code section of the form file are preserved.


Form Description Properties

When Visual Basic saves a form, it arranges the properties in a default ordering. However, you can list properties in any order when creating a form.

Any property you don’t list is set to its default value when loaded. When Visual Basic saves a form, it includes only those properties that do not use default values as their settings. Each control determines whether or not all of its properties are saved, or only those whose values are different from the default settings.

Syntax

Use this syntax to define properties in the form description:

property = value

Text property values must appear within double quotation marks. Boolean properties have a value of – 1 for True and 0 for False. Visual Basic interprets any value other than – 1 or 0 as True. Properties with listed values include their numeric value with the description of the value included as a comment. For example, the BorderStyle property appears like this:

BorderStyle = 0         ' None

Binary Property Values

Some controls have properties that have binary data as their values, such as the Picture property of picture box and image controls or certain properties of custom controls. Visual Basic saves all binary data for a form in a binary data file separate from the form.

Visual Basic saves the binary data file in the same directory as the form. The binary data file has the same file name as the form, but it has an .frx filename extension. Visual Basic reads the binary data file when loading the form. The binary data file (.frx) must be available to the form when Visual Basic loads it. If you share forms with others that use a binary data file, be sure to provide the binary data file (.frx) as well as the form (.frm).

Properties having binary data as their values appear in the form as a reference to a byte offset in the binary data file. For example, the value of a Picture property appears like this in a form description:

Begin VB.Image imgDemo
   Picture = "Myform.frx":02EB
End

The property listing means that the binary data that defines the Picture property of this control begins at byte 2EB (hex) in the file Myform.frx.

Icon Property

The value of the Icon property in a form depends on which icon is used for the form. The following table lists Icon property values and how those properties appear in a form.

Icon property setting | ASCII form contents
The default icon | No reference to the Icon property
(None)
Icon = 0
Any icon other than the default icon Byte offset reference to the binary data file. For example:
Icon = "Myform.frx":0000

TabIndex Property

If the TabIndex property is not specified, Visual Basic assigns the control the earliest possible location in the tab order once all other controls load.

Units of Measurement

Control sizes, x and y coordinates, and other property values using units of measurement are expressed in twips. When a control uses a scale mode other than twips, Visual Basic converts the twip values in the ASCII form to the units of measurement specified by the control’s ScaleMode property when loading the form.

Color Values

Color values appear as RGB values. For example, the ForeColor property appears like this:

ForeColor = &H00FF0000&

Visual Basic can also read QBColor values, converting them to RGB when loading the form. ASCII forms using QBColor values must use this syntax:

ForeColor = QBColor(qbcolor)

where qbcolor is a value from 0 to 15.

Note that the qbcolor argument corresponds to the color values used by graphics statements in other versions of Basic, such as Visual Basic for MS-DOS, Microsoft QuickBasic, and the Microsoft Basic Professional Development System.

Property Objects

Some property objects, such as the Font object, appear as a separate block, showing all of the settings for the various properties of the object. These blocks are enclosed in BeginProperty and EndProperty statements of the following form:

BeginProperty propertyname

property1 = value1

property2 = value2

EndProperty

Basic Code

The Basic code appears in the form immediately after the Attributes section following the last End statement in the form description. Statements in the Declarations section of a form appear first, followed by event procedures, general procedures, and functions.


Sample ASCII Form

Figure A.3 shows the Blanker.frm form from the Blanker sample application.

Figure A.3 Sample form from Blanker.vbp sample application

Here is part of the Blanker form saved in Visual Basic. Portions of the form removed to save space are indicated by a vertical ellipsis.

VERSION 6.00
Begin VB.Form DemoForm
   BackColor = &H00000000&
   Caption = "Screen Blanker Demo"
   ClientHeight = 960
   ClientLeft = 1965
   ClientTop = 1965
   ClientWidth = 7470
   ForeColor = &H00000000&
   Begin Property Font
      name = "MS Sans Serif"
      charset = 0
      .
      .
      .
   End Property
   Height = 5115
   Icon = "Blanker.frx":0018
   Left = 900
   LinkMode = 1                     ' Source
   LinkTopic = "Form1"
   ScaleHeight = 4425
   ScaleWidth = 7470
   Top = 1335
   Width = 7590
   Begin VB.Timer Timer1
      Interval = 1
      Left = 6960
      Top = 120
   End
   Begin VB.CommandButton cmdStartStop
      BackColor = &H00000000&
      Caption = "Start Demo"
      Default = -1
      Height = 390
      Left = 240
      TabIndex = 0
      Top = 120
      Width = 1830
   End
   Begin VB.PictureBox picBall
      AutoSize = -1               ' True
      BackColor = &H00000000&
      BorderStyle = 0               ' None
      ForeColor = &H00FFFFFF&
      Height = 465
      Left = 1800
      Picture = "Blanker.frx":0788
      ScaleHeight = 465
      ScaleWidth = 465
      TabIndex = 1
      Top = 720
      Visible = 0                  ' False
      Width = 465
   End
   .
   .
   .
   Begin VB.Menu mnuOption
      Caption = "&Options"
      Begin VB.Menu mnuLineCtlDemo
         Caption = "&Jumpy Line"
         Checked = -1               ' True
      End
      Begin VB.Menu mnuCtlMoveDemo
         Caption = "Re&bound"
      End
      .
      .
      .
      Begin VB.Menu mnuExit
         Caption = "E&xit"
      End
   End
End
.
.
.
Attribute VB_Name = "DemoForm"
Attribute VB_Creatable = False
Attribute VB_Exposed = False
Dim Shared FrameNum
Dim Shared XPos
Dim Shared YPos
Dim Shared DoFlag
Dim Shared Motion
.
.
.
Sub CircleDemo ()
   Dim Radius
   R = 255 * Rnd
   G = 255 * Rnd
   B = 255 * Rnd

   XPos = ScaleWidth / 2
   YPos = ScaleHeight / 2
   Radius = ((YPos * 0.9) + 1) * Rnd
   Circle (XPos, YPos), Radius, RGB (R, G, B)
End Sub
.
.
.
Private Sub Timer1_Timer ()
.
.
.
End Sub


Form File Loading Errors

When Visual Basic loads a form into memory, it first converts the form to binary format. When you make changes to the form and save the changes, Visual Basic rewrites the file in ASCII format.

When Visual Basic encounters an error while loading a form, it creates a log file and reports that error in the log file. Visual Basic adds error messages to the log file each time it encounters an error in the form. When the form load is finished, Visual Basic displays a message that tells you an error log file was created.

The log file has the same filename as the form file but with a .log filename extension. For example, if errors occurred when loading Myform.frm, Visual Basic would create a log file named Myform.log. If you reload Myform.frm later and errors continue to occur when loading the form, Visual Basic replaces the previous Myform.log file.


Form Load Error Log Messages

The following error messages can appear in an error log file. Note that these error messages deal only with problems that can occur when Visual Basic loads the form description. They do not indicate any problems that can exist in event procedures, general procedures, or any other part of the Basic code.

Cannot load Menu menuname.

This message appears if Visual Basic finds a menu control whose parent menu is defined as a menu separator. Menu controls that act as parents for menu controls in a submenu cannot be menu separators. Visual Basic does not load the menu control.

This message also appears if Visual Basic finds a menu control whose parent menu has its Checked property set to True. Menu controls that act as parents for menu controls in a submenu cannot be checked. Visual Basic does not load the menu control.

Cannot set Checked property in Menu menuname.

This message appears if Visual Basic finds a top-level menu control with its Checked property set to True. Top-level menus cannot have a checkmark. Visual Basic loads the menu control, but doesn’t set its Checked property.

Cannot set Shortcut property in menuname.

This message appears if Visual Basic finds a top-level menu control with a shortcut key defined. Top-level menus cannot have a shortcut key. Visual Basic loads the menu control, but doesn’t set the Shortcut property.

Class classname in control controlname is not a loaded control class.

This message appears if Visual Basic finds a class name it doesn’t recognize.

Control controlname could not be loaded.

This message appears if Visual Basic encounters an unknown type of control in the form description. Visual Basic creates a picture box to represent the unknown control, giving that picture box any valid properties from the unknown control description. When this message appears, a number of invalid property errors are likely to follow.

Control controlname has a quoted string where the property name should be.

This message appears if Visual Basic finds text inside quotation marks instead of a property name, which you do not place inside quotation marks. For example:

"Caption" = "Start Demo"

In this case, the property name Caption should not have been enclosed in quotation marks. Visual Basic ignores the line in the form description that produced this error.

The control name controlname is invalid.

This message appears if the name of a control is not a valid string in Visual Basic. Visual Basic will not load the control.

Control name too long; truncated to controlname.

This message appears if Visual Basic finds a control name longer than 40 characters. Visual Basic loads the control, truncating the name.

Did not find an index property and control controlname already exists. Cannot create this control.

This message appears if Visual Basic finds a control without an index that has the same name as a previously loaded control. Visual Basic doesn’t load the control.

Form formname could not be loaded.

This message appears if Visual Basic encounters the end of file unexpectedly or if the first Begin statement is missing.

The Form or MDIForm name formname is not valid; cannot load this form.

This message appears if the name of a form is not a valid string in Visual Basic. Visual Basic will not load the form.

Valid strings must start with a letter; can include only letters, numbers and underscores; and must have 40 or fewer characters.

The property name propertyname in control controlname is invalid.

This message appears if the name of a property is not a valid string in Visual Basic or is longer than 30 characters. Visual Basic will not set the property.

Property propertyname in control controlname could not be loaded.

This message appears if Visual Basic encounters an unknown property. Visual Basic skips this property when loading the form.

Property propertyname in control controlname could not be set.

This message appears if Visual Basic cannot set the property of the specified control as indicated by the form description.

Property propertyname in control controlname had an invalid value.

This message appears if Visual Basic encounters an invalid value for a property. Visual Basic changes the property value to the default value for that property.

Property propertyname in control controlname has an invalid file reference.

This message appears if Visual Basic couldn’t use a file name reference. This will happen if the referenced file (probably a binary data file for the form) is not found at the specified directory.

Property propertyname in control controlname has an invalid property index.

This message appears if Visual Basic finds a property name with a property index greater than 255. For example:

Prop300 = 5436

Visual Basic ignores the line in the form description that produced this error.

Property propertyname in control controlname has an invalid value.

This message appears if Visual Basic finds a property with a value that is not correct for that control. For example:

Top = Cahr(22)        ' Really wanted Char(22).

Visual Basic sets the property with its default value.

Property propertyname in control controlname must be a quoted string.

This message appears if Visual Basic finds a property value without quotation marks that should appear inside quotation marks. For example:

Caption = Start Demo

Visual Basic ignores the line in the form description that produced this error.

Syntax error: property propertyname in control controlname is missing an '='.

This message appears if Visual Basic finds a property name and value without an equal sign between them. For example:

Text        "Start Demo"

Visual Basic doesn’t load the property.

For More Information Additional information on errors encountered during form load is available by clicking the Help button in the error dialog box or by pressing F1.


Project File (.vbp) Format

Visual Basic always saves project files (.vbp) in ASCII format. The project file contains entries that reflect settings for your project. These include the forms and modules in your project, references, miscellaneous options that you have chosen to control compilation, etc.

Here is what a .vbp file might look like. This project includes modules saved with the class and file and names shown in the table below.

Module type | Class name | File name
MDI form | Aform | A_Form.frm
Form | Bform | B_Form.frm
Standard module | Cmodule | C_Module.bas
Class module | Dclass | D_Class.cls

Type=Exe
Form=B_Form.frm
Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#..\..\..\WINDOWS\SYSTEM\STDOLE2.TLB#OLE Automation
Form=A_Form.frm
Module=CModule; C_Module.bas
Class=DClass; D_Class.cls
Startup="BForm"
Command32=""
Name="Project1"
HelpContextID="0"
CompatibleMode="0"
MajorVer=1
MinorVer=0
RevisionVer=0
AutoIncrementVer=0
ServerSupportFiles=0
VersionCompanyName="Microsoft"
CompilationType=0
OptimizationType=0
FavorPentiumPro(tm)=0
CodeViewDebugInfo=0
NoAliasing=0
BoundsCheck=0
OverflowCheck=0
FlPointCheck=0
FDIVCheck=0
UnroundedFP=0
StartMode=0
Unattended=0
ThreadPerObject=0
MaxNumberOfThreads=1

Entries are added to the .vbp when you add forms, modules, components, etc. to your project. Entries are also added when you set options for your project. Many of these options are set using the Project Properties dialog box.

For More Information Press F1 to get explanations of the options that can be chosen in the Project Properties dialog box.


Visual Basic Coding Conventions

This appendix presents a set of suggested coding conventions for Visual Basic programs.

Coding conventions are programming guidelines that focus not on the logic of the program but on its physical structure and appearance. They make the code easier to read, understand, and maintain. Coding conventions can include:

  • Naming conventions for objects, variables, and procedures.

  • Standardized formats for labeling and commenting code.

  • Guidelines for spacing, formatting, and indenting.

In the sections that follow, each of these areas are discussed, along with examples of good usage.

Topics

Why Coding Conventions?

The main reason for using a consistent set of coding conventions is to standardize the structure and coding style of an application so that you and others can easily read and understand the code.
Object Naming Conventions

Objects should be named with a consistent prefix that makes it easy to identify the type of object. This section lists recommended conventions for controls, data access objects, and menus.
Constant and Variable Naming Conventions

This topic lists recommended conventions for constants and variables supported by Visual Basic. It also discusses the issues of identifying data type and scope.
Structured Coding Conventions

In addition to naming conventions, structured coding conventions, such as code commenting and consistent indenting, can greatly improve code readability. This topic discusses standards for these areas.


Why Coding Conventions?

The main reason for using a consistent set of coding conventions is to standardize the structure and coding style of an application so that you and others can easily read and understand the code.

Good coding conventions result in precise, readable, and unambiguous source code that is consistent with other language conventions and as intuitive as possible.

Minimal Coding Conventions

A general-purpose set of coding conventions should define the minimal requirements necessary to accomplish the purposes discussed above, leaving the programmer free to create the program’s logic and functional flow.

The object is to make the program easy to read and understand without cramping the programmer’s natural creativity with excessive constraints and arbitrary restrictions.

To this end, the conventions suggested in this appendix are brief and suggestive. They do not list every possible object or control, nor do they specify every type of informational comment that could be valuable. Depending on your project and your organization’s specific needs, you may wish to extend these guidelines to include additional elements, such as:

  • Conventions for specific objects and components developed in-house or purchased from third-party vendors.

  • Variables that describe your organization’s business activities or facilities.

  • Any other elements that your project or enterprise considers important for clarity and readability.

For more information For information about restrictions on naming procedures, variables, and constants, see "Code Basics" in "Programming Fundamentals."


Object Naming Conventions

Objects should be named with a consistent prefix that makes it easy to identify the type of object. Recommended conventions for some of the objects supported by Visual Basic are listed below.

Suggested Prefixes for Controls

Control type | prefix | Example
3D Panel | pnl | pnlGroup
ADO Data | ado | adoBiblio
Animated button | ani | aniMailBox
Check box | chk | chkReadOnly
Combo box, drop-down list box | cbo | cboEnglish
Command button | cmd | cmdExit
Common dialog | dlg | dlgFileOpen
Communications | com | comFax
Control (used within procedures when the specific type is unknown) | ctr | ctrCurrent
Data | dat | datBiblio
Data-bound combo box | dbcbo | dbcboLanguage
Data-bound grid | dbgrd | dbgrdQueryResult
Data-bound list box | dblst | dblstJobType
Data combo | dbc | dbcAuthor
Data grid | dgd | dgdTitles
Data list | dbl | dblPublisher
Data repeater | drp | drpLocation
Date picker | dtp | dtpPublished
Directory list box | dir | dirSource
Drive list box | drv | drvTarget
File list box | fil | filSource
Flat scroll bar | fsb | fsbMove
Form | frm | frmEntry
Frame | fra | fraLanguage
Gauge | gau | gauStatus
Graph | gra | graRevenue
Grid | grd | grdPrices
Hierarchical flexgrid | flex | flexOrders
Horizontal scroll bar | hsb | hsbVolume
Image | img | imgIcon
Image combo | imgcbo | imgcboProduct
ImageList | ils | ilsAllIcons
Label | lbl | lblHelpMessage
Lightweight check box | lwchk | lwchkArchive
Lightweight combo box | lwcbo | lwcboGerman
Lightweight command button | lwcmd | lwcmdRemove
Lightweight frame | lwfra | lwfraSaveOptions
Lightweight horizontal scroll bar | lwhsb | lwhsbVolume
Lightweight list box | lwlst | lwlstCostCenters
Lightweight option button | lwopt | lwoptIncomeLevel
Lightweight text box | lwtxt | lwoptStreet
Lightweight vertical scroll bar | lwvsb | lwvsbYear
Line | lin | linVertical
List box | lst | lstPolicyCodes
ListView | lvw | lvwHeadings
MAPI message | mpm | mpmSentMessage
MAPI session | mps | mpsSession
MCI | mci | mciVideo
Menu | mnu | mnuFileOpen
Month view | mvw | mvwPeriod
MS Chart | ch | chSalesbyRegion
MS Flex grid | msg | msgClients
MS Tab | mst | mstFirst
OLE container | ole | oleWorksheet
Option button | opt | optGender
Picture box | pic | picVGA
Picture clip | clp | clpToolbar
ProgressBar | prg | prgLoadFile
Remote Data | rd | rdTitles
RichTextBox | rtf | rtfReport
Shape | shp | shpCircle
Slider | sld | sldScale
Spin | spn | spnPages
StatusBar | sta | staDateTime
SysInfo | sys | sysMonitor
TabStrip | tab | tabOptions
Text box | txt | txtLastName
Timer | tmr | tmrAlarm
Toolbar | tlb | tlbActions
TreeView | tre | treOrganization
UpDown | upd | updDirection
Vertical scroll bar | vsb | vsbRate

Suggested Prefixes for Data Access Objects (DAO)

Use the following prefixes to indicate Data Access Objects.

Database object | Prefix | Example
Container | con | conReports
Database | db | dbAccounts
DBEngine | dbe | dbeJet
Document | doc | docSalesReport
Field | fld | fldAddress
Group | grp | grpFinance
Index | ix | idxAge
Parameter | prm | prmJobCode
QueryDef | qry | qrySalesByRegion
Recordset | rec | recForecast
Relation | rel | relEmployeeDept
TableDef | tbd | tbdCustomers
User | usr | usrNew
Workspace | wsp | wspMine

Some examples:

Dim dbBiblio As Database
Dim recPubsInNY As Recordset, strSQLStmt As String
Const DB_READONLY = 4            ' Set constant.
'Open database.
Set dbBiblio = OpenDatabase("BIBLIO.MDB")
' Set text for the SQL statement.
strSQLStmt = "SELECT * FROM Publishers WHERE _
   State = 'NY'"
' Create the new Recordset object.
Set recPubsInNY = db.OpenRecordset(strSQLStmt, _
   dbReadOnly)

Suggested Prefixes for Menus

Applications frequently use many menu controls, making it useful to have a unique set of naming conventions for these controls. Menu control prefixes should be extended beyond the initial "mnu" label by adding an additional prefix for each level of nesting, with the final menu caption at the end of the name string. The following table lists some examples.

Menu caption sequence | Menu handler name
File Open | mnuFileOpen
File Send Email | mnuFileSendEmail
File Send Fax | mnuFileSendFax
Format Character | mnuFormatCharacter
Help Contents | mnuHelpContents

When this naming convention is used, all members of a particular menu group are listed next to each other in Visual Basic’s Properties window. In addition, the menu control names clearly document the menu items to which they are attached.

Choosing Prefixes for Other Controls

For controls not listed above, you should try to standardize on a unique two or three character prefix for consistency. Use more than three characters only if needed for clarity.

For derived or modified controls, for example, extend the prefixes above so that there is no confusion over which control is really being used. For third-party controls, a lower-case abbreviation for the manufacturer could be added to the prefix. For example, a control instance created from the Visual Basic Professional 3D frame could uses a prefix of fra3d to avoid confusion over which control is really being used.


Constant and Variable Naming Conventions

In addition to objects, constants and variables also require well-formed naming conventions. This section lists recommended conventions for constants and variables supported by Visual Basic. It also discusses the issues of identifying data type and scope.

Variables should always be defined with the smallest scope possible. Global (Public) variables can create enormously complex state machines and make the logic of an application extremely difficult to understand. Global variables also make the reuse and maintenance of your code much more difficult.

Variables in Visual Basic can have the following scope:

Scope | Declaration | Visible in
Procedure-level | 'Private' in procedure, sub, or function | The procedure in which it is declared
Module-level | 'Private' in the declarations section of a form or code module (.frm, .bas) | Every procedure in the form or code module
Global | 'Public' in the declarations section of a code module (.bas) | Everywhere in the application

In a Visual Basic application, global variables should be used only when there is no other convenient way to share data between forms. When global variables must be used, it is good practice to declare them all in a single module, grouped by function. Give the module a meaningful name that indicates its purpose, such as Public.bas.

It is good coding practice to write modular code whenever possible. For example, if your application displays a dialog box, put all the controls and code required to perform the dialog's task in a single form. This helps to keep the application's code organized into useful components and minimizes its run-time overhead.

With the exception of global variables (which should not be passed), procedures and functions should operate only on objects passed to them. Global variables that are used in procedures should be identified in the declaration section at the beginning of the procedure. In addition, you should pass arguments to subs and functions using ByVal, unless you explicitly need to change the value of the passed argument.

Variable Scope Prefixes

As project size grows, so does the value of recognizing variable scope quickly. A one-letter scope prefix preceding the type prefix provides this, without greatly increasing the size of variable names.

Scope | Prefix | Example
Global | g | gstrUserName
Module-level | m | mblnCalcInProgress
Local to procedure | None | dblVelocity

A variable has global scope if it is declared Public in a standard module or a form module. A variable has module-level scope if declared Private in a standard module or form module, respectively.

Note Consistency is crucial to productive use of this technique; the syntax checker in Visual Basic will not catch module-level variables that begin with "p."

Constants

The body of constant names should be mixed case with capitals initiating each word. Although standard Visual Basic constants do not include data type and scope information, prefixes like i, s, g, and m can be very useful in understanding the value and scope of a constant. For constant names, follow the same rules as variables. For example:

mintUserListMax      'Max entry limit for User list
                  '(integer value,local to module)
gstrNewLine            'New Line character
                  '(string, global to application)

Variables

Declaring all variables saves programming time by reducing the number of bugs caused by typos (for example, aUserNameTmp vs. sUserNameTmp vs. sUserNameTemp). On the Editor tab of the Options dialog, check the Require Variable Declaration option. The Option Explicit statement requires that you declare all the variables in your Visual Basic program.

Variables should be prefixed to indicate their data type. Optionally, especially for large programs, the prefix can be extended to indicate the scope of the variable.

Variable Data Types

Use the following prefixes to indicate a variable's data type.

Data type | Prefix | Example
Boolean | bln | blnFound
Byte | byt | bytRasterData
Collection object | col | colWidgets
Currency | cur | curRevenue
Date (Time) | dtm | dtmStart
Double | dbl | dblTolerance
Error | err | errOrderNum
Integer | int | intQuantity
Long | lng | lngDistance
Object | obj | objCurrent
Single | sng | sngAverage
String | str | strFName
User-defined type | udt | udtEmployee
Variant | vnt | vntCheckSum

Descriptive Variable and Procedure Names

The body of a variable or procedure name should use mixed case and should be as long as necessary to describe its purpose. In addition, function names should begin with a verb, such as InitNameArray or CloseDialog.

For frequently used or long terms, standard abbreviations are recommended to help keep name lengths reasonable. In general, variable names greater than 32 characters can be difficult to read on VGA displays.

When using abbreviations, make sure they are consistent throughout the entire application. Randomly switching between Cnt and Count within a project will lead to unnecessary confusion.

User-Defined Types

In a large project with many user-defined types, it is often useful to give each such type a three-character prefix of its own. If these prefixes begin with "u," they will still be easy to recognize quickly when you are working with a user-defined type. For example, “ucli” could be used as the prefix for variables of a user-defined Client type.


Structured Coding Conventions

In addition to naming conventions, structured coding conventions, such as code commenting and consistent indenting, can greatly improve code readability.

Code Commenting Conventions

All procedures and functions should begin with a brief comment describing the functional characteristics of the procedure (what it does). This description should not describe the implementation details (how it does it) because these often change over time, resulting in unnecessary comment maintenance work, or worse yet, erroneous comments. The code itself and any necessary inline comments will describe the implementation.

Arguments passed to a procedure should be described when their functions are not obvious and when the procedure expects the arguments to be in a specific range. Function return values and global variables that are changed by the procedure, especially through reference arguments, must also be described at the beginning of each procedure.

Procedure header comment blocks should include the following section headings. For examples, see the next section, "Formatting Your Code."

Section heading | Comment description
Purpose | What the procedure does (not how).
Assumptions | List of each external variable, control, open file, or other element that is not obvious.
Effects | List of each affected external variable, control, or file and the effect it has (only if this is not obvious).
Inputs | Each argument that may not be obvious. Arguments are on a separate line with inline comments.
Returns | Explanation of the values returned by functions.

Remember the following points:

  • Every important variable declaration should include an inline comment describing the use of the variable being declared.

  • Variables, controls, and procedures should be named clearly enough that inline commenting is only needed for complex implementation details.

  • At the start of the .bas module that contains the project's Visual Basic generic constant declarations, you should include an overview that describes the application, enumerating primary data objects, procedures, algorithms, dialogs, databases, and system dependencies. Sometimes a piece of pseudocode describing the algorithm can be helpful.

Formatting Your Code

Because many programmers still use VGA displays, screen space should be conserved as much as possible while still allowing code formatting to reflect logic structure and nesting. Here are a few pointers:

  • Standard, tab-based, nested blocks should be indented four spaces (the default).

  • The functional overview comment of a procedure should be indented one space. The highest level statements that follow the overview comment should be indented one tab, with each nested block indented an additional tab. For example:
'*****************************************************
' Purpose:   Locates the first occurrence of a
'            specified user in the UserList array.
' Inputs:
'   strUserList():   the list of users to be searched.
'   strTargetUser:   the name of the user to search for.
' Returns:   The index of the first occurrence of the
'            rsTargetUser in the rasUserList array.
'            If target user is not found, return -1.
'*****************************************************

Function intFindUser (strUserList() As String, strTargetUser As _
   String)As Integer
   Dim i As Integer            ' Loop counter.
   Dim blnFound As Integer      ' Target found flag.
   intFindUser = -1
   i = 0
   While i <= Ubound(strUserList) and Not blnFound
      If strUserList(i) = strTargetUser Then
         blnFound = True
         intFindUser = i
      End If
   Wend
End Function

Grouping Constants

Variables and defined constants should be grouped by function rather than split into isolated areas or special files. Visual Basic generic constants should be grouped in a single module to separate them from application-specific declarations.

& and + Operators

Always use the & operator when linking strings and the + operator when working with numerical values. Using the + operator to concatenate may cause problems when operating on two variants. For example:

vntVar1 = "10.01"
vntVar2 = 11
vntResult = vntVar1 + vntVar2    'vntResult = 21.01
vntResult = vntVar1 & vntVar2   'vntResult = 10.0111

Creating Strings for MsgBox, InputBox, and SQL Queries

When creating a long string, use the underscore line-continuation character to create multiple lines of code so that you can read or debug the string easily. This technique is particularly useful when displaying a message box (MsgBox) or input box (InputBox) or when creating an SQL string. For example:

Dim Msg As String
Msg = "This is a paragraph that will be " _
& "in a message box. The text is" _
& " broken into several lines of code" _
& " in the source code, making it easier" _
& " for the programmer to read and debug."
MsgBox Msg

Dim QRY As String
QRY = "SELECT *" _
& " FROM Titles" _
& " WHERE [Year Published] > 1988"
TitlesQry.SQL = QRY


Native Code Compiler Switches

Microsoft Visual Basic allows you to compile your applications to fast, efficient native code, using the same optimizing back-end compiler technology as Microsoft Visual C++. Native code compilation provides several options for optimizing and debugging that aren't available with p-code. These options are traditionally called "switches," because each option can be turned on or off.

This appendix documents the native code compiler options, which appear on the Compile tab of the Project Properties dialog box, available on the Project menu. For more information about native code, see "Compiling Your Project to Native Code" in "More About Programming."

Topics

Optimize for Fast Code

Maximizes the speed of compiled executable files.
Optimize for Small Code

Minimizes the size of compiled executable files.
No Optimizations

Turns off all compiler optimizations.
Favor Pentium Pro

Optimizes code generation to favor the Pentium Pro (P6) processor.
Create Symbolic Debug Info

Generates symbolic debug information in the compiled executable file.
Assume No Aliasing

Tells the compiler that your program does not refer to the same memory location by more than one name.
Remove Array Bounds Checks

Turns off error checking for valid array indexes and dimensions.
Remove Integer Overflow Checks

Turns off error checking for numeric values assigned to integer variables.
Remove Floating-Point Error Checks

Turns off error checking for valid floating point operations and numeric values assigned to floating point variables.
Remove Safe Pentium FDIV Checks

Turns off the generation of special code for Pentium processors with the FDIV bug.
Allow Unrounded Floating-Point Operations

Allows the compiler to compare floating-point expressions without first rounding to the correct precision.


Optimize for Fast Code

Maximizes the speed of compiled executable files by instructing the compiler to favor speed over size.

When the compiler translates Visual Basic statements into machine code, there are often many different sequences of machine code that can correctly represent a given statement or construct. Sometimes these differences offer trade-offs of size versus speed. Selecting this option ensures that when the compiler recognizes such alternatives it will always generate the fastest code sequence possible, even when that may increase the size of the compiled program.


Optimize for Small Code

Minimizes the size of compiled executable files by instructing the compiler to favor size over speed.

When the compiler translates Visual Basic statements into machine code, there are often many different sequences of machine code that can correctly represent a given statement or construct. Sometimes these differences offer trade-offs of size versus speed. Selecting this option ensures that when the compiler recognizes such alternatives it will always generate the smallest code sequence possible, even when that may decrease the execution speed of the compiled program.


No Optimizations

Turns off all optimizations.

With this option selected, the compiler generates code that is significantly slower and larger than if optimization for fast or small code is selected.


Favor Pentium Pro

Optimizes code generation to favor the Pentium Pro (P6) processor. Code generated with this option will still run on earlier processors, but less efficiently.

The Pentium Pro microprocessor architecture allows certain code generation strategies that can substantially improve efficiency. However, code created using these strategies does not perform as well on 80386- and 80486-based or Pentium computers. Therefore, you should only use this option if all or most of the machines your program will run on use the Pentium Pro.


Create Symbolic Debug Info

Generates symbolic debug information in the compiled executable file.

Programs compiled to native code using this option can be debugged using Visual C++ (5.0 or later) or another compatible debugger. Setting this option will generate a .pdb file with the required symbol information for use with compatible symbolic debuggers.


Assume No Aliasing

Tells the compiler that your program does not use aliasing.

An alias is a name that refers to a memory location that is already referred to by a different name. This occurs when using ByRef arguments that refer to the same variable in two ways. For example:

Sub Foo(x as integer, y as integer)
   x = 5   ' Code is referring to the same variable
         ' (the local z in Main)
   y = 6   ' via two different names, x and y.
End Sub
Sub Main
   Dim z as integer
   Foo z,z
End Sub

Using this option allows the compiler to apply optimizations it couldn't otherwise use, such as storing variables in registers and performing loop optimizations. However, you should be careful not to check this option if your program passes arguments ByRef, since the optimizations could cause the program to execute incorrectly.


Remove Array Bounds Checks

Turns off error checking for valid array indexes and the correct number of dimensions of the array.

By default Visual Basic makes a check on every access to an array to determine if the index is within the range of the array. If the index is outside the bounds of the array, an error is returned. Selecting this option will turn off this error checking, which can speed up array manipulation significantly. However, if your program accesses an array with an index that is out of bounds, invalid memory locations may be accessed without warning. This can cause unexpected behavior or program crashes.


Remove Integer-Overflow Checks

Turns off error checking to insure that numeric values assigned to integer variables are within the correct range for the data types.

By default in Visual Basic, a check is made on every calculation to a variable with an integer-style data type (Byte, Integer, Long, and Currency) to be sure that the resulting value is within range of that data type. If the value is of the wrong magnitude, an error will occur. Selecting this option will turn off this error checking, which can speed up integer calculations. If data type capacities are overflowed, however, no error will be returned and incorrect results may occur.


Remove Floating-Point Error Checks

Turns off error checking to insure that numeric values assigned to floating-point variables are within the correct range for the data types, and that division by zero or other invalid operations do not occur.

By default in Visual Basic, a check is made on every calculation to a variable with a floating point data types (Single and Double) to be sure that the resulting value is within range of that data type. If the value is of the wrong magnitude an error will occur. Error checking is also performed to determine if division by zero or other invalid operations are attempted. Selecting this option turns off this error checking which can speed up floating point calculations. If data type capacities are overflowed, however, no error will be returned and incorrect results may occur.


Remove Safe Pentium FDIV Checks

Turns off the generation of special code to make floating point division safe on Pentium processors with the floating-point division (FDIV) bug.

The native code compiler automatically adds extra code for floating-point operations to make these operations safe when run on Pentium processors that have the FDIV bug. Selecting this option produces code that is smaller and faster, but which may in rare cases produce slightly incorrect results on Pentium processors with the FDIV bug.


Allow Unrounded Floating-Point Operations

Allows the compiler to compare the results of floating-point expressions without first rounding those results to the correct precision.

Floating-point calculations are normally rounded off to the correct degree of precision (Single or Double) before comparisons are made. Selecting this option allows the compiler do floating-point comparisons before rounding, when it can do so more efficiently. This improves the speed of some floating-point operations; however, this may result in calculations being maintained to a higher precision than expected, and two floating-point values that might be expected to compare equal might not.

In general this option should not be used if you perform equality comparisons directly on the results of floating-point computations. For example:

Dim Q As Single

Q = <floating-point computation>
  …
If Q = <floating-point computation> then
  ...
End If

If the option is set, the comparison of Q will be made with the result of the floating-point expression, which will likely have higher precision than that of a Single, so the comparison may fail. If the option is not set, the result of the floating-point expression will be rounded to the appropriate precision (Single) before the comparison, then the comparison will succeed.


Adding Help to Your Application

No matter how well crafted the application, at some point most users are going to have questions about how to use it. Unless you're going to be there to answer their questions in person, the best way to handle this is by providing a Help file for the application.

Visual Basic provides support for two different Help systems: the traditional Windows Help system (WinHelp), and the newer HTML Help. This appendix covers the steps necessary to add either WinHelp or HTML Help to your application, pointing out the few differences between the two where applicable. What it doesn't cover is how to author a Help file — there are numerous authoring tools available that can help you to do that.

Topics

Adding Support for Help

Describes the procedure for adding Help to your application using either WinHelp or HTML Help.
Adding Support for What's This Help

Describes the procedure for implementing What's This Help in your application using either WinHelp or HTML Help.
Distributing Help with Your Application

Explains the requirements for packaging and distributing Help.


Adding Support for Help

Adding support for Help to your Visual Basic application is really quite simple. All you need to do is set one property, HelpFile (and, of course, write and compile a Help file), to display Help when the user presses the F1 key or requests Help from a menu. An additional property, HelpContextID, can be set to provide a contextual Help topic for any user interface element in your application. The process of hooking up Help is essentially the same for both WinHelp and HTML Help.

The HelpFile Property

The HelpFile property of the App object is used to specify the file name of the Help file for your application. It requires a valid WinHelp (.hlp) or HTML Help (.chm) file. If the file doesn't exist, an error will occur.

To set the HelpFile property

  1. Select Project Properties from the Project menu to open the Project Properties dialog box.

  2. In the Help File Name field of the General tab, enter the path and file name for your application's Help file (.hlp or .chm).

You can also set the HelpFile programmatically. The following code would specify an HTML Help file that resides in the same directory as the application's executable file:

Private Sub Form_Load()
   App.HelpFile = App.Path & "\foo.chm"
End Sub

The ErrObject object also has a HelpFile property, allowing you to specify a different Help file for error messages. For example, if you have several applications that share the same error messages, you can put Help for the error messages in a single Help file that can be called by the Err.Helpfile property in each application.

The HelpContextID Property

The HelpContextID property is used to link a user interface element (such as a control, form, or menu) to a related topic in a Help file. The HelpContextID property must be a Long that matches the Context ID of a topic in a WinHelp (.hlp) or HTML Help (.chm) file.

For example, you might enter 10000 in the HelpContextID property of a TextBox. When the user selects the TextBox and presses F1, Visual Basic searches for a topic with a Context ID of 10000 in the Help file specified in the application's HelpFile property. If it's found, a Help window will open and display the topic; if not, an error will occur and the Help file's default topic will be displayed.

You should use a unique HelpContextID to match each Help topic in your Help file. In some cases, you may want to assign the same HelpContextID for several objects if the objects share a common Help topic.

You don't necessarily have to enter a HelpContextID for every control on a form. If the user presses F1 on a control with a HelpContextID of 0 (the default), Visual Basic will search for a valid HelpContextID for the control's container.

To assign a HelpContextID for a control or form

  1. Select the control or form for which you want to enter a HelpContextID.

  2. Double-click HelpContextID in the Properties window and enter a valid Long integer.

    Keep track of the value that you enter so that you can use the same value for the context ID of the associated Help topic.

    Note For the CommonDialog control and possibly for some other controls, the name of this property is HelpContext instead of HelpContextID.

To assign a HelpContextID for a menu

  1. Select Menu Editor from the Tools menu.

  2. Choose the menu item for which you want to enter a HelpContextID.

  3. Enter a valid Long in the Select the HelpContextID box.

    Keep track of the value that you enter so that you can use the same value for the context ID of the associated Help topic.

The HelpContextID can also be entered programmatically as follows:

Private Sub Form_Load()
   Command1.HelpContextID = 12345
   MenuHelp.HelpContextID = 23456
   Err.HelpContext = 34567
End Sub

Tip If you have more than a few Help topics, it may help to establish a numbering scheme before you start entering HelpContextID's. Assign a different range of numbers for each form or major element in your application, for example, 1000 – 1999 for the first form, 2000 – 2999 for the second, and so forth.


Adding Support for What's This Help

Visual Basic allows you to easily add What’s This Help to your applications. What's This Help provides quick access to Help text in a popup window without the need to open the Help viewer. It is typically used to provide simple assistance for user interface elements such as data entry fields. Visual Basic supports What's This Help topics in both WinHelp (.hlp) and HTML Help (.chm) files.

Setting the WhatsThisHelp property of a form to True enables What's This Help. When What's This Help is enabled, context-sensitive Help for the form is disabled.

To enable What's This Help for a form

  1. With the form selected, double-click the WhatsThisHelp property in the Properties window to set it to True.

  2. Set the following properties to add a What's This button to the title bar of the form:
    Property | Setting
    BorderStyle | 1 – Fixed Single or 2 – Sizable
    MaxButton | False
    MinButton | False
    WhatsThisButton | True

    Note A form can't have a What's This Help button if it has Minimize and Maximize buttons. As an alternative to the settings shown above, you can also set the BorderStyle property to 3 – Fixed Dialog, since a fixed dialog doesn't have Minimize and Maximize buttons.

  3. Select each control for which you want to provide What's This Help and assign a unique value to the WhatsThisHelpID property of the control.

    Keep track of the value that you enter so that you can use the same value for the context ID of the associated Help topic.

    Important To implement What's This Help in HTML Help, all What's This Help topics must be contained in a Cshelp.txt file that is compiled into the .chm file. For more information, see the documentation for your HTML Help authoring tool.

    You can also enable What's This Help without using a What's This button by setting the WhatsThisHelp property of the form to True and invoking the WhatsThisMode method of a form or the ShowWhatsThis method of a control.


Distributing Help with Your Application

The final step in adding Help to your application is making sure that it gets into the hands of the end user. The requirements for distributing Help with your application differ slightly for WinHelp versus HTML Help.

Distributing WinHelp

Since every Windows system already has the Windows Help Viewer installed, the only thing you need to distribute is the Help file (.hlp) itself. The Package and Deployment Wizard will automatically add a dependency for the Help file referenced by your application. If you're creating setup by another means, you'll need to make sure that the .hlp file is included and installed to the right location (usually in the same directory as the application or the \Windows\Help directory.)

Distributing HTML Help

HTML Help is a relatively new technology, hence you can't assume that every user will have the files needed to view HTML Help. The Package and Deployment Wizard will add a dependency for the HTML Help file (.chm) referenced by your application; but it may not add all dependencies for the HTML Help viewer files. You will need to modify your setup to include these files. Consult the documentation for your HTML Help authoring tool for more information on which files are required in a given situation.


Component Tools Guide

The Component Tools Guide is a suite of guides designed to help you create and use components that interoperate through the open, extensible standard on which ActiveX™ is based: the Component Object Model (COM).

By virtue of this standard, you can design applications from components that communicate through a common set of interfaces — and do it with the speed and ease of use you've become accustomed to with Microsoft® Visual Basic®.

Each of the sections included in the Component Tools Guide covers a different aspect of designing with components — from using existing software to creating your own, from ActiveX controls to DLLs, from the Visual Basic development environment to the Internet.

Parts

Using ActiveX Controls

An introduction to the ActiveX controls included with the Visual Basic Professional Edition. Start here for general information, scenarios of real-life uses, and code.
Creating ActiveX Components

A guide to creating ActiveX controls, ActiveX documents, and ActiveX code components. Includes introductory step by step chapters as well as in-depth design guides.
Extending the Visual Basic Environment with Add-Ins

A guide to using the Visual Basic extensibility object model to tailor the development environment to your unique needs. Use objects in the model to build add-ins that automate repetitive tasks, monitor the development environment for certain events, or perform complex tasks.
Accessing DLLs and the Windows API

A guide to enhancing functionality provided Visual Basic by calling functions in dynamic-link libraries (DLLs). Through DLLs, you can access the thousands of procedures that form the backbone of the Microsoft Windows operating system, as well as routines written in other languages.
Building Internet Applications

An introduction to creating applications that can run on the Internet or an intranet, presenting HTML pages inside forms-based applications, and deploying these applications and other ActiveX components on the Web.


Using ActiveX Controls

This section provides an introduction and scenarios to help you get started with the ActiveX controls — formerly called OLE controls — included with the Microsoft Visual Basic Professional Edition. Start here for general information and for suggestions and code to use when developing your applications. More detailed reference information, including links to properties, methods, and events associated with each control, is included in the Language Reference.

Chapters

ActiveX Controls Overview

An introduction to ActiveX controls, including installation, upgrade, control class names, and distribution.
Using the ActiveX Controls

Introduction to most of the ActiveX controls included with Visual Basic, along with many scenarios (including code) illustrating ways to use them.


ActiveX Controls Overviews

ActiveX Controls Overview

This section, "Using ActiveX Controls," provides a starting point for each of the ActiveX controls, formerly called OLE controls, in the Microsoft Visual Basic programming system. Each ActiveX control is documented in its own section here with task-oriented overviews. These overviews are not meant to replace the language reference information, but to give the new user contexts for using the controls.

For language-specific information, search for the control's name.

Topics

What Is an ActiveX Control?

Short description of ActiveX controls.
Installing and Registering ActiveX Controls

Details about installing and registering the ActiveX controls.
Upgrading VBX Controls to ActiveX Controls

Details about upgrading applications created with .vbx controls.
Loading ActiveX Controls

Load ActiveX controls using the Components dialog box.
ActiveX Controls File Names

Lists the ActiveX controls associated with each .ocx file.
Data Bound ActiveX Controls

List of data-bound ActiveX controls.
The About Property

Description of the About property.
ActiveX Control Class

List of all ActiveX control class names.
Creating, Running, and Distributing Executable (.exe) Files

Details on distributing applications created with ActiveX controls.


What Is an ActiveX Control?

An ActiveX control is an extension to the Visual Basic Toolbox. You use ActiveX controls just as you would any of the standard built-in controls, such as the CheckBox control. When you add an ActiveX control to a program, it becomes part of the development and run-time environment and provides new functionality for your application.

ActiveX controls leverage your capabilities as a Visual Basic programmer by retaining some familiar properties, events, and methods, such as the Name property, which behave as you would expect. Then, however, the ActiveX controls feature methods and properties that greatly increase your flexibility and capability as a Visual Basic programmer.

For example, the Visual Basic Professional and Enterprise editions include the Windows Common controls that allow you to create applications with the look and feel of Windows 95 toolbars, status bars, and tree views of directory structures. Other controls allow you to create applications that take full advantage of the Internet.


Installing and Registering ActiveX Controls

At setup, the Professional and Enterprise Editions automatically install and register ActiveX controls in the \Windows\System or System32 directory. You are then able to use the ActiveX controls at design time to build your applications.

If you plan to create a setup program for your application, you'll need to include information on any ActiveX controls in the Setup.lst file. For more information, see "Distributing Your Applications," in the Visual Basic Programmer's Guide.

Note It is a violation of your license agreement to copy and distribute any information from the "Licenses" section of the system registry.


Upgrading VBX Controls to ActiveX Controls

If you have a project with VBX custom controls that you'd like to replace with ActiveX controls, Visual Basic can do this automatically. Conversion is only possible for VBX custom controls for which replacement ActiveX controls exist on your system. For more information, see "Managing Projects," in the Visual Basic Programmer's Guide.

The following VBX custom controls are no longer supported in Visual Basic However, 32-bit ActiveX versions can be found as .ocx files in the \Tools\Controls directory of your Visual Basic CD-ROM:

  • AniButton

  • Gauge

  • Graph

  • KeyState

  • MSGrid

  • Outline

  • Spin

  • ThreeD

To install these controls, you will have to use the Regsvr32 and RegEdit applications, which are also available in the Tools directory. For instructions on how to use the Regsvr32 and RegEdit applications, consult the Readme.txt file in the \Tools\Controls directory.


Loading ActiveX Controls

ActiveX controls have the file name extension .ocx. You can use the ActiveX controls provided with Visual Basic or obtain additional controls from third-party developers.

You can use ActiveX controls and other insertable objects to your project by adding them to the Toolbox.

Note Visual Basic ActiveX controls are 32-bit controls. Some third-party developers offer ActiveX controls which are 16-bit controls, and these can no longer be used in Visual Basic.

To add a control to a project's Toolbox

  1. On the Project menu, click Components to display the Components dialog box, as seen in figure 1.1.

    Tip You can also display the dialog box by right-clicking on the Toolbox.

  2. Items listed in this dialog box include all registered insertable objects, designers, and ActiveX controls.

  3. To add an ActiveX control to the Toolbox, select the check box to the left of the control name.

  4. Click OK to close the Components dialog box. All of the ActiveX controls that you selected will now appear in the Toolbox.

Figure 1.1 The Components dialog box

To add ActiveX controls to the Components dialog box, click the Browse button, and locate files with the .ocx file name extension. These files are commonly installed in your \Windows\System or System32 directory. When you add an ActiveX control to the list of available controls, Visual Basic automatically selects its check box in the Components dialog box.


ActiveX Control File Names

ActiveX Controls File Names

To load an ActiveX control into the Toolbox, use the Components dialog box from the Project menu. The table below lists the names of components you will find in the dialog box along with the file name (.ocx) and the ActiveX controls contained by the component.

Note The presence or absence of components on your system depends upon which version of Visual Basic you are using.

Component Name | File Name | Control
Microsoft ADO Data Control 6.0 | MSADODC.OCX | ADO Data Control
Microsoft Chart Control 5.5 | MSCHART.OCX | Microsoft Chart
Microsoft Comm Control 6.0 | MSCOMM32.OCX | MSComm
Microsoft Common Dialog Control 6.0 | COMDLG32. OCX | CommonDialog
Microsoft Data Bound Grid Control 5.0 | DBGRID32.OCX | DBGrid
Microsoft Data Bound List Controls 6.0 | DBLIST32.OCX | DBList, DBCombo
Microsoft Data Repeater Control 6.0 | MSDATREP.OCX | DataRepeater
Microsoft Data Grid Control 6.0 | MSDATGRD.OCX | DataGrid
Microsoft Data List Controls 6.0 | MSDATLST.OCX | DataList, DataCombo
Microsoft FlexGrid Control 6.0 | MSFLXGRD.OCX | MSFlexGrid
Microsoft Grid Control | GRID32.OCX | Grid
Microsoft Hierarchical Flex Grid Control 6.0 | MSHFLXGD.OCX | MSHFlexGrid
Microsoft Internet Transfer Control 6.0 | MSINET.OCX | Internet Transfer control
Microsoft MAPI Controls6.0 | MSMAPI32.OCX | MAPIMessages, MAPISession
Microsoft MaskedEdit Control 6.0 | MSMASK32.OCX | MaskedEdit
Microsoft Multimedia Control 6.0 | MCI32.OCX | Multimedia MCI
Microsoft PictureClip Control 6.0 | PICCLP32.OCX | PictureClip
Microsoft RemoteData Control 6.0 | MSRDC20.OCX | RemoteData
Microsoft RichTextBox Control 6.0 | RICHTX32.OCX | RichTextBox
Microsoft SysInfo Control 6.0 | SYSINFO.OCX | SysInfo
Microsoft TabbedDialog Control 6.0 | TABCTL32.OCX | Microsoft Tab Control
Microsoft Windows Common Controls 6.0 | MSCOMCTL.OCX | TabStrip, Toolbar, StatusBar, ProgressBar, TreeView, ListView, ImageList, Slider, ImageCombo
Microsoft Windows Common Controls-2 6.0 | MSCOMCT2.OCX | Animation, UpDown, MonthView, DTPicker, FlatScrollbar
Microsoft Windows Common Controls-3 6.0 | COMCT332.OCX | CoolBar
Microsoft Winsock Control 6.0 | MSWINSCK.OCX | WinSock


Data Bound ActiveX Controls

The following controls are bound controls. This means that they can be linked to a data control and can display field values for the current record in the data set. These controls can also write out values to the data set.

  • DataCombo

  • DataGrid

  • DataList

  • DataRepeater

  • DateTimePicker

  • Hierarchical FlexGrid

  • ImageCombo

  • Masked Edit

  • Microsoft Chart

  • Microsoft FlexGrid

  • MonthView

  • RichTextBox

For More Information For more on using bound controls, see "Using Visual Basic's Standard Controls" in the Programmer's Guide.


The About Property

Some ActiveX controls were developed by independent software vendors. If you would like more information about a control's vendor, click the control and press F4 to display the Properties window. Double-click the About property in the Properties window to open a dialog box that displays information about the vendor.


ActiveX Control Class

The class name for each ActiveX control is listed in the following table. (The class name for a control also appears in the Properties window.)

Control | Class name
Animation | Animation
Communications | MSComm
CoolBar | CoolBar
DataCombo | DataCombo
DataList | DataList
Data Repeater | DataRepeater
DateTimePicker | DTPicker
FlatScrollBar | FlatScrollBar
Hierarchical FlexGrid | MSHFlexGrid
ImageCombo | ImageCombo
ImageList | ImageList
Internet Transfer | Inet
ListView | ListView
MAPI | MapiSession, MapiMessages
Masked edit | MaskEdBox
MonthView | MonthView
MS FlexGrid | MSFlexGrid
Multimedia MCI | MMControl
Picture clip | PictureClip
ProgressBar | ProgressBar
RichTextBox | RichTextBox
Slider | Slider
Microsoft Tabbed Dialog Control | SSTab
StatusBar | StatusBar
SysInfo | SysInfo
TabStrip | TabStrip
Toolbar | Toolbar
TreeView | TreeView
UpDown | UpDown
Microsoft Chart | MSChart
Winsock | Winsock


Creating, Running, and Distributing Executable (.EXE) Files

To run your application under Microsoft Windows outside of Visual Basic, you need to create an executable (.exe) file. You create executable files for applications that use ActiveX controls the same as you would for any other application. There are a few issues to consider, however, when running such an application.

Visual Basic Executable (.exe) Files

An ActiveX control file is accessed both by Visual Basic and by applications created with Visual Basic. When you run an executable file that contains an ActiveX control, the .ocx file associated with it must be registered in the system registry. Otherwise, the application will not be able to find the code needed to create the control.

If a control cannot be found, the Visual Basic run-time DLL generates the error message "File Not Found." If you want to distribute an application that uses ActiveX controls, it is recommended that your installation procedure copy all required .ocx files into the user's \Windows\System directory. Your installation procedure should also register the required controls in the system registry.

You can freely distribute any application you create with the Visual Basic to any Microsoft Windows user. The Package and Deployment Wizard included with Visual Basic provides tools to help you write setup programs that install your application. Users will need copies of the following:

  • The Visual Basic run-time file.

  • Any .ocx files.

  • Additional DLLs, as required by your application or by ActiveX controls.


Using the ActiveX Controls

The topics in this chapter provide introductions to most of the ActiveX controls provided with Microsoft Visual Basic. For many of these, you will also find scenarios — along with code — featuring the controls in sample applications.

Topics

Using the Animation Control

Basic operation covers opening, playing, stopping, and closing silent .avi files.
Using the Communications Control

Describes how to use the Communications control with a modem to dial a phone number, interact with another modem, or add advanced communications functionality to your applications.
Using the CoolBar Control

The CoolBar control allows you to create user-configurable toolbars similar to those found in Microsoft Internet Explorer
Using the DateTimePicker Control

The DateTimePicker control provides a drop-down calendar to users, useful for date or time input.
Using the DataRepeater Control

Create a data-bound user control that allows the user to view one record in a database. The DataRepeater control "repeats" the user control by creating multiple instances of it in a scrolling view, allowing the user to view several records at once.
Using the FlatScrollBar Control

The FlatScrollBar control is an alternative to the original VScrollBar HScrollBar controls and features hot tracking — the scrollbar changes appearance as the cursor hovers over it.
Using the ImageCombo Control

Similar to the standard ComboBox control, the ImageCombo control additionally provides an image to rapidly distinguish items in a list.
Using the ImageList Control

Manage images by adding them to the ImageList control. Insert pictures at design time or run time, and access them at run time. Also covers using the ImageList with other controls.
Using the Internet Transfer Control

Transfer files to and from a remote computer using the HTTP or FTP protocol. Basic operation includes specifying a proxy server, invoking the OpenURL or Execute operation, and using the GetChunk method to retrieve data.
Using the ListView Control

Organize and view your data in any of four different views using the View property. Add items to be ListImages collection using the Add method, and customize the view by specifying images to be associated with the viewed items.
Using the MAPI Controls

Use the MAPI controls to sign-on to an electronic messaging system, access an Inbox, send or receive messages, or send file attachments.
Using the MaskedEdit Control

Describes how to use the MaskedEdit control to create mask patterns that prompt users for date, time, currency, or customized data input.
Using the MonthView Control

The MonthView control presents a graphical representation of a calendar for easy input of dates into any application.
Using the MSChart Control

Learn how to use the rich features of the control, including how to use combination charts, assign and edit backdrops, and load data from an array.
Using the Multimedia Control

Describes how to use the Multimedia control to manage the recording and playback of a variety of multimedia using a simple push-button interface.
Using the PictureClip Control

Describes how to use the PictureClip control to create an image resource for other Visual Basic controls by storing multiple images in a single image resource and then retrieving individual images by "clipping" specific regions.
Using the ProgressBar Control

Give the user feedback about a lengthy operation by associating the Value property with a metric of the operation. Set the Min and Max properties to actual values used in the operation.
Using the RichTextBox Control

Learn how to open and save RTF files in the control, and use the various text attribute properties to specify how text is displayed.
Using the Slider Control

Allow users to set values using the Slider control. Basic operation includes setting Min and Max values, and allowing the user to select a range of data.
Using the StatusBar Control

Add Panel objects to the Panels collection, and program text and images to be displayed in the StatusBar control. Set the Style property to automatically view keyboard status, time, and date.
Using the SysInfo Control

Describes how to use properties and events of the SysInfo control to determine the current operating system and version, and how to monitor and respond to system and Plug and Play events or changes in AC and battery power status.
Using the Tabbed Dialog Control

Introduces the fundamentals of using this control to present several dialogs or screens of information on a single form using the same interface seen in many Microsoft Windows applications.
Using the TabStrip Control

Create tabs by adding Tab objects to the Tabs collection. Use an ImageList control to supply bitmaps for the tabs. Manage tabs by associating each tab with a container control such as the Frame control.
Using the ToolBar Control

Use at Toolbar to give users quick access to frequently used operations. Add Button objects to the control, and associate images, text, and ToolTips with each button. Allow the user to reconfigure the Toolbar by setting the AllowCustomize property.
Using the TreeView Control

Explanation of the Node objects and how they relate to each other. Add Node objects to the Nodes collection using a database table to supply the text.
Using the UpDown Control

Crate your own Spinner control by associating the UpDown control with a TextBox. Set the AutoBuddy property to automatically associate the control with another control.
Using the WinSock Control

Send data to remote computers using either the TCP or UDP protocol. Simple scenarios include building a "chat" application.


Using the Animation Control

The Animation control displays silent Audio Video Interleaved (AVI) clips. An AVI clip is a series of bitmap frames like a movie.

One example is the piece of paper that "flies" between folders when copying files in the Windows 95 system:

Although AVI clips can have sound, such clips cannot be used with the Animation control, and an error will occur if you try to load such a file. Only silent AVI clips can be used. To play .avi files with sound, use the Multimedia (MCI) control. See "Using the Multimedia Control" later in this chapter for more information about the MCI control.

Note A variety of soundless .avi files can be found in the \Graphics\AVI directory of the Visual Basic CD-ROM.

At run time, the Animation control does not have a visible frame.

The Animation control maintains a separate thread of execution while it is playing. Therefore, your application will not be blocked, and can continue to execute within its process.

Possible Uses

  • To create dialog boxes that inform the user of the length and nature of an operation.

  • To play silent instructional video clips about your application.

  • To allow users to play files dropped onto the control.

Basic Operation: Open, Play, Stop, and Close Methods

When using the control, you open an .avi file using the Open method, play it using the Play method, and stop it with the Stop method. After a video has played, use the Close method to close the file. You do not need to close the file before opening a new one.

The following code uses two CommandButton controls, cmdPlay and cmdStop, and a CommonDialog control named dlgOpen. Set the caption of cmdPlay to "Open and Play." The caption of the CommandButton control cmdStop is set to "Stop."

Private Sub cmdPlay_Click()
   ' Configure a CommonDialog control to allow the
   ' user to find .avi files to play. The CommonDialog
   ' control is named "dlgOpen." The Animation control
   ' is named "anmAVI."
   dlgOpen.Filter = "avi files (*.avi)|*.avi"
   dlgOpen.ShowOpen
   anmAvi.Open dlgOpen.FileName
   anmAVI.Play
End Sub

This code stops the video playing:

Private Sub cmdStop_Click()
   anmAVI.Stop
End Sub

Play Method Arguments: Repeat, Start, and Stop

The Play method has three arguments — repeat, start, and stop — which determine how many times a file is played, at which frame to begin playing, and where to stop the file.

If the repeat argument is not supplied, the file will play continuously. For example, the following code will play a file continuously until the user clicks the cmdStop button:

Private Sub cmdPlay_Click()
   dlgOpen.Filter = "avi files (*.avi)|*.avi"
   dlgOpen.ShowOpen
   anmAVI.Open dlgOpen.FileName
   ' Play the file indefinitely.
   anmAVI.Play
End Sub

Private Sub cmdStop_Click()
   anmAVI.Stop
End Sub

The following code plays the file ten times, from the sixth to sixteenth frames (the first frame is frame 0):

anmAVI.Play 10, 5, 15

Play Files Automatically Using the AutoPlay Property

If the AutoPlay property is set to True, the control will begin playing a file as soon as it is loaded. Conversely, to stop a file from playing, set the AutoPlay property to False, as shown in the following code:

Private Sub cmdPlay_Click()
   ' Setting the AutoPlay property to True plays
   ' the file as soon as it is loaded. Thus there's
   ' no need for a Play method.
   dlgOpen.Filter = "avi files (*.avi)|*.avi"
   dlgOpen.ShowOpen
   anmAvi.AutoPlay = True
   anmAVI.File = dlgOpen.FileName
End Sub

Private Sub cmdStop_Click()
   ' Set AutoPlay to False to stop playing.
   anmAVI.AutoPlay = False
End Sub

Center the Play Area Using the Center Property

You can specify whether or not to center the video in the control by using the Center property. When the Center property is set to False, the control automatically sizes itself at run time to the size of the video. At design time, the left and top edges of the control define the area where the video will be displayed as seen:

When the Center property is set to True, the control does not resize itself. Instead, the video will be displayed in the center of the area defined by the control, as seen:

Note If the area defined by the control at design time is smaller than the video, the edges of the video will be clipped.

Distribution Note The Animation control is part of a group of ActiveX controls that are found in the MSCOMCT2.OCX file. To use the Animation control in your application, you must add the MSCOMCT2.OCX file to the project. When distributing your application, install the MSCOMCT2.OCX file in the user’s Microsoft Windows System or System 32 directory. For more information on how to add an ActiveX control to a project, see the Programmer’s Guide.


Using the Communications Control

The Communications control allows you to add both simple serial port communication functionality to your application and advanced functionality to create a full-featured, event-driven communications tool.

The Communications Control

The Communications control provides an interface to a standard set of communications commands. It allows you to establish a connection to a serial port, connect to another communication device (a modem, for instance), issue commands, exchange data, and monitor and respond to various events and errors that may be encountered during a serial connection.

Possible Uses

  • To dial a phone number.

  • To monitor a serial port for incoming data.

  • To create a full-featured terminal program.

Sample Applications: Dialer.vbp and VBTerm.vbp

The Dialer.vbp and VBTerm.vbp sample applications which are listed in the samples directory, demonstrate simple and complex (respectively) programming techniques of the Communications control.

Basics of Serial Communications

Every computer comes with one or more serial ports. They are named successively: COM1, COM2, and so on. On a standard PC, the mouse is usually connected to the COM1 port. A modem may be connected to COM2, a scanner to COM3, etc. Serial ports provide a channel for the transmission of data from these external serial devices.

The essential function of the serial port is to act as an interpreter between the CPU and the serial device. As data is sent through the serial port from the CPU, Byte values are converted to serial bits. When data is received, serial bits are converted to Byte values.

A further layer of interpretation is needed to complete the transmission of data. On the operating system side, Windows uses a communications driver, Comm.drv, to send and receive data using standard Windows API functions. The serial device manufacturer provides a driver that connects its hardware to Windows. When you use the Communications control, you are issuing API functions, which are then interpreted by Comm.drv and passed to the device driver.

As a programmer, you need only concern yourself with the Windows side of this interaction. As a Visual Basic programmer, you need only concern yourself with the interface that the Communications control provides to API functions of the Windows communications driver. In other words, you set and monitor properties and events of the Communications control.

Establishing the Serial Connection

The first step in using the Communications control is establishing the connection to the serial port. The following table lists the properties that are used to establish the serial connection:

Properties | Description
CommPort | Sets and returns the communications port number.
Settings | Sets and returns the baud rate, parity, data bits, and stop bits as a string.
PortOpen | Sets and returns the state of a communications port. Also opens and closes a port.

Opening the Serial Port

To open a serial port, use the CommPort, PortOpen, and Settings properties. For example:

' Open the serial port
MSComm1.CommPort = 2
MSComm1.Settings = "9600,N,8,1"
MSComm1.PortOpen = True

The CommPort property sets which serial port to open. Assuming that a modem is connected to COM2, the above example sets the value to 2 (COM2) and connects to the modem. You can set the CommPort property value to any number between 1 and 16 (the default is 1). If, however, you set this value to a COM port that does not exist for the system on which your application is run, an error will be generated.

The Settings property allows you to specify the baud rate, parity, and the number of data bits and stop bits. By default, the baud rate is set at 9600. The parity setting is for data validation. It is commonly not used, and set to "N". The data bits setting specifies the number of bits that represent a chunk of data. The stop bit indicates when a chunk of data has been received.

Once you’ve specified which port to open and how data communication is to be handled, you use the PortOpen property to establish the connection. It is a Boolean value, True or False. If, however, the port is not functional, if the CommPort property is set incorrectly, or if the device does not support the settings you’ve specified, an error will be generated or the external device may not work correctly. Setting the value of the PortOpen property to False closes the port.

Working with a Modem

In most cases, you will use the Communications control to program your application to work with a modem. With the Communications control, you can use the standard Hayes-compatible command set to dial a phone number, or connect to and interact with another modem.

Once the serial port connection has been established using the CommPort, Settings, and PortOpen properties, you use the Output property to activate and interact with the modem. The Output property is used to issue commands which control the interaction between one modem and another. For example:

' Activate the modem and dial a phone number.
MSComm1.Output = "ATDT 555-5555" & vbCr

In the example above, the command "AT" initiates the connection, "D" dials the number, and "T" specifies touch tone (rather than pulse). A carriage return character (vbCr) must be specified when outputting to a terminal. You do not need to add the return character when outputting byte arrays.

When a command is successfully processed, an "OK" result code will be returned. You can test for this result code to determine if a command was processed successfully.

For More Information For a complete list of Hayes-compatible commands, check your modem documentation.

Setting Receive and Transmit Buffer Properties at Design Time

When a port is opened, receive and transmit buffers are created. To manage these buffers, the Communications control provides you with a number of properties that can be set at design time using the control’s Property Pages.

Setting buffer properties at design time

Buffer Memory Allocation

The InBufferSize and OutBufferSize properties specify how much memory is allocated to the receive and transmit buffers. Each are set by default to the values shown above. The larger you make the number, the less memory you have available to your application. If, however, your buffer is too small, you run the risk of overflowing the buffer unless you use handshaking.

Note Given the amount of memory available to most PCs at this time, buffer memory allocation is less crucial because you have more resources available. In other words, you can set the buffer values higher without affecting the performance of your application.

The RThreshold and SThreshold Properties

The RThreshold and SThreshold properties set or return the number of characters that are received into the receive and transmit buffers before the OnComm event is fired. The OnComm event is used to monitor and respond to changes in communications states. Setting the value for each property to zero (0) prevents the OnComm event from firing. Setting the value to something other than 0 (1, for instance) causes the OnComm event to be fired every time a single character is received into either buffer.

For More Information See "The OnComm Event and the CommEvent Property" in this topic for more information on these properties.

The InputLen and EOFEnable Properties

Setting the InputLen property to 0 causes the Communications control to read the entire contents of the receive buffer when the Input property is used. When reading data from a machine whose output is formatted in fixed-length blocks of data, the value of this property can be set appropriately.

The EOFEnable property is used to indicate when an End of File (EOF) character is found during data input. Setting this to True causes data input to stop and the OnComm event to fire to inform you that this condition has occurred.

For More Information See "Managing the Receive and Transmit Buffers" and "The OnComm Event and the CommEvent Property" in this topic for more information.

Managing the Receive and Transmit Buffers

As mentioned above, receive and transmit buffers are created whenever a port is opened. The receive and transmit buffers are used to store incoming data and to transmit outgoing data. The Communications control allows you to manage these buffers by providing you with a number of properties that are used to place and retrieve data, return the size of each buffer, and handle both text and binary data. Properly managing these buffers is an important part of using the Communications control.

The Receive Buffer

The Input property is used to store and retrieve data from the receive buffer. For example, if you wanted to retrieve data from the receive buffer and display it in a text box, you might use the following code:

TxtDisplay.Text = MSComm1.Input

To retrieve the entire contents of the receive buffer, however, you must first set the InputLen property to 0. This can be done at design or run time.

You can also receive incoming data as either text or binary data by setting the InputMode property to one of the following Visual Basic constants: comInputModeText or comInputModeBinary. The data will either be retrieved as string or as binary data in a Byte array. Use comInputModeText for data that uses the ANSI character set and comInputModeBinary for all other data, such as data that has embedded control characters, Nulls, etc.

As each byte of data is received, it is moved into the receive buffer and the InBufferCount property is incremented by one. The InBufferCount property, then, can be used to retrieve the number of bytes in the receive buffer. You can also clear the receive buffer by setting the value of this property to 0.

The Transmit Buffer

The Output property is used to send commands and data to the transmit buffer.

Like the Input property, data can be transmitted as either text or binary data. The Output property, however, must transmit either text or binary data by specifying either a string or Byte array variant.

You can send commands, text strings, or Byte array data with the Output property. For example:

' Send an AT command
MSComm1.Output = "ATDT 555-5555"

' Send a text string
MsComm1.Output = "This is a text string"

' Send Byte array data
MSComm1.Output = Out

As previously mentioned, transmit lines must end with a carriage return character (vbCr). In the last example, Out is a variable defined as a Byte array: Dim Out() As Byte. If it were a string variant, it would be defined as: Dim Out() As String.

You can monitor the number of bytes in the transmit buffer by using the OutBufferCount property. You can clear the transmit buffer by setting this value to 0.

Handshaking

An integral part of managing the receive and transmit buffers is ensuring that the back-and-forth transmission of data is successful — that the speed at which the data is being received does not overflow the buffer limits, for example.

Handshaking refers to the internal communications protocol by which data is transferred from the hardware port to the receive buffer. When a character of data arrives at the serial port, the communications device has to move it into the receive buffer so that your program can read it. A handshaking protocol ensures that data is not lost due to a buffer overrun, where data arrives at the port too quickly for the communications device to move the data into the receive buffer.

You set the Handshaking property to specify the handshaking protocol to be used by your application. By default, this value is set to none (comNone). You can, however, specify one of the other following protocols:

Setting | Value | Description
comNone | 0 | No handshaking (Default).
comXOnXOff | 1 | XOn/XOff handshaking.
comRTS | 2 | RTS/CTS (Request To Send/Clear To Send) handshaking.
comRTSXOnXOff | 3 | Both Request To Send and XOn/XOff handshaking.

The protocol that you choose depends upon the device to which you’re connecting. Setting this value to comRTSXOnXOff supports both of the protocols.

In many cases, the communications protocol itself handles handshaking. Therefore, setting this property to something other than comNone may result in conflicts.

Note If you do set this value to either comRTS or comRTSXOnXOff, you need to set the RTSEnabled property to True. Otherwise, you will be able to connect and send, but not receive, data.

The OnComm Event and the CommEvent Property

Depending upon the scope and functionality of your application, you may need to monitor and respond to any number of events or errors which may occur during the connection to another device or in the receipt or transmission of data.

The OnComm event and the CommEvent property allow you to trap and check the value of communication events and errors.

When a communication event or error occurs, the OnComm event is fired and the value of the CommEvent property is changed. Therefore, if necessary, you can check the value of the CommEvent property each time the OnComm event is fired. Because communications (especially over telephone lines) can be unpredictable, trapping these events and errors allows you to respond to them appropriately.

The following table lists the communication events that will trigger the OnComm event. The values will then be written to the CommEvent property.

Constant | Value | Description
comEvSend | 1 | There are fewer than SThreshold number of characters in the transmit buffer.
comEvReceive | 2 | Received RThreshold number of characters. This event is generated continuously until you use the Input property to remove the data from the receive buffer.
comEvCTS | 3 | Change in Clear To Send line.
comEvDSR | 4 | Change in Data Set Ready line. This event is only fired when DSR changes from 1 to 0.
comEvCD | 5 | Change in Carrier Detect line.
comEvRing | 6 | Ring detected. Some UARTs (universal asynchronous receiver-transmitters) may not support this event.
comEvEOF | 7 | End Of File (ASCII character 26) character received.

The OnComm event is also triggered, and a value is written to the CommEvent property, when the following errors are encountered.

Setting | Value | Description
comEventBreak | 1001 | A Break signal was received.
comEventFrame | 1004 | Framing Error. The hardware detected a framing error.
comEventOverrun | 1006 | Port Overrun. A character was not read from the hardware before the next character arrived and was lost.
comEventRxOver | 1008 | Receive Buffer Overflow. There is no room in the receive buffer.
comEventRxParity | 1009 | Parity Error. The hardware detected a parity error.
comEventTxFull | 1010 | Transmit Buffer Full. The transmit buffer was full while trying to queue a character.
comEventDCB | 1011 | Unexpected error retrieving Device Control Block (DCB) for the port.


Using the CoolBar Control

The CoolBar control (Comctl332.ocx) allows you to create user-configurable toolbars similar to those found in Microsoft Internet Explorer. The CoolBar control is a container control, able to host child controls. It consists of a collection of one or more resizable regions known as bands. Each band can host a single child control.

Using the CoolBar control is the same as using most other ActiveX controls, with the following exceptions.

  • Adding Controls to a CoolBar

  • Displaying Pictures on a CoolBar

  • Using Other Controls with the CoolBar

Note This version of the CoolBar control requires that Internet Explorer 3.0 or later be installed on both the development computer as well as any computer where the CoolBar control will be distributed.

At this time CoolBar is known to work in Visual Basic 5.0 Service Pack 2 or later, Internet Explorer 3.0 or later, Visual C++ 4.0 or later, and Visual FoxPro 5.0 or later. It should also work in any host container that supports the ISimpleFrame interface.


Adding Controls to a CoolBar

The method for adding child controls to a CoolBar control is slightly different from that of adding controls to other containers. The control must first be associated with a Band object; there is a limit of one child control per band.

To add a control to a CoolBar

  1. With the CoolBar control selected, select a control from the ToolBox and draw it on the CoolBar.

  2. Open the Property Page for the CoolBar and select the Bands tab.

  3. Use the Index buttons to select the index of the Band object on which you want the control to appear.

  4. Select the control from the Child list.

The child control will then move and resize along with the Band object at run time.

Note If you simply add a control without associating it to a band, it will appear as a floating control over the CoolBar at run time. To avoid this situation, set the Visible property of the control to False.

You can have additional controls contained on a CoolBar and swap them in and out at run time using the Child property of the Band object.


Displaying Pictures on a CoolBar

The CoolBar control provides several properties that allow a great deal of flexibility in displaying pictures.

When you assign an image to the Picture property of the CoolBar control, that image will be tiled seamlessly across all bands, behind any child controls. By setting the FixedBackground property of a Band object to False, the Image will be tiled within that band.

The Band object has a Picture property that allows you to display a different background picture when the UseCoolbarPicture property is set to False. The Band object also has an Image property which can be used to display an icon to the right of the move handle.

The EmbossPicture property can be used to force an image to be displayed in two colors, similar to the Internet Explorer toolbar. The two colors to be used are determined by the EmbossHighlight and EmbossShadow properties.

When the EmbossPicture property is set to True, the image assigned to the Picture property will be dithered to the EmbossHighlight and EmbossShadow colors. The dithering process uses a fixed threshold to determine which colors in the image will be mapped to the highlight and shadow colors. Choose images with a good definition between dark and light colors for best results.


Using Other Controls with the CoolBar

There are certain limitations to the controls that can be used as child controls on a CoolBar control.

The CoolBar control can only host controls which expose a Window handle. Lightweight controls such as Label, Image, and Shape controls don't expose a Window handle and can't be used. Although you can place these controls on the CoolBar, they won't appear at run time and won't be listed in the Child list box on the Property Page.

Although each band in the CoolBar control can only host a single child control, you can get around this limitation by placing a container control (such as a PictureBox) on the band and hosting additional controls within that container. Keep in mind that if you do this, you will need to create the code to resize the nested controls in response to changes in the CoolBar.

Some controls may not behave as expected when contained within a CoolBar control. For example, a contained Toolbar control won’t paint properly when its Wrappable property is set to True and the CoolBar is resized. To avoid this problem, set the Wrappable property to False.


Using the DataRepeater Control

The DataRepeater control functions as a data-bound container of any user control you create. For example, imagine creating a user control that contains three TextBox controls, and a CheckBox control. This user control is designed to show one record of an employee database — displaying name, birth date, employee number, and marital status of the employee.

After compiling the control into an .ocx, the DataRepeater control's RepeatedControlName property is set to the user control. The DataRepeater is then bound to a data source, such as the ADO Data Control, which sets up a connection between the user control and the employee database. At run time, the DataRepeater displays several instances of the user control—each in its own row, and each bound to a different record in the database. The result can resemble the figure below.

Employee Records Repeated in DataRepeater Control

At run time, the user can scroll through a recordset using the HOME, END, PAGEUP, PAGEDOWN, and arrow keys.

Possible Uses

  • To create a catalog that includes images of each product.

  • To create a bankbook application to track personal finances.

  • To create a custom data-bound grid that includes ComboBox controls.

Creating a Data-bound User Control for use in the DataRepeater Control

The first step when using the DataRepeater control is to create a data-bound user control. The procedure below creates a simple control that can be repeated in the DataRepeater.

For More Information Details about creating a data-bound user control can be found in "Binding a Control to a Data Source."

Creating a data-bound User control for use in the DataRepeater Control

  1. Create a new ActiveX Control project.

  2. In the Properties window, rename Project1 to ProductsCtl.

  3. In the Properties window, rename UserControl1 to ctlProducts.

  4. Add two TextBox controls and two Label controls to the form, and set their properties as shown in the table below.
    Object | Property | Setting
    Text1 | Name | txtProductName
    Text2 | Name | txtUnitPrice
    Label1 | Caption | Product Name
    Label2 | Caption | Unit Price

    Because the user control will be repeated, you may want to minimize its height; the simple user control described above looks like this:

  5. Add the code below to the control to create Let and Get properties of the user control.
    Public Property Get ProductName() As String
ProductName = txtProductName.Text
End Property

Public Property Let ProductName(ByVal newProductName As String)
txtProductName.Text = newProductName
End Property

Public Property Get UnitPrice() As String
UnitPrice = txtUnitPrice.Text ' Return a String!
End Property

Public Property Let UnitPrice(ByVal newUnitPrice As String)
txtUnitPrice.Text = newUnitPrice ' NewUnitPrice is a String!
End Property

Private Sub txtProductName_Change()
PropertyChanged "ProductName"
End Sub

Private Sub txtUnitPrice_Change()
PropertyChanged "UnitPrice"
End Sub

    Important Notice in the above code that the UnitPrice property is declared as a string. This is to allow the user of the DataRepeater control to use the DataFormat object to format the string as Currency. If you type the new value as Currency (what you might expect), the formatting supplied by the DataFormat object will be stripped.

  6. Use the Procedure Attributes dialog box to make the properties data-bound.

    On the Tools menu click Procedure Attributes. On the Procedures Attributes dialog box, click Advanced. The Name box contains the property you want to make data-bound, and should contain ProductName. Click Property is data bound, then click Show in DataBindings collection at design time. Click the Name box and click UnitPrice. Once again, click Property is data bound, then click Show in DataBindings collection at design time. Click OK to close the dialog box.

  7. Using the Windows Explorer, create a new folder on your hard disk named ProductsCtl.

  8. On the File menu click Save Project. Save the project in the new folder using the names provided in the dialog boxes.

  9. On the File menu click Make ProductsCtl.ocx. Save the .ocx to the same folder.

When you compile the user control into an .ocx, Visual Basic registers the control for you, allowing you to use it in the DataRepeater control.

Using the Data-bound User Control in the DataRepeater Control

Once you have built and compiled a data-bound user control, you can have it repeated in the DataRepeater control.

Using a data-bound user control in the DataRepeater control

  1. Create a new Standard Exe project.

  2. Set the following properties for the project and form:
    Object | Property | Setting
    Project1 | Name | prjRepeater
    Form1 | Name | frmRepeater

  3. Add the DataRepeater control and ADO Data Control to the Toolbox.

    On the Project menu, click Components. In the Components dialog box, click the Controls tab, and check Microsoft Data Repeater Control and Microsoft ADO Data Control. Click OK to close the dialog box.

  4. Draw a DataRepeater control on the form. Make the control large enough to accommodate several "rows" of the control you want to repeat. One "row" is the height of the repeated control, as determined by the size of the UserControl object's designer.

  5. Draw an ADO Data Control on the form, beneath the DataRepeater control.

  6. Click the ADO Data Control to select it. Then, on the Properties window, click the ConnectionString property. Using the ConnectionString dialog box, create a connection string that accesses the Northwind database.

  7. On the Properties window, click Source, and type the following SQL statement:
    SELECT * FROM Products
  8. Click the DataRepeater control to select it. On the Properties window, click DataSource, and click ADODC1 to set the data source.

  9. In the Properties window, click RepeatedControlName to display a drop-down list of all controls available on the computer. On the list, click ProductsCtl.ctlProducts. The selected control will be repeated in the DataRepeater control.

Binding the User Control Properties to the ADO Data Control

Once the user control is contained by the DataRepeater control, you must bind the user control's properties to the record source.

  1. Right-click the DataRepeater control, and then click DataRepeater Properties. On the Property Pages dialog box, click the RepeaterBindings tab.

  2. Click the PropertyName box to display a drop-down list of the data-bound properties of the repeated control. Click ProductName.

  3. Click the DataField box to display a drop-down list of available data fields from the data source. Click ProductName.

  4. Click the Add button to add the pair of property and data field to the RepeaterBindings collection.

  5. Repeat steps 2 through 4 for the remaining property (UnitPrice).

  6. Click the Format tab.

  7. In the Format Item box, click UnitPrice.

  8. In the Format Type box, click Currency. In the Symbol box, select a currency character appropriate to your country.

  9. Click OK to close the dialog box.

  10. Press F5 to run the project. You can then use the scroll bar to scroll through the recordset, or click the ADO Data Control's navigation buttons.


Exposing User Control Events of the RepeatedControl

When you create a user control, it's common to create public events. When the user control is placed on a standard Visual Basic form, the custom events automatically appear in the form's code module.

With the DataRepeater control, however, exposing the user control's events is not an automatic occurrence. Because the user control is not put directly on a form, its events cannot be exposed in the code module. There is, however, a way of achieving the same end.

In brief, the major steps are:

  1. Add a class module to the user control project, and name the module CtlEvents.

  2. Add a public event to the module named ProductChange.

  3. Add a public procedure to the module named FireControlChange that raises the ProductChange event (using the RaiseEvent statement).

  4. Add a public property to the user control named Events that returns a reference to the CtlEvents object.

  5. In the Change event of each constituent control of the user control, invoke the FireControlChange procedure to raise events.

Note The following step-by-step procedure builds on the user control and DataRepeater control project built in the topic "Using the DataRepeater Control."

Creating events to be exposed by the user control

  1. Add a Class Module to the ActiveX Control project.

  2. Change the name of the class module from Class1 to CtlEvents.

  3. Set the Instancing property to 6 – GlobalMultiuse.

  4. Add a public event declaration to the Declarations section of the class module. For demonstration purposes, the code below adds only one event declaration. In a real application, you would add one event declaration for every control on the user control.
    Option Explicit
Public Event ProductChange()
  5. Add a public procedure to the class module. In the procedure, add the RaiseEvent statement to raise the ProductChange event. This procedure is invoked in events of the UserControl, and its sole purpose is to raise the event.
    Public Sub FireControlChange()
RaiseEvent ProductChange
End Sub

    The ProductChange event is the event that will be exposed by CtlEvents when using the ProductsCtl user control in a DataRepeater control.

  6. In the user control object's code module, declare a variable typed as the new class, as shown below:
    Option Explicit
Dim EventsObj As New CtlEvents
  7. Add a public Get procedure to the user control. In the procedure, use a Set statement to return the object reference.
    Public Property Get Events() As CtlEvents
Set Events = EventsObj ' returns the CtlEvents object reference
End Property
  8. In the Change event of the control you want to monitor, invoke the class object's procedure:
    Private Sub txtProductName_Change()
PropertyChanged ("ProductName")
EventsObj.FireControlChange
End Sub
  9. Save and compile the project (make the .ocx).

To expose the events of the User Control in the container form

The following steps take place in the project that contains the DataRepeater control. In that project, a DataRepeater control contains the user control with the events created in steps 1 to 9 above.

  1. In the project that contains the DataRepeater control, use the Components dialog box to add the ProductsCtl (ProductsCtl.ocx) to the Toolbox. This step is needed to ensure that the Visual Basic Package and Deployment Wizard will correctly include the necessary .ocx file with the project. It also allows access at design time to the ProductsCtl events in code.

  2. In the Declarations section of the Form object's code module, paste the following code that declares an object variable with the WithEvents keyword:
    Option Explicit
Dim WithEvents objRepCtl As CtlEvents ' Be sure to use WithEvents
  3. In the DataRepeater control's RepeatedControlLoaded event set the variable to the Events property. You must set the variable using the DataRepeater control's RepeatedControl property, as shown below:
    Private Sub DataRepeater1_RepeatedControlLoaded()
Set objRepCtl = DataRepeater1.RepeatedControl.Events
End Sub

    Because you can set the RepeatedControlName property at run time, the RepeatedControlLoaded was designed to allow you to set control properties that can only be initialized after the control has been loaded.

  4. The new object objRepCtl (with events) should now appear in the drop-down list of the Object box, in the upper left corner of the code module. Select the object from the list, and add code to the event of the object, as shown below.
    Private Sub objRepCtl_ProductChange()
 Debug.Print "objRepCtl_ProductChange"
End Sub
  5. Run the project. When you alter the Products field, the event will occur.

Adding Events to the Control

With a few simple additions of code, you can easily add more events to the user control. First add public events — one for each event you want to raise — to the class module:

Option Explicit
Public Event ProductChange()
Public Event UnitPriceChange() ' <- This is a new Event declaration.

Then add an argument to the class module's FireControlChange procedure. A Select Case statement is added to the procedure to distinguish which control is calling the procedure. Using that argument, the correct event is raised:

Public Sub FireControlChange(ctlName As String)
   Select Case ctlName
   Case "ProductName"
      RaiseEvent ProductChange
   Case "UnitPrice"
      RaiseEvent UnitPriceChange
   Case Else
      ' Handle other cases here.
   End Select
End Sub

Finally, switch to the UserControl object's code module. In the Change event of the controls you wish to monitor, invoke the procedure with the correct argument, as shown below.

Private Sub txtProductName_Change()
   PropertyChanged "ProductName"
   EventsObj.FireControlChange "ProductName" ' Invoke procedure.
End Sub

Private Sub txtUnitPrice_Change()
   PropertyChanged "UnitPrice"
   EventsObj.FireControlChange "UnitPrice" ' Invoke procedure.
End Sub


Maintaining a History of Clicked Records in the DataRepeater Control

A common task while using the DataRepeater control is to maintain a history of viewed records. The history can be displayed in a ListBox control. Any time the user clicks on an item in the history, the DataRepeater control displays that record in its client area. As with other data-aware controls (such as the DataGrid control), you (the developer) must store and retrieve bookmarks for each record you wish to track.

Important Bookmarks are valid only for the life of a recordset, and are not persistable or interchangeable across recordsets.

The following step-by-step creates a history by first storing the bookmarks of viewed records in an array. A value associated with the record is also added to a ListBox control. When the user clicks an item in the ListBox control, the ListIndex of the clicked item is used to retrieve the bookmark of the record. Setting the VisibleRecords property to the bookmark causes the record to appear in the DataRepeater control.

The step-by-step builds upon the project created in "Using the DataRepeater Control."

To create a history of viewed records

  1. Add a Reference to the Microsoft ActiveX Data Objects 2.0 Library.

    On the Project menu, click References to open the References dialog box. Check Microsoft ActiveX Data Objects 2.0 Library, and click OK to close the dialog box.

  2. In the Declarations section of the form's code module, declare an array of variants, and an object variable for the ADODB Recordset object. Use the ADODB recordset to navigate.
    Option Explicit
Private varBookmarks() As Variant
Private rsProducts As ADODB.Recordset
  3. In the form's Load event, use the ReDim statement to initialize the array. Set the recordset object variable to the recordset of the ADO Data Control.
    Private Sub Form_Load()
ReDim varBookmarks(0)
Set rsProducts = adodc1.Recordset
End Sub
  4. Draw a ListBox control on the form, and change its name from Listbox1 to lstHistory.

  5. Add the following code to the DataRepeater control's CurrentRecordChanged event:
    Private Sub DataRepeater1_CurrentRecordChanged()
' The CurrentRecordChanged event occurs immediately after the form
' is opened. At that point, the UBound(varBookmarks) = 0, and no
' records have been added. Thus you must add the first record to
' the history list, and its bookmark to the array. Then increment
' the array index to 1 and exit the sub. The next time a record is
' clicked, this code will be ignored.
Dim iUpper As Integer ' Upper bound of bookmark array.
Dim i As Integer      ' Counter for ListBox
If UBound(varBookmarks) = 0 Then
lstHistory.AddItem rsProducts!ProductName
varBookmarks(0) = DataRepeater1.CurrentRecord
ReDim Preserve varBookmarks(1)
Exit Sub
End If

' The code below now executes when the user clicks a record.
' First check the listbox to ensure the clicked record hasn't
' already been added to the history list. If so, exit.
For i = 0 To lstHistory.ListCount - 1
If lstHistory.List(i) = rsProducts!ProductName _
Then Exit Sub
Next I

' If no duplicates, then add the record to the history, and
' cache the bookmark in the array.
lstHistory.AddItem rsProducts!ProductName
varBookmarks(UBound(varBookmarks)) = DataRepeater1.CurrentRecord
' Increment the array.
ReDim Preserve varBookmarks(UBound(varBookmarks) + 1)
End Sub
  6. The code below sets the VisibleRecords property to a cached bookmark. Paste the code into the form's code module.
    Private Sub lstHistory_Click()
Debug.Print lstHistory.Text, lstHistory.ListIndex
DataRepeater1.VisibleRecords(1) = _
varBookmarks(lstHistory.ListIndex)
End Sub
  7. Run the project. Scroll through the recordset using the DataRepeater control's scrollbars. Click on any record to add it to the history. Click any item in the listbox to return to that record.


Using the DateTimePicker Control

The DateTimePicker control displays date and/or time information and acts as the interface through which users can modify date and time information. The control's display consists of fields that are defined by the control's format string. When the DateTimePicker is dropped down, a MonthView calendar is displayed.

The control has two different modes:

  • Dropdown Calendar mode (default) – enables the user to display a dropdown calendar that can be used to select a date.

  • Time Format mode – enables the user to select a field in the date display (i.e. the month, day, year, etc.) and press the up/down arrow to the right of the control to set its value.

You can use the control to display the date in various preset formats including Short Date (11/14/97), LongDate (Friday, November 14, 1997) and Time (7:00:00 PM). You can also specify custom formats using formatting strings, or create your own formats with callback fields.

DateTimePicker control in Dropdown Calendar Mode

DateTimePicker control in Time Format Mode

Possible Uses

  • To present date information where a restricted or specially formatted field is required, such as in a payroll or scheduling application.

  • To give users the ability to select a date with the click of a mouse instead of typing a date value.

Using the Two Modes of the Control

The DateTimePicker operates as a masked edit control for entering date and time values. Each part of the date or time is treated as a separate field within the edit portion of the control. As the user clicks on each field, it is highlighted and they can use the up and down arrow keys to increment or decrement the value of the field. The user can also type values directly into the control, where applicable.

The UpDown property determines which mode the control is in. When UpDown is set to False, the control is in Dropdown Calendar mode. (The default value) When the UpDown property is set to True, the DateTimePicker is in Time Format mode.

In Time Format mode, two scroll arrows appear on the right side of the control. The user can click these arrows with the mouse to increment or decrement the value of the currently selected field. In Dropdown Calendar mode, the control drops down a calendar that the user can use to select dates.

The dropdown calendar has most of the features of the MonthView control. See "Using The MonthView Control" for more information on how to set up and format the dropdown calendar portion of the DateTimePicker control.

Setting and Returning Dates

The currently selected date in the control is determined by the Value property. You can set the Value of the control before it is displayed (for example, at design time or in the Form_Load event) to determine which date will be initially selected in the control:

DTPicker1.Value = "10/31/97"

By default, the control's Value is set to the current date. If you change the DateTimePicker's Value in code, the control is automatically updated to reflect the new setting.

The Value property returns a raw date value, or a null value. The DateTimePicker control has several properties that return specific information about the displayed date:

  • The Month property returns the integer value (1-12) of the month containing the currently selected date.

  • The Day property returns the day number (1-31) currently selected.

  • The DayOfWeek property returns a value indicating the day of the week the selected date falls on (values correspond to the values of vbDayOfWeek constants.)

  • The Year property returns the year containing the selected date as an integer value.

  • The Week property returns the number of the week containing the selected date.

Use the Change event to determine when the user has changed the date value in the control.

Using The Checkbox to Select No Date

The CheckBox property makes it possible to specify whether the control returns a date. By default, CheckBox is set to False and the control always returns a date.

To enable the user to specify no date, set the CheckBox property to True (for example, if you are using the DateTimePicker to enter the completion date of a project but the project has not yet been completed).

When CheckBox is set to True, a small check box appears in the edit portion of the control to the left of the date and time. If the box is not checked, the Value property returns a null value. If the user checks this checkbox, the control returns the displayed date through its Value property.

Working With Date & Time Formats

The DateTimePicker gives you a tremendous amount of flexibility in formatting the display of dates and times in the control. You can use all the standard Visual Basic formatting strings, or you can create custom formats by using callback fields.

The Format property determines how the control formats the raw date value. You can choose from one of the predefined formatting options, or use the custom formatting feature of the control.

The CustomFormat property defines the format expression used to display the contents of the control. You specify a format string that tells the control how to format the date output. The DateTimePicker controls supports the following format strings:

String | Description
d | The one- or two-digit day.
dd | The two-digit day. Single digit day values are preceded by a zero.
ddd | The three-character day-of-week abbreviation.
dddd | The full day-of-week name.
h | The one- or two-digit hour in 12-hour format.
hh | The two-digit hour in 12-hour format. Single digit values are preceded by a zero.
H | The one- or two-digit hour in 24-hour format.
HH | The two-digit hour in 24-hour format. Single digit values are preceded by a zero.
m | The one- or two-digit minute.
mm | The two-digit minute. Single digit values are preceded by a zero.
M | The one- or two-digit month number.
MM | The two-digit month number. Single digit values are preceded by a zero.
MMM | The three-character month abbreviation.
MMMM | The full month name.
s | The one- or two- digit seconds.
ss | The two-digit seconds. Single digit values are proceeded by a zero.
t | The one-letter AM/PM abbreviation (that is, "AM" is displayed as "A").
tt | The two-letter AM/PM abbreviation (that is, "AM" is displayed as "AM").
X | A callback field that gives programmer control over the displayed field (see below.) Multiple 'X' characters can be used in series to signify unique callback fields.
y | The one-digit year (that is, 1996 would be displayed as "6").
yy | The last two digits of the year (that is, 1996 would be displayed as "96").
yyy | The full year (that is, 1996 would be displayed as "1996").

You can add body text to the format string. For example, if you want the control to display the current date with the format "Today is: 05:30:31 Friday Nov 14, 1997". The format string you would use is "'Today is: 'hh':'m':'s ddddMMMdd', 'yyy". Body text must be enclosed in single quotes.

Creating Custom Formats with Callback Fields

One of the custom format fields described above is a callback field. A callback field allows you to customize the output of certain parts of a format string. To declare a callback field, you must include one or more 'X' characters (ASCII Code 88) anywhere in the body of the format string. Callback fields are displayed in left-to-right order.

When a new date is displayed in a format that includes one or more callback fields, The Format and FormatSize events are raised for each callback field. You can use the Format event to specify a custom response string, and the FormatSize event to determine the space needed to display the string. This behavior gives you complete control of how a callback field is displayed.

Each sequence of X’s has a unique meaning. For example, X might mean "st/nd/rd/th" (for "1st" "2nd" "3rd" "4th" etc) and "XX" may mean "first" "second" "third" "fourth" etc. These fields do not format the users’ text, they format the date into a displayable format.

For example, let's say you want to display the month in Spanish as well as English, using a format like this:

July (Julio) 29

You would create a format string that looked like this:

MMMM XXXX d

When processing the Format and FormatSize events, you can check which callback field is being called by comparing the input format sting with "XXXX". If the field string matches, an output string "(Julio)" can be built and the length of the output string can be supplied. The number of Xs is only used by an application to determine what text to supply for a callback field. When processing the FormatSize event, the size of the text can be programmatically calculated.

The Format event is called whenever the control needs to fill in the callback field, such as when the user selects a different date from that of the dropdown calendar. However, the FormatSize event is called only when there is a change to the format string (for instance, if you change it from "XX" to "XXXX".) This means that when you calculate the size of the callback field in the FormatSize event, you must take into account any possible value that can be returned by the Format event.

For example, you would use the following process to implement the callback field in the format string mentioned above.

  1. In the (General)(Declarations) section of the form, declare an array variable to hold the lookup table for the names of the Spanish months:
    Private sSpMonth(12) As String
  2. In the Load event of the form, populate the lookup table and set the DTPicker to use the custom format string:
    sSpMonth(0) = "Enero"
sSpMonth(1) = "Febrero"
sSpMonth(2) = "Marzo"
sSpMonth(3) = "Abril"
sSpMonth(4) = "Mayo"
sSpMonth(5) = "Junio"
sSpMonth(6) = "Julio"
sSpMonth(7) = "Agusto"
sSpMonth(8) = "Septiembre"
sSpMonth(9) = "Octubre"
sSpMonth(10) = "Noviembre"
sSpMonth(11) = "Diciembre"

DTPicker1.Format = dtpCustom
DTPicker1.CustomFormat = "MMMM (XXX) dd, yy"
  3. In the FormatSize event, search through the list of possible return values to find the longest one. Specify this as the length of the formatted string. This will prevent clipping of the Spanish month name when the value in the control is changed.
    Private Sub DTPicker1_FormatSize(ByVal CallbackField As String,
Size As Integer)
Dim iMaxMonthLen As Integer
Dim iC As Integer
Select Case CallbackField
Case "XXX"
iMaxMonthLen = 0
For iC = 0 To 11
If iMaxMonthLen < Len(sSpMonth(iC)) Then
iMaxMonthLen = Len(sSpMonth(iC))
End If
Next iC
End Select
Size = iMaxMonthLen
End Sub
  4. In the Format event, return the appropriate value as the formatted string:
    Private Sub DTPicker1_Format(ByVal CallbackField As String,
FormattedString As String)
Select Case CallbackField
Case "XXX"
FormattedString = sSpMonth(DTPicker1.Month - 1)
End Select
End Sub

You can create unique callback fields by repeating the "X" character. Thus, the format string "XX dddd MMM dd', 'yyy XXX" contains two callback fields. You can use Select or If statements to process multiple callback strings in the Format and FormatSize events.

Callback fields are treated as valid fields, so the application must be prepared to handle the CallbackKeyDown event. You can use this event to process individual keystrokes in the callback field and to provide keystroke validation or automated entry completion. For example, if the user was entering a month and they had typed the letter "D", you could use the CallbackKeyDown event to fill in the callback field with the word "December."

Formatting the Calendar Control

There are many options for formatting the calendar drop-down portion of the control. All the properties of the DateTimePicker that are preceded by the word "Calendar" affect the formatting of the calendar drop down. These properties correspond to properties in the MonthView control. For example, the DateTimePicker's CalendarTitleTextColor property is comparable to the TitleTextColor property of the MonthView. See the topic Using The MonthView Control for more information about calendar formatting issues.

Keyboard Interface

The Calendar control can be manipulated with the keyboard. The following table describes the different actions you can perform with the control at run time.

Key | Description
LEFT ARROW | Selects the next field to the left. If this key is pressed when the left-most field is selected, the selection wraps to the right-most field.
RIGHT ARROW | Selects the next field to the right. If this key is pressed when the right-most field is selected, the selection wraps to the left-most field.
UP ARROW | Increments the value of the selected field.
DOWN ARROW | Decrements the value of the selected field.
HOME | Changes the value of the selected field to its upper limit.
END | Changes the value of the selected field to its lower limit.
UP ARROW | Increments the value for the selected field.
DOWN ARROW | Decrements the value of the selected field.
PLUS (+) on Numeric Keypad | Increments the value of the selected field.
MINUS (-) on Numeric Keypad | Decrements the value of the selected field.

The following table describes the different actions you can perform while the dropdown calendar is displayed.

Key | Description
LEFT ARROW | Selects the next day
RIGHT ARROW | Selects the previous day
UP ARROW | Selects the same day of week in the previous week
DOWN ARROW | Selects the same day of week in the next week
PAGE UP | Scrolls the display to past months.
PAGE DOWN | Scrolls the display to future months.
CTRL+PAGE UP | Scrolls the display to the previous year.
CTRL+PAGE DOWN | Scrolls the display to the next year.

Distribution Note The DateTimePicker control is part of a group of ActiveX controls that are found in the MSCOMCT2.OCX file. To use the DateTimePicker control in your application, you must add the MSCOMCT2.OCX file to the project. When distributing your application, install the MSCOMCT2.OCX file in the user's Microsoft Windows System or System32 directory. For more information on how to add an ActiveX control to a project, see the Programmer's Guide.


Using the FlatScrollBar Control

A FlatScrollBar control provides the same functionality as the standard Windows scroll bar, with the addition of an enhanced interface. The FlatScrollBar can appear in one of three ways: as the standard, three-dimensional (beveled) scroll bar; as a two-dimensional (flat) scroll bar; or, as a flat scrollbar with arrows that become beveled when the mouse pointer hovers over it.

The FlatScrollBar can serve as either a horizontal or a vertical scroll bar. The Orientation property of the control determines how the scroll bar is used.

The FlatScrollBar also gives you the ability to disable either of the scroll arrows. Scroll arrows can be disabled by default, or toggled in response to program activity. For example, you can deactivate the downward scroll arrow when the thumb reaches the bottom of the scroll bar, informing the user that she can no longer scroll down.

Possible Uses

  • To create an interface that is visually consistent with Microsoft Internet Explorer 4.0.

  • To add a multimedia-style interface to existing applications.

  • To provide a variety of interface styles to the user without adding new controls.

Set the Min and Max Properties at Design Time or Run Time

The Min and Max properties determine the upper and lower limits of a FlatScrollBar control; you can set these properties at either design time or at run time. At design time, change the value of either property using the property sheet. You can also right-click on the control and select Properties to invoke the Property Pages, and change the value of either property from there.

At run time, you can reset the Min and Max settings to accommodate different ranges. For example, if the size of a container changes, you can reset the Max value to reflect the container's new height or width.

SmallChange and LargeChange Properties

The SmallChange and LargeChange properties determine how the FlatScrollBar control increments or decrements its Value property when the user clicks the control. The SmallChange property specifies how many units the thumb will move when the user presses the up or down (left or right) scroll arrows. The LargeChange property specifies how many units the thumb will move when the user clicks inside the area of the control but not on a scroll arrow or the thumb.

Displaying Different Interface Styles using the FlatScrollBar

The FlatScrollBar control has three different interface styles. The interface used is determined by the setting of the Appearance property. Use the interface that best complements the overall look and feel of your program's user interface.

You can set the Appearance property to fsb3D to have the FlatScrollBar appear as a standard Windows scroll bar. Set it to fsbFlat to give the scroll bar a 2–D appearance. A setting of fsbTrack3D will make the scroll bar appear 2–D, but the scroll arrows and thumb will become beveled in response to the mouse pointer.

Disabling Scroll Arrows in Response to Scroll Bar Movement

To give the user additional visual feedback when using the FlatScrollBar, you can disable scroll arrows in response to the Value of the control. This shows the user which scrolling options are available at any given time.

Arrows are specified according to their position, which changes based on the Orientation of the control. The left button of a horizontal scroll bar becomes the up button of a vertical scroll bar, and the right button becomes the down button. This relationship is reflected in the constant names used to specify the settings of the Arrows property.

Use the Arrows property to specify which of the control's scroll arrows are enabled. If you set the control to use only one arrow, the other will automatically be disabled. By default, both scroll arrows are enabled.

To disable the scroll arrows based on the control's Min and Max settings, enter the following code in the Change event of the FlatScrollBar control:

If FlatScrollBar1.Value = FlatScrollBar1.Min Then
    FlatScrollBar1.Arrows = fsbRightDown
ElseIf FlatScrollBar1.Value = FlatScrollBar1.Max Then
    FlatScrollBar1.Arrows = fsbLeftUp
Else
    FlatScrollBar1.Arrows = fsbBoth
End If


Using the ImageCombo Control

An ImageCombo control is similar to a standard Windows combo box control, with some important differences. The most visible difference is the ability to include pictures with each item in the list portion of the combo. By using graphic images, you can make it easier for the user to identify and choose items from a list of possible selections.

A less visible though equally important difference is the way the ImageCombo manages the list portion of the control. Each item in the list is a ComboItem object, and the list itself is the ComboItems collection of objects. This facilitates list management, making it easy to access items individually or collectively, and assign or change the properties that determine item content and appearance. This structure also makes it easier to deal with the images assigned to list items.

Because the items in the list are objects in a collection, certain properties found in the standard combo box (such as List, ListIndex, and ItemData) are no longer required. Therefore, these properties are not present in the ImageCombo control.

Each item in an ImageCombo list can have two pictures associated with it. The first picture, as specified by the Image property, appears in the drop-down portion of the control next to the text of the list item. The SelImage property specifies the list item's picture when it has been selected from the list. The SelImage picture appears next to the item in the edit portion of the combo box, as well as in the list portion.

To manage the images used for the list items, the ImageCombo uses the ImageList common control. Images are assigned to items in the ImageCombo through an index or key value that references a picture stored in the ImageList control.

The ImageCombo control also supports multiple levels of indentation. The amount of indentation is a property of the individual list item, so items maintain their level of indentation even if the list is reordered. Having items indented at different levels makes it possible to emphasize certain parts of the list or display hierarchical relationships.

Possible Uses

  • To create a Windows Explorer style interface or create custom dialog boxes that resemble the common File Open and File Save dialogs.

  • To create a list that uses different graphics and/or indenting to indicate the status of list items based on some external context, such as the state of other controls on the form.

  • To present hierarchical information to the user in a drop-down list format.

  • To add visual enhancements or indicators to standard drop-down lists.

  • To display a combo box with a standard appearance that makes use of advanced object-based properties, methods and structures in code.

Adding Items to the ImageCombo

To add a new item to an ImageCombo, you use the Add method to create a new ComboItem object in its ComboItems collection. You can supply optional arguments for the Add method to specify many of the properties of the new item, including its Index and Key values, any pictures it will use, and the level of indentation it will have. The Add method returns a reference to the newly created ComboItem object.

The following code adds a new item called "Signal1" to the top of the list in an ImageCombo, as indicated by the supplied Index value of 1. The new item appears in the control as "Traffic," as specified in the object's Text property. The code then takes the reference to the new item returned by the Add method and uses it to change the item's ToolTip text.

Dim objNewItem As ComboItem

   Set objNewItem = ImageCombo1.ComboItems.Add(1, _
   "Signal1", "Traffic")

   objNewItem.ToolTipText = "Traffic Light"

Using Pictures with ImageCombo list items

Images for the list items are supplied by an ImageList control associated with the ImageCombo control. For more information on using the ImageList control with other controls, see "Using the ImageList control."

To associate an ImageList Control with the ImageCombo at run time

  1. Populate the ImageList control with the images that will be used in the ImageCombo control.

  2. Right-click on the ImageCombo control and click Properties to open the Property Pages dialog box.

  3. On the General tab, click the ImageList box and select the ImageList control you have populated.

To associate an ImageList control with the ImageCombo control at run time, simply set the ImageList property to the name of the ImageList control, as shown in the example below:

Private Sub Form_Load()
   Set ImageCombo1.ImageList = ImageList1
End Sub

Setting an item's picture

To specify the image that will appear next to a list item in the ImageCombo control, set the Image property of a ComboItem object equal to the Index or Key value of an image in the ImageList control. For example, if the first image in the ImageList control was assigned a Key value of "Stoplight", the following two lines of code accomplish the same thing:

ImageCombo1.ComboItems("Signal1").Image = 1
ImageCombo1.ComboItems("Signal1").Image = "Stoplight"

Alternatively, you can specify the image that will be associated with the ComboItem when you create it. Simply specify the Key or Index of the correct image as a parameter of the Add method, for example:

ImageCombo1.ComboItems.Add(1, "Signal1", "Traffic", _
"Stoplight")

The selected item image

When you select an item from the list, the image specified by the ComboItem's SelImage property appears next to the item in the edit portion of the combo. The next time you drop down the combo box, the SelImage picture will appear next to the item in the list.

If your ImageList control contained a picture with a Key value of "Greenlight," you could use the following code to use that picture as the selected image for a list item:

ImageCombo1.ComboItems("Signal1").SelImage = _
"Greenlight"

Or you could specify the image to use as the selected image when the new item is added to the list. Pass the Key or Index value of the image as a parameter to the Add method:

ImageCombo1.ComboItems.Add(1, "Signal1", "Traffic", _
"Stoplight", "Greenlight")

Changing the Indentation of list items

The information in a combo list is often organized hierarchically. To facilitate this type of display, each ComboItem object has a specific level of indenting, as determined by the value of its Indentation property. Each level of indentation represents a space of ten pixels from the edge of the list, so a ComboItem with an Indentation of 3 would be indented by 30 pixels from a ComboItem with an Indentation of 0.

To set the default indentation of all items on the list, set the value of the ImageCombo control's Indentation property, either at design time using the property sheet, or through code.

Setting the Default Indentation of ComboItems

  • Select the Indentation property from the Property Sheet and change its value to an integer greater than zero. Each level of indentation represents ten pixels.

Setting the Default Indentation of ComboItems Through Code

Use the following code:

ImageCombo1.Indentation = 1

Setting the Indentation of Individual ComboItem Objects

To set the indentation of a particular list item, set the Indentation of the ComboItem object, specifying a Key or and Index value to identify the object. For example, the following code indents every third item in the list by 20 pixels:

For Each ComboItem In ImageCombo1.ComboItems
   If (ComboItem.Index / 3) = _
   (Int(ComboItem.Index / 3)) Then
      ComboItem.Indentation = 2
   End If
Next ComboItem

The following code indents the item with a Key value of "RightOn" by 40 pixels:

ImageCombo1.ComboItems("RightOn").Indentation = 4


Using the ImageList Control

An ImageList control contains a collection of images that can be used by other Windows Common Controls — specifically, the ListView, TreeView, TabStrip, and Toolbar controls. For example, the ImageList control can store all the images that appear on a Toolbar control's buttons.

The ImageList control can also be used with controls that assign a Picture object to a Picture property, such as the PictureBox, Image, and CommandButton controls.

Using the ImageList control as a single repository saves you development time by allowing you to write code that refers to a single, consistent catalog of images. Instead of writing code that loads bitmaps or icons (using the LoadPicture function), you can populate the ImageList once, assign Key values if you wish, and write code that uses the Key or Index properties to refer to images.

The control uses bitmap (.bmp), cursor (.cur), icon (.ico), JPEG (.jpg), or GIF (.gif) files in a collection of ListImage objects. You can add and remove images at design time or run time. The ListImage object has the standard collection object properties: Key and Index. It also has standard methods, such as Add, Remove, and Clear.

For More Information "Programming with Objects," in the Visual Basic Programmer's Guide, offers introductory information about working with objects and collections.

Finally, the control features the Overlay, Draw, and ExtractIcon methods, which allow you to create composite images, draw images on objects with an hDC property (such as the Form and Printer objects), and create an icon from a bitmap stored in the control.

Possible Uses

  • To store the images that represent open folders, closed folders, and documents. These images can then be dynamically assigned to the TreeView control's Node object to represent its different states as it expands or collapses, or whether or not it is a document or a folder.

  • To store images that represent common computer operations, such as saving, opening, and printing files. These images can then be assigned to Button objects on a Toolbar control used by your application.

  • To store images for drag-and-drop operations, such as MousePointer icons, and DragIcons.

Managing ListImage Objects and ListImages Collections

The ImageList control contains the ListImages collection of ListImage objects, each of which can be referred to by its Index or Key property value. You can add or remove images to the control at design time or run time.

Adding ListImage Objects at Design Time

To add an image to at design time, use the ImageList control's Property Pages dialog box.

To add ListImage objects at design time

  1. Right-click the ImageList control and click Properties.

  2. Click the Images tab to display the ImageList control's Property Pages, as shown below.

    ImageList control Property Pages dialog box

  3. Click Insert Picture to display the Select Picture dialog box.

  4. Use the dialog box to find either bitmap or icon files, and click Open.

    Note You can select multiple bitmap or icon files.

  5. Assign a unique Key property setting by clicking in the Key box and typing a string.

  6. Optional. Assign a Tag property setting by clicking in the Tag box and typing a string. The Tag property doesn't have to be unique.

  7. Repeat steps 3 through 6 until you have populated the control with the desired images.

Adding ListImage Objects at Run Time

To add an image at run time, use the Add method for the ListImages collection in conjunction with the LoadPicture function. The following example occurs in a form's Load event; an ImageList control named "imlImages" is loaded with a single bitmap:

Private Sub Form_Load()
   ' Assuming the path is correct, the open.bmp
   ' picture will be added to the ListImages
   ' collection. The Key property will also be
   ' assigned the value "open"
   imlImages.ListImages. _
   Add ,"open", LoadPicture("c:\bitmaps\open.bmp")
End Sub

Assigning a unique Key property value to the ListImage object allows you to create code that is easier to read. When assigning the image to a property, you can use its Key value instead of its Index value. Thus, assigning an image to a property might result in code like the following:

' Assign an image to a TreeView control Node object.
' The unique key of the image is "open".
TreeView1.Nodes.Add , , ,"Folder1","open"

Determining Image Sizes

You can insert any size image into the ImageList control. However, the size of the image displayed by the second control depends on one factor: whether or not the second control is also a Windows Common control bound to the ImageList control.

When the ImageList control is bound to another Windows Common Control, images of different sizes can be added to the control, however the size of the image displayed in the associated Windows Common Control will be constrained to the size of the first image added to the ImageList. For example, if you add an image that is 16 by 16 pixels to an ImageList control, then bind the ImageList to a TreeView control (to be displayed with Node objects), all images stored in the ImageList control will be displayed at 16 by 16 pixels, even if they are much larger or smaller.

On the other hand, if you display images using the Picture object, any image stored in the ImageList control will be displayed at its original size, no matter how small or large.

Note An exception is when you use an image from the ImageList control with the Image control. Setting the Image control's Stretch property to True will cause the image to resize to fit the control.

At design time, you can specify the height and width, in pixels, of images in the control by choosing a size from the General tab of the ImageList control's Property Pages dialog box. You can choose a predetermined size, or click Custom and set the image size by typing the size you desire in the Height and Width boxes. This can only be done when the ImageList contains no images. Attempting to change the size after the control contains images will result in an error.

Methods That Allow You to Create Composite Images

You can use the ImageList control to create a composite image (a picture object) from two images by using the Overlay method in conjunction with the MaskColor property. For example, if you have an "international no" image (a circle with a diagonal bar inside it), you can lay that image over any other image, as shown:

The syntax for the Overlay method requires two arguments. The first argument specifies the underlying image; the second argument specifies the image that overlays the first. Both arguments can be either the Index or the Key property of a ListImage object.

Thus the code to achieve the effect above is as follows:

' The composite image appears in a PictureBox
' control named "picOver". The Index value of
' the cigarette image is 2; the index value of the
' "no" symbol is 1.
ImageList1.MaskColor = vbGreen
Set picOver.Picture = ImageList1.Overlay(2, 1)

You could also use the Key property of the images, resulting in this code:

' Assuming the first image's Key is "smokes", and the
' second is "no".
Set picOver.Picture = ImageList1.Overlay("smokes","no")

The code example above also illustrates how the MaskColor property works. In brief, the MaskColor property specifies the color which will become transparent when an image is overlaid over another. The "no" image has a green background color. Thus, when the code specifies that the MaskColor will be vbGreen (an intrinsic constant), the green in the image becomes transparent in the composite image.


Using the ImageList with Other Controls

You can use the ImageList control as a repository of images for use by other Windows Common Controls and by controls with a Picture property.

Using the ImageList with Other Windows Common Controls

The ImageList control can be used to supply images for the following controls using certain of their properties, as listed in the following table.

Windows Common Control | Control Object | Properties Settable with ImageList Images
ImageCombo control | ComboItem | Image and SelImage
ListView control | ListItem | SmallIcon and Icon properties
TreeView control | Node | Image and SelectedImage properties
Toolbar control | Button | Image property
Toolbar control | Button | HotImageList property
Toolbar control | Button | DisabledImageList property
TabStrip control | Tab | Image property

For More Information For examples of using the ImageList with the ImageCombo, TreeView, ListView, Toolbar, and TabStrip controls, see the scenario topics for those controls. (For example, see "TreeView Control Scenario: Bind the TreeView to the Biblio.mdb Database.")

To use the ImageList with these controls, you must first associate the ImageList with the other control, and then assign either the Key or Index property to one of the properties listed in the table above. This can be done at design time or run time. All of the Windows Common controls, except the ListView control (discussed in this topic), have an ImageList property that can be set with the name of the ImageList control you are using.

Important You should populate the ImageList control with images before you associate it with another control. Once you have associated an ImageList with a control, and assigned any image to a property of the control, the ImageList control will not allow you to add any more images.

To associate the ImageList control with the TreeView, TabStrip, or Toolbar control at design time

  1. Right-click on the control using images from the ImageList control and click Properties to display the Property Pages dialog box.

  2. On the General tab, select the name of the ImageList control from the ImageList box.

To associate the ImageList control at run time, you might use the following code:

' Associate an ImageList named "imlImages" with a
' TreeView control named "tvwDB."
Set tvwDB.ImageList = imlImages

Once you have associated an ImageList control with another control, you can set properties for various objects using either the Key or Index property of an image in the ImageList control. For example, the following code sets the Image property of a TreeView control's Node object to an ImageList image with the Key property "leaf."

Private Sub Form_Load()
   ' The TreeView is named "tvwData."
   ' Add a node and set its Image property.
   ' The Key value of the image is "leaf."
   tvwData.Nodes.Add , ,"1 node","Top","leaf"
End Sub

Using the ImageList Control with the ListView Control

The ListView control can use two ImageList controls simultaneously. Instead of having a single ImageList property, the ListView control has an Icons and a SmallIcons property, each of which can be associated with an ImageList control. This can be done at design time or at run time.

To associate two ImageList controls with the ListView control at design time

  1. Right-click on the ListView control and click Properties to display the Property Pages.

  2. Click the ImageLists tab.

  3. In the Normal box, select the name of an ImageList control.

  4. In the Small box, select the name of another ImageList control.

You can also assign the ImageList controls at run time with code like that shown in the following example:

' Assuming the ListView control is named "lvwDB", the
' first ImageList is named "imlSmallImages," and the
' second is named "imlImages."
Set lvwDB.SmallIcons = imlSmallImages
Set lvwDB.Icons = imlImages

The ImageList control used depends on the display mode determined in the View property of the ListView control. When the ListView control is in Icon view, it uses the images supplied by the ImageList named in the Icons property. In any of the other views (List, Report, or SmallIcon), the ListView uses the images from the ImageList named in the SmallIcons property.

For More Information For details about the ListView control see "Using the ListView Control" later in this chapter.

Assigning ListImage Objects By Index or Key Property

After you have associated the ImageList control with one of the Windows Common Controls, you can specify a particular image using the image's Index or Key property.

For example, if you are using the ImageList with a TreeView control, the following code will assign the third ListImage object (which has an Index value of 3) to a new Node object's Image property:

' The TreeView control is named "tvwDB."
' The fifth argument of the Add method
' specifies an image by either the ListImage
' object's Index or Key property.
tvwDB.Nodes.Add , , ,"node x", 3

On the other hand, you could use the Key property to achieve the same end:

' Assuming the Key property is "open."
tvwDB.Nodes.Add , , ,"node x", "open"

Because the ListImage object's Key property must be a unique string, at run time, you can then use the Key property instead of the Index property to reference the image. This results in code that is easier to read.

Tip Because the Key must be a unique string, using a descriptive name for each ListImage object will make your code easier to read and debug.

Using the ImageList Control with Controls Not Part of the Windows Common Controls

You can also use the ImageList as an image repository for objects which have a Picture property. These include the following:

  • CommandButton control

  • OptionButton control

  • Image control

  • PictureBox control

  • CheckBox control

  • Form object

  • Panel object (StatusBar control)

The ListImage object's Picture property returns a Picture object, which can be assigned to another control's Picture property. For example, the following code will display the third ListImage object in a PictureBox control named "picBox":

Set picBox.Picture = ImageList1.ListImages(3).Picture


ImageList Scenario 1: Add Open, Save, and Print Images to a Toolbar Control

ImageList Scenario: Add Open, Save, and Print Images to a Toolbar Control

Toolbars typically contain a row of buttons where each button performs a frequently used operation when pressed. Toolbar buttons often save screen space by using images to represent the operation. In this scenario, three common functions — opening, saving, and printing a file — are represented by images assigned to Button objects of a Toolbar control. The images and buttons commonly used to represent these functions are shown here:

These objects are used in the following example:

  • ImageList control named "imlTool"

  • Toolbar control named "tbrStandard"

To add images to a Toolbar control

  1. Add images to the ImageList and assign unique Key property values to each object.

  2. Associate the ImageList with the Toolbar control.

  3. Assign images to Button objects using the Buttons tab.

Add Images to the ImageList and Assign Unique Key Property Values

To add ListImage objects at design time

  1. Right-click the ImageList control and click Properties to display the Property Pages dialog box.

  2. Click the Images tab.

  3. Click Insert Picture to display the Select Picture dialog box.

  4. Use the dialog box to find the bitmap files in the following table. The bitmaps can be found in the samples directory.

  5. After finding a file, click the file, and click Open, or double-click the file to open insert it into the ImageList control.

  6. In the Images tab, click the Key box and type the Key property value, as found in the following table. The Images tab should resemble the figure below.
    File | Key
    open.bmp | open
    save.bmp | save
    print.bmp | print

Images tab with three ListImage objects

Associate the ImageList with the Toolbar Control

Before you can assign the images to Button objects, you must first associate the ImageList with the Toolbar control.

To associate an ImageList with a Toolbar control

  1. Right-click on the Toolbar control and click Properties to display the control's Property Pages, as shown in the figure below.

  2. On the General tab, select the name of the ImageList control from the ImageList box.

Associate an ImageList with a Toolbar control

Assign Images to Button Objects Using the Buttons Tab

To assign an image to a Button object

  1. Click the Buttons tab (on the Toolbar control's Property Pages dialog box) to display the Buttons tab, shown in the figure below.

  2. Click Insert Button to insert a new Button object.

  3. Click the Image box and type the Key value of a ListImage object.

  4. Click Apply.

  5. Repeat steps 2 to 4 to add more buttons, and assign images to the new Button objects.

Add Button objects and assign Images using the Buttons tab


Using the Internet Transfer Control

The Internet Transfer control implements two widely-used Internet protocols: the HyperText Transfer Protocol (HTTP) and the File Transfer Protocol (FTP). Using the Internet Transfer control, you can connect to any site that uses one of these protocols, and retrieve files using either the OpenURL or Execute method.

Possible Uses

  • To add an FTP browser to any application.

  • To create an application that automatically downloads files from a public FTP site.

  • To parse a World Wide Web site for graphics references and download the graphics only.

  • To present a custom display of dynamic data retrieved from a Web page.

Basic Operation

The functionality of the Internet Transfer control depends on the protocol you wish to use. Because the two supported protocols work differently, the operations you can perform depend on which protocol you are using. For example, the GetHeader method only works with HTTP (HTML documents).

However, there are a few procedures that are common to both protocols. Basically, in order to use either protocol, you must:

  1. Set the AccessType property to a valid proxy server.

  2. Invoke the OpenURL method with a valid URL.

  3. Invoke the Execute method with a valid URL and command appropriate to the protocol.

  4. Use the GetChunk method to retrieve data from the buffer.

Setting the AccessType Property: Using a Proxy Server

In order to make any kind of connection to the Internet, you must determine how your computer is connected to the Internet. If you are on an intranet, you will probably be connected to the Internet via a proxy server.

In short, a proxy server is an intermediary between your computer and the Internet. All computers on an intranet that need to connect to the Internet must do so through a proxy server. Thus the proxy functions as a firewall between the intranet and the Internet, discarding invalid end-user and external requests, thereby protecting the intranet from hostile actions.

To find the proxy settings on your computer

Note The following steps apply only to the Windows 95 and Windows NT® 4.0 systems.

  1. On the Taskbar of your computer, click Start.

  2. On the Settings item, click the Control Panel.

  3. Double-click the Internet icon.

  4. On the Internet Properties dialog box, click Connection.

  5. Under Proxy Server, confirm that the Connect Through a Proxy Server check box is selected.

  6. If it is selected, click Settings. The name of proxy servers you use for various protocols will be found in the dialog box. If no proxy is defined, contact your system administrator for available proxy servers.

If you intend to use a proxy other than that named in the dialog box, set the AccessType property to icNamedProxy (2). Then set the Proxy property to the name of the proxy, as shown in the code below:

Inet1.Proxy = "myProxyName"
Inet1.AccessType = icNamedProxy

On the other hand, if you are content to use the default proxy (as determined by your computer's registry), ignore the Proxy property, and simply set the AccessType to icUseDefault (0).

The settings for AccessType are shown in the following table:

Constant | Value | Description
icUseDefault | 0 | (Default) Use Defaults. The control uses default settings found in the registry to access the Internet.
icDirect | 1 | Direct to Internet. The control has a direct connection to the Internet.
icNamedProxy | 2 | Named Proxy. Instructs the control to use the proxy server specified in the Proxy property.

Invoke the OpenURL Method

After you set the AccessType property, the most basic operation is to use the OpenURL method with a valid URL. When you use the OpenURL method, the result will depend on the target URL. For example, the following URL will return the HTML document found at www.microsoft.com:

' A TextBox control named Text1 contains the
' result of the method. The Internet Transfer
' control is named Inet1.
Text1.Text = Inet1.OpenURL("http://www.microsoft.com")

As a result, the TextBox control is filled with the HTML source, which may resemble the figure below:

In this case, the default action was to return the HTML document located at the URL. However, if the URL is modified to target a specific text file, the actual file would be retrieved. For example, the following code:

Text1.Text = Inet1. _
OpenURL("ftp://ftp.microsoft.com/disclaimer.txt")

would result in the actual text of the file, as shown below:

Tip When you use either the OpenURL or Execute method, you need not set the Protocol property. The Internet Transfer control will automatically set itself to the correct protocol, as determined by the protocol portion of the URL.

Finally, you can use the OpenURL method with a URL that includes appended data. For example, many Web sites offer the ability to search a database. To search, send a URL that includes the search criteria. For example, the following code would use a search engine named "search.exe" with the criteria "find=Maui."

Dim strURL As String
strURL = _
"http://www.howzit.com/cgi-bin/search.exe?find=maui
Text1.Text = Inet1.OpenURL(strURL)

If the search engine finds a match for the criteria, an HTML document would be assembled and returned with the appropriate information.

Saving to a File Using the OpenURL Method

If you wish to save the data retrieved through the OpenURL method to a file, use the Open, Put, and Close statements, as shown in the code below. This example streams a binary file into a Byte array before saving the data to disk:

Dim strURL As String
Dim bData() As Byte      ' Data variable
Dim intFile As Integer   ' FreeFile variable
strURL = _
"ftp://ftp.microsoft.com/Softlib/Softlib.exe"
intFile = FreeFile()      ' Set intFile to an unused
                        ' file.
' The result of the OpenURL method goes into the Byte
' array, and the Byte array is then saved to disk.
bData() = Inet1.OpenURL(strURL, icByteArray)
Open "C:\Temp\Softlib.exe" For Binary Access Write _
As #intFile
Put #intFile, , bData()
Close #intFile

A similar procedure can be used to write a text file to disk, except no Byte array is needed; the data is saved directly to the file:

Dim strURL As String      ' URL string
Dim intFile As Integer   ' FreeFile variable
IntFile = FreeFile()
strURL = "http://www.microsoft.com"
Open "c:\temp\MSsource.txt" For Output _
As #IntFile
Write #IntFile, Inet1.OpenURL(strURL)
Close #IntFile

Synchronous and Asynchronous Transmission

The OpenURL method results in a synchronous transmission of data. In this context, synchronous means that the transfer operation occurs before any other procedures are executed. Thus the data transfer must be completed before any other code can be executed.

On the other hand, the Execute method results in an asynchronous transmission. When the Execute method is invoked, the transfer operation occurs independently of other procedures. Thus, after invoking the Execute method, other code can execute while data is received in the background.

What does this mean for the user of the Internet Transfer control? In short, using the OpenURL method results in a direct stream of data that you can save to disk (as shown above), or view directly in a TextBox control (if the data was text). On the other hand, if you use the Execute method to retrieve data, you must monitor the control's connection state using the StateChanged event. When the appropriate state is reached, invoke the GetChunk method to retrieve data from the control's buffer. This operation is discussed in greater detail below.

Using the Execute Method with the FTP Protocol

The Execute method has four arguments: url, operation, data, and requestHeaders. FTP operations take only the operation argument and the url argument, which is optional. For example, to get a file from a remote computer, you could use the following code:

Inet1.Execute "FTP://ftp.microsoft.com", _
"GET disclaimer.txt C:\Temp\Disclaimer.txt"

If you are used to using FTP to retrieve files from anonymous FTP servers, you will be familiar with certain commands used to navigate through server trees, and to retrieve files to a local hard disk. For example, to change directory with the FTP protocol, you would use the command "CD" with the path of the directory you wish to change to.

For the most common operations, such as putting a file on a server and retrieving a file from a server, the Internet Transfer control uses the same or a similar command with the Execute method. For example, the following code uses the "CD" command as an argument of the Execute method to change directory:

' The txtURL textbox contains the path to open. The
' txtRemotePath textbox contains the path to change to.
Inet1.Execute txtURL.Text, "CD " & txtRemotePath.Text

Note When using the Execute method with FTP commands, the data and requestHeaders arguments are not used. Instead, all of the operations and their parameters are passed as a single string in the operation argument; parameters are separated by a space. In the descriptions below, do not confuse the terms "file1" and "file2" with the data and requestHeaders arguments.

The syntax for FTP operations is:

operationName file1 file2

For example, to get a file, the following code includes the operation name ("GET"), and the two file names required by the operation:

' Get the file named Disclaimer.txt and copy it to the
' location C:\Temp\Disclaimer.txt
Inet1.Execute, _
"GET Disclaimer.txt C:\Temp\Disclaimer.txt"

The following table lists the supported FTP commands of the control:

Operation Description Example
CD file1 Change Directory. Changes to the directory specified in file1.
Execute , "CD docs\mydocs"
CDUP Change to Parent. Same as "CD .."
Execute , "CDUP"
DELETE file1 Deletes the file specified in file1.
Execute , "DELETE discard.txt"
DIR [ file1 ] Searches the directory specified in file1. If file1 isn't supplied, the current working directory is searched. Use the GetChunk method to return the data.
Execute , "DIR /mydocs"
GET file1 file2 Retrieves the remote file specified in file1, and creates a new local file specified in file2.
Execute , _
"GET getme.txt C:\gotme.txt"
MKDIR file1 Creates a directory as specified in file1. Success is dependent on user privileges on the remote host.
Execute , "MKDIR /myDir"
PUT file1 file2 Copies a local file specified in file1 to the remote host specified in file2.
Execute , _
"PUT C:\putme.txt /putme.txt"
PWD Print Working Directory. Returns the current directory name. Use the GetChunk method to return the data.
Execute , "PWD"
QUIT Terminate current connection
Execute , "QUIT"
RECV file1 file2 Same as GET.
Execute , _
"RECV getme.txt C:\gotme.txt"
RENAME file1 file2 Renames a file. Success is dependent on user privileges on the remote host.
Execute ,
"RENAME old.txt new.txt"
RMDIR file1 Remove directory. Success is dependent on user privileges on the remote host.
Execute , "RMDIR oldDir"
SEND file1 Copies a file to the FTP site. (same as PUT.)
Execute , _
"SEND C:\putme.txt /putme.txt"
SIZE file1 Returns the size of the file specified in file1.
Execute "SIZE /largefile.txt"

Important If your proxy server is a CERN proxy server, direct FTP connections (using the Execute method) are disallowed. In that case, to get a file, use the OpenURL method with the Open, Put, and Close statements, as shown earlier in "Saving to a File Using the OpenURL Method." You can also use the OpenURL method to get a directory listing by invoking the method and specifying the target directory as the URL.

Using the Execute Method with the HTTP Protocol

The HTTP protocol allows client machines to request data from the server using the GET, HEAD, POST, and PUT commands. These operations are shown in the following table:

Operation Description Example
GET Retrieves the file named in url.
Execute "http://www.microsoft.com" & _
"/default.htm", "GET"
HEAD Retrieves only the headers of the file named in the URL property.
Execute , "HEAD"
POST Provides additional data to support a request to the remote host.
Execute , "POST", strFormData
PUT Replaces data at the specified URL.
Execute , "PUT", "replace.htm"

The Common Gateway Interface and the Execute Method

Many World Wide Web sites offer the ability to search a database. This is accomplished by using the HTTP protocol's ability to send queries using the Common Gateway Interface (CGI).

It is not in the scope of this topic to explain the CGI; however, if you are familiar with the CGI, you can use the Execute method to construct an application that simulates the behavior of World Wide Web sites. For example, the code below shows a typical CGI query string:

http://www.yippee.com/cgi-bin/find.exe?find=Hangzhou

This same query could be sent using the Execute method as shown below:

Dim strURL As String, strFormData As String
strURL = "//www.yippee.com/cgi-bin/find.exe"
strFormData = "find=Hangzhou"
Inet1.Execute strURL, "POST", strFormData

If you are expecting a result back from a server (as in the example above), you must use the GetChunk method to retrieve the resulting HTML document.

Using the State Event with the GetChunk Method

When you are downloading data from a remote computer, an asynchronous connection will be made. For example, using the Execute method with the operation "GET", will cause the server to retrieve the requested file. When the entire file has been retrieved, the State argument will return icResponseCompleted (12). At that point, you can use the GetChunk method to retrieve the data from the buffer. This is shown in the example below:

Private Sub Inet1_StateChanged(ByVal State As Integer)
   Dim vtData As Variant ' Data variable.
   Select Case State
   ' ... Other cases not shown.
   Case icResponseCompleted ' 12
      ' Open a file to write to.
      Open txtOperation For Binary Access _
      Write As #intFile

      ' Get the first chunk. NOTE: specify a Byte
      ' array (icByteArray) to retrieve a binary file.
      vtData = Inet1.GetChunk(1024, icString)

      Do While LenB(vtData) > 0
         Put #intFile, , vtData
         ' Get next chunk.
         vtData = Inet1.GetChunk(1024, icString)
      Loop
      Put #intFile, , vtData
      Close #intFile
   End Select
End Sub

Logging on to FTP Servers

FTP servers come in two flavors: public and private. Public servers, as suggested by the name, are open to anyone. Private servers, on the other hand, won't let you log on unless you are a bona fide user of the server. In either case, the FTP protocol demands that you supply a user name and a password. The two are used to authenticate a user and allow (or disallow) subsequent actions.

To log on to public servers the common practice is to log in as "anonymous," (UserName = "anonymous") and send your e-mail name as the password. However this process is simplified even further with the Internet Transfer control. By default, if you do not supply UserName and Password property values, the control sends "anonymous" as your UserName, and your e-mail name for the Password.

If you are logging on to a private server, simply set the UserName, Password, and URL properties as appropriate, and invoke the Execute method, as shown in the example below:

With Inet1
   .URL = "ftp://ftp.someFTPSite.com"
   .UserName = "John Smith"
   .Password = "mAuI&9$6"
   .Execute ,"DIR"   ' Returns the directory.
   .Execute ,"CLOSE" ' Close the connection.
End With

After you have invoked the Execute method, the FTP connection will remain open. You can then continue to use the Execute method to perform other FTP operations such as CD and GET. When you have completed the session, close the connection using the Execute method with the CLOSE operation. You can also close the connection automatically by changing the URL property, and invoking either the OpenURL or Execute method; such action will close the current FTP connection, and open the new URL.


Using the ListView Control

The ListView control displays data as ListItem objects. Each ListItem object can have an optional icon associated with the label of the object. The control excels at representing subsets of data (such as members of a database) or discrete objects (such as document templates).

Possible Uses

  • To display the results of a query on a database.

  • To display all the records in a database table.

  • In tandem with a Treeview control, to give users an expanded view of a TreeView control node.

Four Different Views Available

The ListView control can display data in four different view modes (as shown in the following figures). How you program the control depends on which of the different views you want the user to see (or select).

Each of these views has a particular advantage over the others. Some of these are listed in the following table:

View | Advantage
Icon | Can be manipulated with the mouse, allowing the user to drag and drop the object and rearrange the objects.
SmallIcon | Allows more ListItem objects to be viewed. Like Icon view, objects can be rearranged by the user.
List | Presents a sorted view of ListItems object.
Report | Presents a sorted view, with SubItems allowing extra information to be displayed.

ListView in Icon view

ListView in SmallIcon view

ListView in List view

ListView in Report view

Change the View with the View Property

To change the display mode, use the View property. The following code sets the View property to the Report view (3), using the intrinsic constant lvwReport:

' The name of the control is "ListView1"
ListView1.View = lvwReport

Using the View property, you can allow the end user to dynamically change the view. In the Form's Load event, the ComboBox is populated with View choices:

Private Sub Form_Load()
   ' Populate the ComboBox control. The
   ' ComboBox control is named "cmbChooseView."
   With cmbChooseView
      . AddItem "Icon"         '0
      . AddItem "Small Icon"   ' 1
      . AddItem "List"         ' 2
      . AddItem "Report"      ' 3
   End With
End Sub

In the ComboBox control's Click event, the View of the control can then be reset, as shown:

Private Sub cmbChooseView_Click()
   ' The name of the ListView control is "lvwDB."
   lvwDB.View = cmbChooseView.ListIndex
End Sub

Two ImageList Controls for Icons and SmallIcons

A ListItem object consists of a label (the Text property) and an optional image which is supplied by an ImageList control. However, the ListView control, unlike other controls, can use two ImageList controls, which you set using the Icons and SmallIcons properties. Whether or not you use one or both ImageList controls depends on the intended display mode, as determined by the View property.

In the List, SmallIcon, and Report views, you can use a small icon to represent the ListItem object. One ImageList control (specified by the SmallIcons property) supplies the images for use in any of these three views. At design time or run time, set the SmallIcons property to the ImageList control that will supply these images. At design time, use the ListView control's Property Pages dialog box to set the ImageList for the SmallIcons. At run time, use the following code:

ListView1.SmallIcons = imlSmallIcons

In contrast, when the control is in Icon view, the control uses a different set of images supplied by a second ImageList control. Set the Icons property to this second ImageList control at design time using the Property Pages dialog box, or use the following code at run time:

ListView1.Icons = imlIcons

Note The size of the icons you use is determined by the ImageList control. The available sizes are 16 x 16, 32 x 32, 48 x 48, and Custom. For more information about the ImageList control, see "ImageList Control."

If you are planning to use more than one view, and you wish to display images, you must set the SmallIcon and Icon properties for each ListItem object. The following code first declares an object variable of type ListItem, then sets the object variable to a single ListItem object added to the collection using the Add method. The SmallIcon and Icon images are then set with the object variable reference:

Dim itmX as ListItem
Set itmX = ListView1.ListItems.Add()
' Assuming an image named "smallBook" exists in the
' ImageList set to the SmallIcons property.
itmX.SmallIcon = "smallBook"
' Assuming an image named "BigBook" exists in the
' ImageList that is set to the Icons property.
itmX.Icon = "BigBook"

After setting an image with the SmallIcon and Icon properties, the correct image will be displayed automatically when switching views using the View property.

ColumnHeaders Are Displayed in Report View

A unique feature of the Report view is ColumnHeader objects. The ListView control contains a collection of ColumnHeader objects in the ColumnHeaders collection.

A ColumnHeader

The ColumnHeader object has a Text property that displays text when the control is in Report view. You can also set the Width property of each ColumnHeader, as well as the Alignment property (which sets the alignment of the text displayed in the ColumnHeader object). The following example code creates four ColumnHeader objects and sets their Text and Width properties:

Dim colX As ColumnHeader ' Declare variable.
Dim intX as Integer ' Counter variable.
For intX = 1 to 4
   Set colX = ListView1.ColumnHeaders.Add()
   colX.Text = "Field " & intX
   colX.Width = ListView1.Width / 4
Next intX

Set Column Text with the ListSubItems Collection

Notice that in any of the views except Report view, the ListItem object displays only one label — the Text property. But in Report view, every ListItem object can have several other text items. For example, the "Hitchhiker's Guide to Visual Basic…" also has an author ("Vaughn, William R."), year (1996), and ISBN number associated with it. Each of these text items are members of the ListSubItems collection. To create a ListSubItem object, use the Add method for the ListSubItems collection. Thus, to set the author, year and ISBN number of a ListItem object, the code might look like this:

' The control is named lvwAuthors.
lvwAuthor.ListItems(23).ListSubItems.Add , , _
"Hitchhiker's Guide to Visual Basic and SQL Server"
lvwAuthor.ListItems(23).ListSubItems.Add , , _
"Vaughn, William R."
lvwAuthor.ListItems(23).ListSubItems.Add , , "1996"
lvwAuthor.ListItems(23).ListSubItems.Add , , " 1-55615-906-4"

Note The ListItems string array used in Visual Basic versions 4.0 and 5.0 has been supplanted by the ListSubItems collection.

SubItems Depend on ColumnHeaders Presence

Both the presence and number of ListSubItem objects depends on the presence and number of ColumnHeader objects. That is, you cannot create any ListSubItem objects if there are no ColumnHeader objects present. Further, the number of ColumnHeader objects determines the number of ListSubItem objects you can set for the ListItem object. And the number of ListSubItems is always one less than the number of ColumnHeader objects. This is because the first ColumnHeader object is always associated with the Text property of the ListItem object, as shown below:

First ColumnHeader and SubItems

Thus, if the number of ColumnHeader objects in the above example is 4, then the maximum possible number of members of the ListSubItems collection is 3.


ListView Scenario 1: Using the ListView Control with the TreeView Control

The ListView control is often used in tandem with the TreeView control. (For more information on the TreeView control, see "Using the TreeView control.") The combination allows the end user to "drill down" through several hierarchical layers; the TreeView displays the larger structure, while the ListView displays the individual sets of records as each Node object is selected, as shown in the following illustration:

TreeView and ListView together

In this scenario, whenever a "Publisher" node on the TreeView control is clicked (the NodeClick event), the code populates the ListView control with the book titles owned by the publisher.

This scenario builds upon the scenario found in "Using the TreeView Control" by further binding the Biblio.mdb to the ListView control. The complete code for the four scenarios can be found in "ListView Scenarios: Complete Code."

Sample Application: DataTree.vbp

The code examples in this topic are taken from the DataTree.vbp sample application, which is listed in the Samples directory.

The following procedure uses these objects:

  • Form object named "frmDataTree"

  • TreeView control named "tvwDB"

  • ListView control named "lvwDB"

  • ImageList control named "imlIcons"

  • ImageList control named "imlSmallIcons"

To use a ListView control with the TreeView control

  1. Add a reference to the Data Access Objects (DAO 3.5) to the project.

  2. Declare module-level variables for the Database and ListItem objects.

  3. Populate two ImageList controls with appropriate images.

  4. Assign the ImageList controls to the Icons and SmallIcons properties of the ListView control.

In the Form Load event:

  5. Set the Database object variable to the Biblio.mdb database using the OpenDatabase statement.

In the TreeView control's NodeClick event:

  6. If the Node's Tag property is "Publisher" then call procedures to create ColumnHeaders and populate the ListView control.

Add a Reference to the Data Access Objects (DAO 3.5) to the Project

To bind a database to the ListView control, you must first add a reference to the current version of Data Access Objects (DAO).

Declare Module-Level Variables for the Database and ListItem Objects

Since you will want to access the Biblio.mdb database several times during a single session, it's more efficient to keep a single copy of the database open by creating a global Database object. Thereafter, you can access the database without reopening it. In the Declarations section of the form, write:

Private mDbBiblio As Database

(The variable is declared as Private, making it a module-level scope variable. If you want the database to be used by other modules, use the Public statement, and rename the variable to reflect its global status, i.e., gDbBiblio.)

When adding ListItem objects to the ListItems collection, you should use the Set statement with a variable of type ListItem.

Dim TempItem As ListItem
Set TempItem = lvwDB.ListItems.Add()

While you can declare the variable whenever you add ListItem objects, it is more efficient to declare a single module-level ListItem object variable once and use it to add all ListItem objects. Again in the Declarations section, write:

Private mTempItem As ListItem

Populate Two ImageList Controls with Appropriate Images

To use images in the ListView control, you must first populate two ImageList controls with images. Thereafter, at design time you can set the Icons and SmallIcons properties to the two ImageList controls. See "Using the ImageList control" for more information.

Assign the ImageList Controls to the Icons and SmallIcons Properties of the ListView Control

If you do not wish to set the Icon and SmallIcon properties at design time, you can set them at run time, as shown:

lvwDB.Icons = imlIcons
lvwDB.SmallIcons = imlSmallIcons

One reason for setting the ImageList controls at run time rather than design time would be to dynamically change the images for a different user. For example, a user with a monochrome screen may want icons that have a higher contrast of elements.

Set the Database object variable to the Biblio database using the OpenDatabase statement

The Form object's Load event can be used to initialize the Database variable. The code for this would be:

Set mDbBiblio = DBEngine.Workspaces(0). _
OpenDatabase("BIBLIO.MDB")

After you have successfully initialized the Database object variable, you can freely access it from anywhere within the code module.

If the Node's Tag property is "Publisher" Then Call Procedures to Create ColumnHeaders and Populate the ListView Control

When the TreeView control is populated ("See Using the TreeView Control"), the Tag property of every Node object is set to the name of the database table to which the Node belongs. In this scenario, if the Tag property's value is "Publisher," then the code invokes two user-designed procedures. The first, MakeColumns, creates columns for the ListView control.

The second function, GetTitles, populates the ListView control with ListItem objects. The function, however, requires an argument, the Key property of the Node object which contains the publisher's unique ID from the "Publishers" table. The GetTitles function uses the Key to search the "Titles" database to return all titles that belong to the publisher. The complete NodeClick code is shown:

Private Sub tvwDB_NodeClick(ByVal Node As Node)
   If Node.Tag = "Publisher" Then
      MakeColumns ' Create Columnheaders.
      GetTitles Val(Node.Key) ' Make ListItem objects.
End Sub

Tip The Key property value cannot be an integer, but must be a unique string. However, the string can be an integer followed by a string, for example "7 ID." The code above uses the Val function which returns only that part of the Key property value which is an integer. Therefore, when setting the Node object's Key property, use a string appended to the PubID field's value. For example:

tvwDB.Nodes(x).Key = rsPublishers!PubID & " ID"

Using the Val function will then return only the original PubID value.

Next Step: The MakeColumns Procedure

To see the MakeColumns function, see "ListView Controls Scenario 2: Using a Procedure to Create ColumnHeaders."


ListView Scenario 2: Using a Procedure to Create ColumnHeaders

The code examples in this topic are taken from the DataTree.vbp sample application, which is listed in the Samples directory.

In the previous scenario, "ListView Control Scenario 1: Using the ListView Control with the TreeView Control," the ListView control and the TreeView control are showed working in tandem. In that scenario, the TreeView control's NodeClick event is used to call two procedures. The first procedure, "MakeColumns," which creates ColumnHeader objects, is outlined here.

The following example uses this object:

  • ListView control named "lvwDB"

To create ColumnHeader objects are

  1. Clear the ColumnHeaders collection with the Clear method and create ColumnHeaders.

  2. Use the Add method to create ColumnHeader objects.

Clear the ColumnHeaders collection with the Clear method and Create ColumnHeaders

The procedure first clears all members of the ColumnHeaders collection using the Clear method:

lvwDB.ListItems.Clear

This step is necessary if you intend to create different sets of ColumnHeader objects for different tables. For example, you may want to populate the ListView control not only when the Publisher nodes are clicked, but also when the user clicks the root of the tree. In that case, the ListView control would be populated with a different list.

Use the Add Method to Create ColumnHeader Objects

After clearing the ColumnHeaders collection, you use the Add method to add ColumnHeader objects to the collection, as shown:

Private Sub MakeColumns()
   ' Clear the ColumnHeaders collection.
   lvwDB.ColumnHeaders.Clear
   ' Add four ColumnHeaders.
   lvwDB.ColumnHeaders.Add , , "Title", 2000
   lvwDB.ColumnHeaders.Add , , "Author"
   lvwDB.ColumnHeaders.Add , , "Year", 350
   lvwDB.ColumnHeaders.Add , , "ISBN"
End Sub

Note that the Add method syntax allows you to set the Width property for each ColumnHeader object. In the above code, the Width property is set only for the "Title" and "Year" ColumnHeader objects.


ListView Scenario 3: Using a Procedure to Retrieve Titles from the Biblio.mdb Database

The code examples in this topic are taken from the DataTree.vbp sample application which is listed in the Samples directory.

In scenario, "Using the ListView Control with the TreeView Control," the ListView control and the TreeView control are shown working in tandem. The TreeView control's NodeClick event is used to call two procedures, one to create ColumnHeader objects, and a second to populate the ListView control.

This scenario continues by developing the second procedure, called "GetTitles," that populates the ListView control with titles from the Biblio.mdb database.

The following example uses these objects:

  • Form object named "frmDataTree"

  • TreeView control named "tvwDB"

  • ListView control named "lvwDB"

  • ImageList control named "imlIcons"

  • ImageList control named "imlSmallIcons"

To create a procedure that populates the ListView control

  1. In the GetTitles procedure, clear the ListItems collection with the Clear method.

  2. Use a query to create a "Titles" recordset.

  3. Use the Do Until statement to create a ListItem object for each record in the recordset.

GetTitles Procedure: Clear the ListItems collection with the Clear method

Each time you invoke the GetTitles procedure, you will query the Biblio.mdb database to create a new collection of ListItem objects. The first thing to do, however, is to clear the old collection with the Clear method:

lvwDB.ListItems.Clear ' Clears all ListItems.

Use a query to create a "Titles" recordset

When the GetTitles procedure is invoked, the Key property's value of the Node property is passed to the procedure. Since the Key property contains the unique number that identifies the publisher (the PubID field), this value can be used to search the Titles table for all matches with the same PubID value.

The most efficient method to accomplish this is to create a query that finds only those records which have the same PubID value in the Titles table's PubID field. This query is:

Set rsTitles = mDbBiblio.OpenRecordset _
("select * from Titles where PubID = " & PubID)

Use the Do Until Statement to Create a ListItem Object for Each Record in the Recordset

Once the recordset is created, you can add to the ListItem collection. The following code iterates through this recordset to create a ListItem object for every record, setting the Text and SubItems properties with data from the recordset. The complete procedure is shown here:

Private Sub GetTitles(PubID)
   ' Clear the old titles.
   lvwDB.ListItems.Clear
   ' Declare object variable of type Recordset
   Dim rsTitles As Recordset
   ' While on this record, create a recordset using a
   ' query that finds only titles that have the same
   ' PubID. For each record in this recordset, add a
   ' ListItem object to the ListView control, and set
   ' the new object's properties with the record's
   ' Title, ISBN and Author fields.
   Set rsTitles = mDbBiblio.OpenRecordset _
   ("select * from Titles where PubID = " & PubID)

   Do Until rsTitles.EOF
   ' Add ListItem.
         Set mItem = lvwDB.ListItems.Add()
         mItem.Text = rsTitles!TITLE
         mItem.SmallIcon = "smlBook"
         mItem.Icon = "book"
         mItem.Key = rsTitles!ISBN
         ' Use a function to get the author and set
         ' the SubItems(1) property.
         mItem.SubItems(1) = GetAuthor(rsTitles!ISBN)
         If Not IsNull(rsTitles![Year Published]) Then
            mItem.SubItems(2) =  _
            rsTitles![Year Published]
         End If
         mItem.SubItems(3) = rsTitles!ISBN
         rsTitles.MoveNext
   Loop
End Sub


ListView Scenario 4: Using a Function to Return an Author's Name from the Biblio.mdb Datbase

ListView Scenario 4: Using a Function to Return an Author's Name from the Biblio.mdb Database

The code examples in this topic are taken from the DataTree.vbp sample application, which is listed in the Samples directory.

In the scenario, "ListView Control Scenario 1: Using the ListView Control with the TreeView Control," the ListView control and the TreeView control are showed working in tandem. In that scenario, the TreeView control's NodeClick event is used to call a procedure called "GetTitles" to populate the ListView control. That procedure queries the "Titles" table to create a recordset, then creates a ListItem object for each record it finds.

One piece of information that is not contained in the "Titles" table is the name of the book's author. Instead, the "Titles" table contains a field named "ISBN" that stores the ISBN number of the book. The value of this field is also contained in a second table called "Title Author" which links the ISBN value with a field (Au_ID) that identifies the author.

To retrieve the author's name, the function must

  1. Find the ISBN value in the "Title Author" recordset.

  2. Find the AuthorID value in the Authors recordset.

  3. Return the name of the author.

Setup

The scenario uses the following object:

  • ListView control named "lvwDB"

Find the ISBN Value in the "Title Author" Recordset

The GetAuthor function uses the value of the ISBN field to search the "Title Author" recordset. As in other database operations, first declare an object variable of type RecordSet, then open the "Title Author" table and assign the reference to the object variable. However, since the code uses two recordsets, declare the object variables together, then open the recordsets, as shown.

Dim rsTitleAuthor As Recordset
Dim rsAuthors As Recordset

Set rsTitleAuthor = mDbBiblio. _
OpenRecordset("Title Author", dbOpenDynaset)
Set rsAuthors = mDbBiblio. _
OpenRecordset("Authors", dbOpenDynaset)

With the rsAuthors object variable and the value from the ISBN field, search the "Title Author" table:

Dim strQuery As String
strQuery = "ISBN = " & "'" & ISBN & "'"
rsTitleAuthor.FindFirst strQuery

Find the AuthorID value in the Authors recordset

The FindFirst method will return the first match that meets the criteria in the query. The other field in the "Title Author" table is the "Au_ID" field, which contains the ID of an author in the "Authors" table. Therefore, we can now use the value from the "Au_ID" field in the query, and search the "Authors" table, as shown:

' Reset query string.
strQuery = "Au_ID = " & rsTitleAuthor!AU_ID
' Search again using the FindFirst method.
rsAuthors.FindFirst strQuery

Return the Name of the Author

Finally, the "Authors" table has been entered, and the "Au_ID" field has been searched. Presuming no errors have been made, the current recordset should return the name of the author. The following code instructs the function to return the author's name to the calling code:

GetAuthors = rsAuthors!Author

The Complete GetAuthor Function

The complete function is shown:

Private Function GetAuthor(ISBN)
   ' Declare DAO object variables.
   Dim rsTitleAuthor As Recordset
   Dim rsAuthors As Recordset
   ' Set object variables to recordsets.
   Set rsTitleAuthor = mDbBiblio. _
   OpenRecordset("Title Author", dbOpenDynaset)
   Set rsAuthors = mDbBiblio. _
   OpenRecordset("Authors", dbOpenDynaset)
   ' Create query string.
   Dim strQuery As String
   strQuery = "ISBN = " & "'" & ISBN & "'"
   rsTitleAuthor.FindFirst strQuery
   ' If there is no author, return "n/a."
   ' Otherwise, return the name of the author.

   If rsTitleAuthor.NoMatch Then
      GetAuthor = "n/a"
      Exit Function
   Else
      ' Presume we have found the right recordset.
      ' Then reset the string query with Au_ID
      ' field value and search "Authors" table.
      strQuery = "Au_ID = " & rsTitleAuthor!AU_ID
      rsAuthors.FindFirst strQuery
      ' Return the name of the author from the Author
      ' field.
      GetAuthor = rsAuthors!Author
   End If
End Function


ListView Scenarios: Complete Code

The code discussed in the ListView scenarios 1 through 4, except for the procedure to populate the TreeView controls, is shown below. For code to populate the TreeView control, see "TreeView Control Scenario: Binding the TreeView to the Biblio.mdb Database." For a working example of this code, see the sample application DataTree.vbp which is listed in the Samples directory.

' General Declarations
Private mDbBiblio As Database ' Database variable.

Private Sub Form_Load()
   ' Open the Biblio.mdb and set the object variable
   ' to the database.
   Set mDbBiblio = DBEngine.Workspaces(0). _
   OpenDatabase("Biblio.mdb")

   ' Code to populate the TreeView control
   ' isn't shown here.
End Sub

Private Sub tvwDB_NodeClick(ByVal Node As Node)
   ' Check the Tag for "Publisher." If so, then
   ' call the MakeColumns procedure and then the
   ' GetTitles function.
   If Node.Tag = "Publisher" Then
      MakeColumns
      GetTitles Val(Node.Key)
   End If
End Sub

Private Sub MakeColumns()
   ' Clear the ColumnHeaders collection.
   lvwDB.ColumnHeaders.Clear
   ' Add four ColumnHeaders.
   lvwDB.ColumnHeaders.Add , , "Title", 2000
   lvwDB.ColumnHeaders.Add , , "Author"
   lvwDB.ColumnHeaders.Add , , "Year", 350
   lvwDB.ColumnHeaders.Add , , "ISBN"
End Sub

Private Sub GetTitles(PubID)
' Clear the old titles.
lvwDB.ListItems.Clear
' Declare object variable of type Recordset.
Dim rsTitles As Recordset
' While on this record, create a recordset using a
' query that finds only titles that have the same
' PubID. For each record in this recordset, add a
' ListItem object to the ListView control, and set
' the new object's properties with the record's
' Title, ISBN, and Author fields.
Set rsTitles = mDbBiblio.OpenRecordset _
("select * from Titles where PubID = " & PubID)

Do Until rsTitles.EOF
' Add ListItem.
      Set mItem = lvwDB.ListItems.Add()
      mItem.Text = rsTitles!TITLE
      mItem.SmallIcon = "smlBook"
      mItem.Icon = "book"
      mItem.Key = rsTitles!ISBN
      ' Use a function to get the author and set
      ' the SubItems(1) property.
      mItem.SubItems(1) = GetAuthor(rsTitles!ISBN)
      If Not IsNull(rsTitles![Year Published]) Then
         mItem.SubItems(2) =  _
      rsTitles![Year Published]
      End If
      mItem.SubItems(3) = rsTitles!ISBN
      rsTitles.MoveNext
Loop
End Sub

Private Function GetAuthor(ISBN)
   ' Declare DAO object variables.
   Dim rsTitleAuthor As Recordset
   Dim rsAuthors As Recordset
   ' Set object variables to recordsets.
   Set rsTitleAuthor = mDbBiblio. _
   OpenRecordset("Title Author", dbOpenDynaset)
   Set rsAuthors = mDbBiblio. _
   OpenRecordset("Authors", dbOpenDynaset)
   ' Create query string.
   Dim strQuery As String
   strQuery = "ISBN = " & "'" & ISBN & "'"
   rsTitleAuthor.FindFirst strQuery
   ' If there is no author, return "n/a."
   ' Otherwise, return the name of the author.

   If rsTitleAuthor.NoMatch Then
      GetAuthor = "n/a"
      Exit Function
   Else
      ' Presume we have found the right recordset.
      ' Then reset the string query with Au_ID
      ' field value and search "Authors" table.
      strQuery = "Au_ID = " & rsTitleAuthor!AU_ID
      rsAuthors.FindFirst strQuery
      ' Return the name of the author from the Author
      ' field.
      GetAuthor = rsAuthors!Author
   End If
End Function


Using the MAPI Control

Using the MAPI Controls

The messaging application program interface (MAPI) controls allow you to create mail-enabled Visual Basic applications. MAPI is a set of core system components that seamlessly connect any mail-enabled or workgroup application to MAPI-compliant information services. For example, the Microsoft Exchange messaging system can be connected to most private or public e-mail systems through the use of MAPI drivers.

In Visual Basic, the MAPI controls are used to interact with the underlying message subsystem. To use these controls, you must first install a MAPI-compliant e-mail system like Microsoft Exchange. The underlying messaging services are provided by the workgroup environment — the Microsoft Exchange Server running under Windows 95 or Windows NT, for instance.

Using the MAPI controls involves two steps: establishing a MAPI session and then using various properties and methods to access and manage an individual Inbox. For example, create and send a message, include a file attachment, verify the recipient's address against the e-mail system's address book, etc.

The MAPISession control signs on and establishes a MAPI session. It is also used to sign off from a MAPI session. The MAPIMessages control contains all the properties and methods needed to perform the messaging system functions described above.

The MAPI controls are invisible at run time. In addition, there are no events for the controls. To use them you must set the appropriate properties or specify the appropriate methods.

Note If you attempt to run a program that uses the MAPI controls, make sure that you have the 32-bit MAPI DLLs installed properly or you may not be able to perform MAPI functions such as SignOn. For example, on Windows 95, you must install Exchange during the operating system setup, or install it separately from the control panel to correctly use MAPI functions.

Possible Uses

  • To add messaging functionality to your application.

  • To create a full-featured electronic mail application.

Using the MAPISession control

The MAPISession control is used to sign in and out of a MAPI session. Assuming that MAPI services are present, logging in with the MAPISession control is merely a matter of providing a registered user name and password. The MAPISession control will determine the electronic mail settings specified in the operating system and invoke the underlying message subsystem (the mail server).

The MAPISession control

Setting MAPISession Properties

You can set the MAPISession properties at design time using the MAPISession Property Pages. Right-click the MAPISession control and click Properties to display the Property Pages dialog box.

Setting the MAPISession properties at design time

The UserName and Password properties are used to provide a valid sign-on to the underlying messaging system. You can either set these properties at design time or prompt the user at run time. Setting the LogonUI property to True will provide the user with the sign-on dialog box of the underlying mail system. If such a dialog box does not exist, this property will be ignored. You can create a custom dialog box to prompt the user for this information.

The NewSession property specifies whether a new mail session should be established. If a valid session is already established, setting the NewSession property will allow two sessions to run concurrently.

The DownloadMail property specifies whether the user's mail will be downloaded at the beginning of the current session automatically. Setting this value to True will download all the user's mail to their Inbox. Depending upon the mail system and the amount of incoming mail, this can be a processing-intensive operation. By setting this property to False, the user can choose to download mail at a later time or set the interval at which mail is automatically downloaded.

The SignOn and SignOff Methods

Once the UserName and Password properties have been set, use the SignOn method at run time to begin the MAPI session.

For example:

mpsSession.SignOn

When the session is established, the session handle is stored in the SessionID property. Depending upon the value of the NewSession property, the session handle may refer to a newly created session or an existing session.

To end the session use the SignOff method.

Note The Action property can also be used to sign in and out of a MAPI session. It is recommended, however, that you use the SignOn and SignOff methods instead. The Action property is included for compatibility with earlier versions of Visual Basic.

The SessionID Property

When the SignOn method is used to successfully establish a messaging session, the SessionID property will return a unique messaging session handle. The SessionID value will be used by the MAPIMessages control to create an association with a valid messaging session. By default, this value is 0.

Using the MAPIMessages Control

Once you've logged into a messaging session using the MAPISession control, you use the MAPIMessages control to receive, send, or read messages from the Inbox which was specified at sign-on time.

The MAPIMessages control provides you with properties and methods to perform basic electronic mail tasks. For example, composing a message, addressing it to a recipient, verifying that the recipient's address is valid, or attaching a file.

The MAPIMessages control

In most cases, the MAPIMessage control is used to provide e-mail capabilities to certain functions within an application. For example, you may want to send a notification message to a workgroup alias after your application has automatically created a report. In other words, you can add e-mail capabilities to your application, without creating full-fledged e-mail applications.

You can, however, use the MAPI controls to create very powerful mail-enabled and workgroup applications.

Sample Application: VBMail.vbp

The examples in the following sections demonstrate the basic use of MAPI in Visual Basic. For more detailed use of the properties and methods of the MAPI controls refer to the VBMail.vbp sample application, which is listed in the Samples directory.

Note You can also use the OLE Messaging technology to add MAPI functionality to your application. Information on OLE Messaging can be found in the Microsoft Exchange Software Development Kit.

Associating SessionID Properties

The SessionID property of the MAPIMessages control contains the messaging session handle returned by the SessionID property of the MAPISession control. To associate the MAPIMessages control with a valid messaging session, set this property to the SessionID of a MAPISession control that was successfully signed on. For example:

mpmMessage.SessionID = mpsSession.SessionID

This association to a valid MAPI session must be made before you can access messages and begin working with the MAPIMessages control.

Accessing Messages

Successfully logging into a MAPI session accesses the Inbox of the registered user specified by the UserName and Password properties of the MAPISession control. The Inbox is the message store. When the Inbox is opened two buffers are created: the compose buffer and the read buffer.

The read buffer is made up of an indexed set of messages fetched from the user's Inbox. The MsgIndex property is used to access individual messages within this set, starting with a value of 0 for the first message and incrementing by one for each message through the end of the set.

The message set is built using the Fetch method. The set includes all messages of type FetchMsgType and is sorted as specified by the FetchSorted property. The value of the FetchMsgType property is controlled by the underlying message system. The FetchSorted property can be set to add messages to the message set (read buffer) in the order they are received or in the order specified by the user's Inbox. Previously read messages can be included or left out of the message set with the FetchUnreadOnly property.

Messages in the read buffer can't be altered by the user, but can be copied to the compose buffer for alteration.

Messages are created or edited in the compose buffer. The compose buffer is active when the MsgIndex property is set to –1. Many of the messaging actions are valid only within the compose buffer, such as sending messages, saving messages, or deleting recipients and attachments.

Composing and Managing Messages

Managing messages is the primary function of the MAPIMessages control. Composing, sending, receiving, and deleting messages are performed by using methods that correspond to these basic functions (compose, copy, delete, etc.). The following table lists the MAPIMessages methods that specifically apply to messages:

Method | Description
Compose | Composes a new message
Copy | Copies the currently indexed message to the compose buffer
Delete | Deletes a message, recipient, or attachment
Fetch | Creates a message set from selected messages in the Inbox
Forward | Forwards a message
Reply | Replies to a message
ReplyAll | Replies to all message recipients
Save | Saves the message currently in the compose buffer
Send | Sends a message

To perform an action on an individual message, it must first be selected using the MsgIndex property. The message identified by the MsgIndex property is called the currently indexed message.

None of the other message properties can be set until a message is selected with the MsgIndex property. The index number can range from –1 to MsgCount –1 (the MsgCount property returns the total number of messages in the message set.) When a new message is created, the value of the MsgIndex property is set to –1.

Composing a Message

Use the Compose method to create a new message. When the Compose method is used, the compose buffer is cleared and the MsgIndex property is set to –1.

'Compose new message
mpmMessage.Compose

Completing a message involves several steps: determining the recipient of the message, choosing a subject title, and writing the message.

Addressing the Message

To set the recipient's name and address, use the RecipDisplayName and the RecipAddress properties. The RecipDisplayName is the proper name of the recipient, for example, "Richard Tull". The RecipAddress property contains the recipient's e-mail address: "richtull", for example.

'Address message
mpmMessage.RecipDisplayName = "Richard Tull"
mpmMessage.RecipAddress = "richtull"

Addresses for recipients outside the local workgroup (a message sent to someone at another company via the Internet, for example) require a complete Internet e-mail address: "richtull@littlemag.com".

Verifying the Recipient's Name

The recipient's name is verified when the message is sent by checking it against the list of registered users in the e-mail system, using the ResolveName method. Name verification for recipients outside the local workgroup is handled in various ways by the underlying message system.

The following example resolves the recipient's valid e-mail name by invoking the ResolveName method and setting the AddressResolveUI property to True.

' Resolve recipient name
mpmMessage.AddressResolveUI = True
mpmMessage.ResolveName

The AddressResolveUI property can be set to either display a details dialog box (True) or generate an error (False) when an ambiguous or invalid recipient address is encountered when the message is sent. The details dialog box will offer you an alternative address if a close match is found.

The Message Subject and Text

The MsgSubject property specifies the subject line for the message. You may enter up to 64 characters, including the Null character.

The body of the message is contained in the MsgNoteText property. For inbound messages, each paragraph is terminated with a carriage return-line feed pair (vbCrLf). Outbound messages can be delimited with a carriage return (vbCr), line feed (vbLf), or a carriage return-line feed pair.

'Create the message
mpmMessage.MsgSubject = "Status Report"
mpmMessage.MsgNoteText = "Build successful!"

Sending the Message

To send the message, use the Send Method. The Send method allows you to send a message with or without user interaction. Setting the value to True will display the compose message dialog box of the underlying e-mail system (Microsoft Exchange, for example). Setting it to False will send the message without displaying the compose message dialog. The following example sends the message without prompting for user interaction:

'Send the message
mpmMessage.Send False

Handling File Attachments

You can add file attachments to outgoing messages using the file attachment properties. These properties are listed in the following table:

Property | Description
AttachmentCount | Returns the total number of attachments associated with the currently indexed message.
AttachmentIndex | Sets the currently indexed attachment.
AttachmentName | Specifies the name of the currently indexed attachment file.
AttachmentPathName | Specifies the full path name of the currently indexed attachment.
AttachmentPosition | Specifies the position of the currently indexed attachment within the message body.
AttachmentType | Specifies the type of the currently indexed file attachment.

To add an attachment to an outgoing message, use the AttachmentPathName property to specify the name and path of the file. For example:

'Add attachment
mpmMessage.AttachmentPathName = "c:\Status _
Report.doc"

If the path name is incorrect or empty, an error will be generated.

Simply specifying the AttachmentPathName property will send the attachment with the message, use the name of the file to display in the message body, and position the attachment at the beginning of the message.

The AttachmentName property can be used to specify a different name for the attached file. If this property isn't set, the actual name of the file will be displayed in the message body.

The AttachmentPosition property is used to position the attachment within the message body. By default, the value is "0" and the attachment is located at the beginning of the message body. To position the attachment at the end of the message, count the number of characters in the message body. For example, in a message body that is five characters long, you could place an attachment at the end of the message by setting the value to 4. (The message body occupies character positions 0 to 4).

Two attachments can't be placed in the same position within the same message. You also can't place an attachment equal to or beyond the end of the message body. You can append an extra space or a vbCrLf character to the end of the message body and then set the AttachmentPosition property to one character less than the length of the MsgNoteText property.

Managing Messages

Many of the remaining properties and methods of the MAPIMessages control can be used to manage messages just as you would in a full-featured e-mail application.

By accessing messages in the read buffer you can sort, delete, or forward one or a number of messages. The following table lists the properties you can use to manage messages:

Property | Description
MsgConversationID | Specifies the conversation thread identification value for the currently indexed message.
MsgCount | Returns the total number of messages present in the message set during the current messaging session.
MsgDateReceived | Returns the date on which the currently indexed message was received.
MsgID | Returns the string identifier of the currently indexed message.
MsgIndex | Specifies the index number of the currently indexed message.
MsgOrigAddress | Returns the mail address of the originator of the currently indexed message.
MsgOrigDisplayName | Returns the originator's name for the currently indexed message.
MsgRead | Returns a Boolean expression indicating whether the message has already been read.
MsgReceiptRequested | Specifies whether a return receipt is requested for the currently indexed message.
MsgSent | Specifies whether the currently indexed message has already been sent to the mail server for distribution.
MsgType | Specifies the type of the currently indexed message.

Working with the Address Book

The e-mail system's address book contains all of the addressing information for each registered user in the e-mail system. The address book is a dialog box which allows the user to look up or verify recipient addresses. The address book properties allow you to set or modify elements of the address book.

The Show Method

The e-mail system's address book is displayed using the Show method. The Show method can be set to show either the address book or the recipient details dialog box.

By default, the value is set to False and the address book dialog box is displayed when the Show method is used.

To display the details dialog box, set the value to True. The amount of information displayed in this dialog box depends upon the e-mail system. At minimum, the name and address of the recipient are displayed.

Address Book Properties

The address book properties allow you to set or modify elements of the e-mail system's address book. The following table lists these properties:

Property | Description
AddressCaption | Specifies the caption appearing at the top of the address book.
AddressEditFieldCount | Specifies which edit controls to display to the user in the address book.
AddressLabel | Specifies the appearance of the "To" edit control in the address book.
AddressModifiable | Specifies whether the address book can be modified by the user.
AddressResolveUI | Specifies whether a dialog box is displayed for receipt name resolution during addressing.

All of the address book properties can be set at design time using the MAPIMessages control's Property Pages dialog box.

Setting the MAPIMessages properties at design time

Setting the address book properties at design time allows you to specify options for the display and functionality of the address book dialog box. For example, you can modify the caption that appears in the address book dialog box using the AddressCaption property.


Using the MaskedEdit Control

The MaskedEdit control is used to prompt users for data input using a mask pattern. You can also use it to prompt for dates, currency, and time, or to convert input data to all upper- or lowercase letters. For example, to prompt the user to enter a phone number, you can create the following input mask: "(___) - ___ - ____". If you don't use an input mask, the MaskedEdit control behaves much like a standard text box.

When you define an input mask using the Mask property, each character position in the MaskedEdit control maps to a placeholder of a specified type, or to a literal character. Literal characters, or literals, give visual cues about the type of data being used. For example, the parentheses surrounding the area code of a telephone number are literals: (206).

The input mask prevents users from entering invalid characters into the control. If the user attempts to enter a character that conflicts with the input mask, the control generates a ValidationError event.

The MaskedEdit control is a bound control and can be used with a data control to display or update field values in a data set.

Possible Uses

  • To prompt for date/time, number, or currency information.

  • To prompt for custom mask formats such as a telephone number or any other input that follows a pattern.

  • To format the display and printing of mask input data.

  • To work with a data control to display and update field values in a data set.

The Mask Property

The Mask property determines the type of information that is input into the MaskedEdit control. The Mask property uses characters such as the pound sign (#), backslash (\), comma (,), and ampersand (&) as placeholders that define the type of input. The following table lists all the characters you can use to set the Mask property:

Mask character | Description
# | Digit placeholder.
. | Decimal placeholder. The actual character used is the one specified as the decimal placeholder in your international settings. This character is treated as a literal for masking purposes.
, | Thousands separator. The actual character used is the one specified as the thousands separator in your international settings. This character is treated as a literal for masking purposes.
: | Time separator. The actual character used is the one specified as the time separator in your international settings. This character is treated as a literal for masking purposes.
/ | Date separator. The actual character used is the one specified as the date separator in your international settings. This character is treated as a literal for masking purposes.
\ | Treat the next character in the mask string as a literal. This allows you to include the '#', '&', 'A', and '?' characters in the mask. This character is treated as a literal for masking purposes.
& | Character placeholder. Valid values for this placeholder are ANSI characters in the following ranges: 32-126 and 128-255.
> | Convert all the characters that follow to uppercase.
< | Convert all the characters that follow to lowercase.
A | Alphanumeric character placeholder (entry required). For example: a z, A Z, or 0 9.
a | Alphanumeric character placeholder (entry optional).
9 | Digit placeholder (entry optional). For example: 0 9.
C | Character or space placeholder (entry optional).
? | Letter placeholder. For example: a z or A Z.
Literal | All other symbols are displayed as literals; that is, as themselves.

To create an input mask, you combine mask characters with literal characters. Literal characters are characters which rather than representing some data type or format, are used as themselves. For example, to create an input mask for a phone number you define the Mask property as follows:

MaskEdBox1.Mask = (###) - ### - ####

The pound sign (a digit placeholder) is used with the left and right parentheses and the hyphen (literal characters). At run time, the MaskedEdit control would look like the following:

A MaskedEdit control with a phone number mask

When you define an input mask, the insertion point automatically skips over literals as you enter data or move the insertion point.

The Text and ClipText Properties

All data entered in the MaskedEdit control is contained in and can be retrieved from the Text property. This is a run time only property and includes all the literal and prompt characters of the input mask. For instance, retrieving data from the Text property of the example above returns the string "(555) - 555 - 5555" – the phone number that was entered.

The ClipText property also returns data entered in the MaskedEdit control, but without the literal and prompt characters. Using the example above, retrieving data from the ClipText property returns the string "5555555555". The ClipText property is available only at run time.

Defining the Input Character

By default, all mask characters are underlined. This indicates to the user that the character is a placeholder for data input. When the user enters a valid character, the underline disappears. If you want the underline to remain, you can set the FontUnderline property of the MaskedEdit control to True.

You can also change the underline input character to a different character by using the PromptChar property. For example, to change the underline (_) character to the asterisk (*) character, you simply redefine the value of the PromptChar property:

MaskEdBox1.PromptChar = "*"

Using Mask Characters as Literals

If you want to use a mask character as a literal, you can precede the mask character with a backslash (\). For example, if you want the pound sign (#) to display, you set the mask as follows:

MaskEdBox1.Mask = "\##"

This would produce a mask that displays a pound sign (#) followed by a blank space for entering a number.

The Format Property

You can modify how the MaskedEdit control is displayed and printed using the Format property. The Format property provides you with standard formats for displaying number, currency, and date/time information.

The following table lists the standard formats you can use with the Format property:

Data type | Value | Description
Number | (Default) Empty string | General Numeric format. Displays as entered.
Number | $#,##0.00;($#,##0.00) | Currency format. Uses thousands separator; displays negative numbers enclosed in parentheses.
Number | 0 | Fixed number format. Displays at least one digit.
Number | #,##0 | Commas format. Uses commas as thousands separator.
Number | 0% | Percent format. Multiplies value by 100 and appends a percent sign.
Number | 0.00E+00 | Scientific format. Uses standard scientific notation.
Date/Time | (Default) c | General Date and Time format. Displays date, time, or both.
Date/Time | dddddd | Long Date format. Same as the Long Date setting in the International section of the Microsoft Windows Control Panel. Example: Tuesday, May 26, 1992.
Date/Time | dd-mmm-yy | Medium Date format. Example: 26-May-92.
Date/Time | ddddd | Short Date format. Same as the Short Date setting in the International section of the Microsoft Windows Control Panel. Example: 5/26/92.
Date/Time | ttttt | Long Time format. Same as the Time setting in the International section of the Microsoft Windows Control Panel. Example: 05:36:17 A.M.
Date/Time | hh:mm A.M./P.M. | Medium Time format. Example: 05:36 A.M.
Date/Time | hh:mm | Short Time format. Example: 05:36.

You use the Format property with the Mask property. For example, to create a mask that prompts for a Short Date input that displays in the Long Date format, you set the Mask and Format properties as follows:

MaskEdBox1.Mask = "##-##-##"
MaskEdBox1.Format = "dddddd"

When the user enters the date in the short format (06-27-96, for instance), the MaskedEdit control verifies that the entered data is valid, and then, when the focus passes to the next control, it is displayed as "Thursday, June 27, 1996".

Note To automatically shift the focus to the next control when the data has been verified as valid, set the AutoTab property of the MaskedEdit control to True.

The Format property also allows you to specify custom formatting using the same format expressions defined by the Visual Basic Format function.

For More Information See "Format Function" or "Format Property (MaskedEdit control)."

Setting Properties at Design Time

You can set the property values at design time using the MaskedEdit control Property Pages. Click the Custom option in the Properties window of the MaskedEdit control to bring up the Property Pages dialog box, as shown below:

Setting the Mask property at design time

You enter the mask and format patterns as in the run time examples above. The Format drop down list allows you to select any of the predefined standard formats shown above. This dialog box also allows you to easily set such properties as PromptChar, ClipMode, and MaxLength.

A MaskedEdit field can have a maximum of 64 characters (the valid range is 1 to 64 characters). This includes literal characters as well as mask characters. You can set this value using the MaxLength property. At design time, this property is set automatically to the number of characters in the pattern when you enter a mask pattern.

The ClipMode property specifies whether or not literal characters are included when doing a cut or copy command. By default, when a selection in the MaskedEdit control is copied to the Clipboard, the entire selection, including the literals, is transferred. To limit the copy operation to only the data entered by the user, set the ClipMode property to True.

The ValidationError Event

The ValidationError event occurs when the MaskedEdit control receives invalid input, as determined by the input mask. For example, if you've defined an input mask that prompts for numbers, a ValidationError event will occur if the user attempts to enter a letter. Unless you write an event handler to respond to the ValidationError event, the MaskedEdit control will simply remain at the current insertion point — nothing will happen.

Mask characters are validated as they are entered and the insertion point is shifted to the right. When a character is entered or deleted out of sequence (when a digit is inserted or deleted after the phone number has been entered, for example), all nonliteral characters shift either to the right or left. When the shift occurs, if an invalid character replaces the position of a valid character, the ValidationError event is triggered.

For example, suppose the Mask property is defined as "?###", and the current value of the Text property is "A12." If you attempt to insert the letter "B" before the letter "A," the "A" would shift to the right. Since the second value of the input mask requires a number, the letter "A" would cause the control to generate a ValidationError event.

The MaskedEdit control also validates the values of the Text property at run time. If the Text property settings conflict with the input mask, the control generates a run-time error.

You can select text in the same way you would with a standard text box control. When selected text is deleted, the control attempts to shift the remaining characters to the right of the selection. However, any remaining character that might cause a validation error during this shift is deleted, and no ValidationError event is generated.

For More Information See "ValidationError Event."

Using MaskedEdit as a Bound Control

The MaskedEdit control is a bound control. This means that it can be linked to a data control and display field values for the current record in a data set. The MaskedEdit control can also write out values to a data set.

Note When the value of the field referenced by the DataField property is read, it is converted to a Text property string, if possible. If the recordset is updatable, the string is converted to the data type of the field.

The MaskedEdit control has three bound properties: DataChanged, DataField, and DataSource.

For More Information See "Using the ADO Data Control" in "Using Visual Basic's Standard Controls" in the Programmer's Guide for information on using bound controls.


Using the MonthView Control

The MonthView control makes it easy for users to view and set date information via a calendar-like interface. Users can select a single date or a range of dates.

The control can be navigated using either the keyboard or mouse. Buttons at the top of the control are used to scroll months in and out of view.

In addition, the control has the ability to display up to 12 months at a time. This can be helpful when you want to give users the ability to view date information around the date of interest.

MonthView control

Possible Uses

  • To present date information where a calendar representation of a date is easier to understand than that of a Label or Textbox control.

  • To give users the ability to choose a date with the click of a mouse rather than typing a date value.

  • To enable users to view multiple months as part of an advanced booking system such as those used by hotels and airlines.

Setting and Returning Dates

The currently selected date in the control is determined by the Value property. You can set the Value of the control before it is displayed (for example, in the form's Load event) to determine which date will be initially selected in the control:

MonthView1.Value = "10/31/97"

By default, the control's Value is set to the current date, and the current date is selected when the control first appears. If you change the MonthView's Value through code, the control will be updated immediately to reflect the new setting.

Certain events of the control can also be used to return date values. For example, the DateClick event is fired whenever the user clicks on a date in the control. The DateClick event returns a DateClicked value that evaluates to the date the user clicked. Similarly, the DateDblClick event returns a DateDblClicked value indicating the date that was double-clicked to cause the event. When using these events, you should refer to the value returned by the event, rather than the Value property of the control, as Value is not updated until after the event has occurred.

The Value property returns a date value that you can format in whatever manner you wish. The MonthView control also has several properties that return specific date information. The Month property returns the integer value (1-12) of the month containing the currently selected date. The Day property returns the day number (1-31) currently selected, and the DayOfWeek property returns a value indicating the day of the week the selected date falls on (values correspond to the values of vbDayOfWeek constants.) The Year property returns the year containing the selected date as an integer value. Finally, there is a Week property that will return the number of the week containing the selected date.

These properties can be set as well as read at run time, with varying effects on the currently selected date. For example, changing the value of the DayOfWeek property will select the date that falls on the specified day in the same week. This will change the value of the Day property, but depending on how the week falls, it may also change the value of the Month property. Changing the value of the Year property will cause the current date to be selected in the specified year, which may affect the value of DayOfWeek.

You should be especially careful when changing the value of the Week property. Setting a different week as the current week will not change the value of DayOfWeek, but will affect the value of Day, and possibly the values of Week, Month or even Year. For example, if the user has selected a Friday, and you set the value of Week to 52 in a year that ends on a Wednesday, the value of Week will become 1, and the values of Month and Year will also change. Weeks 1 and 52 will usually overlap.

Selecting A Range of Dates

You can use the MonthView control to display a contiguous range of dates, or to allow the user to select a date range. In order to extend the selection to more than one date, the MultiSelect property must be set to True. You can control the maximum number of days that may be selected by changing the value of the MaxSelCount property. By default, the maximum selection allowed is seven days.

The SelStart and SelEnd properties determine which days are selected. You can check the values of these properties to find out what range the user has selected. If only one date is selected, the two values will be the same. You can also set the values of these properties in code, which will cause a range of dates to become selected in the control. When setting the SelStart and SelEnd properties through code, you must observe the following rules:

  • The SelStart date must occur before the SelEnd date.

  • The selected range must contain the currently selected date. If necessary, you should set the Value property to one of the dates in the range before setting the SelStart and SelEnd values.

  • The number of dates included in the range cannot exceed the maximum range size as specified by MaxSelCount.

For example, to select the week before Halloween through code, you would use the following:

MonthView1.Value = "10/31/97"
MonthView1.MaxSelCount = 7
MonthView1.SelStart = "10/25/97"
MonthView1.SelEnd = "10/31/97"

Formatting the Control's Appearance

The MonthView control allows you to customize its appearance in many ways. Various color attributes such as MonthBackColor, TitleBackColor, TitleForeColor and TrailingForeColor enable you to create a unique color scheme or the control. For example, TrailingForeColor determines the color of the dates that precede and follow the displayed month or months.

The font settings for the control determine the width and height of the control. Using larger fonts causes the control to grow to accommodate the increased font size. Conversely, you can set the font to a smaller size to shrink the control. Font size is set through the Font property.

Displaying More Than One Month

The MonthView control can display from one to twelve months at a time. You specify how many months will be displayed and how they will be arranged within the control. You do this by using the MonthRows and MonthColumns properties. For example, setting MonthRows to 2 and MonthColumns to 3 would result in a MonthView control that displayed six months at a time. Clicking on the arrow buttons at the top of the control would scroll the next six months or the previous six months into view.

Individual months are separated by a separator bar. The size of the separator is dependent on control size and display characteristics, but you can retrieve the dimensions used via the SeperatorHeight and SeparatorWidth properties. You can also retrieve the size of a single calendar by using the CalendarWidth and CalendarHeight properties. Together these properties are useful in determining how the control will change size if another row or column is added, and adjusting the size and position of your controls accordingly.

Other Formatting Settings

You can determine which day of the week will appear as the first day by changing the value of the StartOfWeek property. You can choose to display week numbers in the control by setting the ShowWeekNumbers property to True. Week numbers appear in a separate column to the left of the first day of the week. The display of the current date at the bottom of the control can be enabled or disabled using the ShowToday property.

Formatting Specific Days

Any day number on the MonthView control can also have its font set to bold. This capability can be used to draw attention to dates (i.e. holidays, vacations, etc.) or just distinguish them from ordinary dates. More than one date can be bolded at the same time. Although the bold information for a day is not preserved while scrolling from month to month, this behavior can be simulated using the GetDayBold event.

The following example makes all Sundays bold, even when new months are scrolled into view:

Private Sub MonthView1_GetDayBold(ByVal StartDate As Date, ByVal Count As Integer, State() As Boolean)
   Dim i As Integer
   i = vbSunday
   While i < Count
      State(i - MonthView1.StartOfWeek) = True
      i = i + 7
   Wend
End Sub

Keyboard Interface

The MonthView control can be manipulated using the keyboard. The following table describes the different actions you can perform with the control at runtime.

Key | Description
LEFT ARROW | Select the next day
RIGHT ARROW | Select the previous day
UP ARROW | Select the same day of week in the previous week
DOWN ARROW | Select the same day of week in the next week
PAGE UP | Scrolls the display to past months.
PAGE DOWN | Scrolls the display to future months.
CTRL+PAGE UP | Scrolls the display to the previous year.
CTRL+PAGE DOWN | Scrolls the display to the next year.

Navigating the MonthView

In addition to the two buttons at the top of the control, the MonthView control provides other ways to set the currently displayed month(s). You can display a particular month of any particular year.

To select a particular month

  1. Click on the month name at the top of the calendar. A context menu will appear with the names of all the months.

  2. From the menu, choose the name of the month you want to select.

The control will display the selected month in the current year.

To go directly to a particular year

  1. Select the month you want.

  2. Click on the year number at the top of the calendar. This will place it in edit mode.

  3. Either click the up/down scroll buttons to scroll the year or type the year in the edit box.

  4. Click anywhere outside the edit box or press the ENTER key when you are done.

The control will display the current month in the year you specified.

Distribution Note The UpDown control is part of a group of ActiveX controls that are found in the COMCT232.ocx file. To use the UpDown control in your application, you must add the COMCT232.ocx file to the project. When distributing your application, install the COMCT232.ocx file in the user’s Microsoft Windows System or System32 directory. For more information on how to add an ActiveX control to a project, see the Programmer’s Guide.


Using the MSChart Control

The MSChart control allows you to plot data in charts according to your specifications. You can create a chart by setting data in the control’s properties page, or by retrieving data to be plotted from another source, such as a Microsoft Excel spreadsheet. The information in this topic focuses on using an Excel worksheet as a data source.

Possible Uses

  • To chart dynamic data, such as current prices of selected commodities.

  • To plot stored data, such as product prices, for graphic analysis of trends.

Plot Data Using Arrays and the ChartData Property

The simplest way of plotting a chart is to create an array of numeric values, and then set the ChartData property to the array, as shown in the following example:

' This code might be pasted into the Load event
' of a Form that has an MSChart control named
' "MSChart1".
Dim arrPrices(1 to 10)
Dim i As Integer
For i = 1 to 10
   arrPrices(i)= i * 2
Next i
MSChart1.ChartData = arrPrices

The code above produces a simple, single-series chart. A “series” in a chart, is set of related data points. For example, a typical series might be the prices of a commodity over the course of a year. The chart below shows a single-series chart.

To create a more complex, multi-series chart, you must create a multi-dimensioned array, as shown in the following example:

' The number of series are determined by the second
' dimension. In this example, the chart will have two
' series, with five data points in each series.
Dim arrPriceQuantity(1 to 5, 1 to 2)
Dim i as Integer
For i = 1 to 5
   arrPriceQuantity(i, 1) = i ' Series 1
   arrPriceQuantity(i, 2) = 0 - i ' Series 2
Next i
MsChart1.ChartData = arrPriceQuantity

This will produce the following chart:

Adding Labels to the Chart

When you create a multi-dimensioned array, the first series can be assigned a string; when the array is assigned to the ChartData property, the strings become the labels for the rows. The following code shows this feature.

Dim arrValues(1 to 5, 1 to 3)
Dim i as Integer
For i = 1 to 5
   arrValues(i, 1) = "Label " & i ' Labels
   arrValues(i, 2) = 0 + i ' Series 1 values.
   arrValues(i, 3) = 2 * i ' Series 2 values.
Next i
MsChart1.ChartData = arrValues

The above code produces the chart shown below:

As you can see, creating a chart using the ChartData property can be quick and simple. However, the problem with using an array is getting the data into the array. Most users of this kind of data will probably prefer to use a spreadsheet program, such as Microsoft Excel, or perhaps a database program, such as Microsoft Access, to store and retrieve the data.

Setting or Returning a Data Point

Once you have created a chart, using an array from a spreadsheet or other data source, you may also want to set or return the value of a particular data point. This can be done by first setting the Row and (if applicable) Column properties, then setting or returning the Data property. For example, in a simple (single-series) chart, the following code would change the third data point.

With MSChart1
   ' Change third data point to 50.
   .Row = 3
   .Data = 50
End With

If the chart has more than one series use the Column property to designate the series, then set the Row and Data properties as above.

With MSChart1
   ' Set the second data point of the fourth series
   ' to 42.
   .Column = 4
   .Row = 2
   .Data = 42
End With

Using the PointActivated Event to Change a Data Point

If you've started to explore the MSChart control, you will notice that it has a large number of events. These events allow you to program the chart to respond to practically any action of the user. As an example of this programmability, the PointActivated event is used in the following example to show how a data point can be changed using the Series and DataPoint parameters. (The PointActivated event occurs whenever a data point is double-clicked.) The Series and DataPoint parameters correspond to the Column and Row properties, and can thus be used to set the Data property:

Private Sub MSChart1_PointActivated(Series As _
   Integer, DataPoint As Integer, MouseFlags As _
   Integer, Cancel As Integer)
   With MSChart1
      .Column = Series
      .Row = DataPoint
      .Data = InputBox _
      ("Change the data point:", , .Data)
   End With
End Sub


Data Binding the MSChart Control

The Microsoft Chart control is a data bound control, and allows you to graphically represent numeric data. Unlike other data bound controls, however, the Chart control cannot be used with the Remote Data Control, or the Data control. It can be used with the ADO Data Control, an ADO Recordset, and the Data Environment. This example will show how to open an ADO Recordset that contains the fields you want to display, and set the Chart control's DataSource property to the Recordset object. If the first field contains string data, that data will be used as the X axis labels.

The example below shows three series of data by first creating a Recordset object that has four fields; the first field contains the labels for the X axis, and remaining fields are displayed as series data.

Option Explicit
' Be sure to set a reference to the Microsoft ActiveX Data
' Objects 2.0 Library.
Private rsProducts As New ADODB.Recordset
Private cn As New ADODB.Connection

Private Sub Form_Load()
   Dim strQuery As String ' SQL query string.

   ' First change the path to a valid path for your machine.
   cn.ConnectionString = _
   "Provider=Microsoft.Jet.OLEDB.3.51;Data Source=" & _
   "C:\Program Files\Microsoft Visual Studio\VB98\nwind.mdb" ' <-Change this path.

   ' Open the connection.
   cn.Open

   ' Create a query that retrieves only four fields.
   strQuery = "SELECT ProductName, UnitPrice, " & _
   "UnitsInStock, UnitsOnOrder FROM Products WHERE SupplierID = 1"
   ' Open the recordset.
   rsProducts.Open strQuery, cn, adOpenKeyset
   ' Set the DataSource to the recordset.
   With MSChart1
      .ShowLegend = True
      Set .DataSource = rsProducts
   End With
End Sub


Using an Excel Worksheet to Populate an Array

If you have data stored in an Excel worksheet, you can use the GetObject method to get a reference to the workbook that contains the worksheet, then retrieve the values using the reference. To first get the reference, the only argument needed is the path to the workbook, as shown below:

' Place this code in the Declarations section.
Option Explicit
' If you are using Office 97, be sure to set a
' reference to Microsoft Excel 8.0 Object Library.
' If you are using Office 95, set a reference to
' Microsoft Excel 5.0 Object Library, and declare
' the variable as a Worksheet.
Dim wkbObj As WorkBook ' Declare an object variable.

Private Sub Form_Load()
   ' Then set the variable with the GetObject method.
   Set wkbObj = GetObject _
      ("C:\My Documents\MySpread.xls")
End Sub

Note In order to use Excel objects, you must set a reference to the Excel Objects library. To do this, click the Project menu, then click References. Search for the Excel Objects library and double-click it. Click OK. Also, note that if you're using GetObject with the Excel 5.0 Object Library, you must declare the variable as a WorkSheet; if you are using the Excel 8.0 Object Library, declare the variable as a WorkBook.

After setting a reference to the Excel object library, you can use the reference to walk through the Excel object model, populating the array with data from the worksheet. To do this, use the Range method in conjunction with the Value property to get the data from any single cell in a spreadsheet.

Dim arrPrices (1 to 7)
Dim i As Integer
For i = 1 to 7
   ' Fill the array with seven values from column B of
   ' the worksheet.
   arrPrices(i) = wkbObj.Worksheets(1) _
   .Range("B" & i + 1).Value
Next i

If you're not familiar with Excel spreadsheets, the following figure shows where the values are coming from.

Typical Excel Spreadsheet

The layout of a spreadsheet, and the method used to refer to its cells, also maps conveniently to the method of referring to data points in the MSChart control. For example, a "column" in a spreadsheet (as in column "B" in the preceding illustration) corresponds to a "column" in the MSChart. And when you create a multi-series chart, each column corresponds to a series. Similarly, the "row" of a spreadsheet corresponds to the "row" in a "column" in the MSChart. For this reason, it helps to engineer a chart with a spreadsheet in mind.

For example, just as we first created a multi-series chart by increasing the dimensions of the array, we can now fill the array by using more than one column of the worksheet. In other words, to create a multi-series chart, we fill two (or more) columns of the spreadsheet with numbers, and use the columns to fill an array, as illustrated in the following code:

Dim arrData (1 to 7, 1 to 2)
Dim i As Integer
For i = 1 to 7
   ' Values from column A fill the first series of the
   ' array. If these values are strings, they become
   ' the labels for the rows.
   arrData(i, 1) = wkbObj.Worksheets(1) _
   .Range("A" & i + 1).Value

   ' Then values from column B fill the second.
   arrData(i, 2) = wkbObj.Worksheets(1) _
   .Range("B" & i + 1).Value
Next i

Tip Use the CurrentRegion property to return the number of rows in an Excel column. You can then use this number to specify the upper bound of the array's first dimension.

The preceding code, used in conjunction with the spreadsheet shown in the figure above, will produce a chart that has each row labeled with the appropriate day of the week.


Manipulating the Chart Appearance

The MSChart control has many visible parts, all of which can be programmed. To get a grasp on how this is accomplished, it's useful to examine the following figure pointing out the parts of a chart.

Each of these parts has a corresponding object in the MSChart control which can be used to change the format of practically any element of the chart. For example, the code below dramatically changes the look of a chart by changing the colors of the Plot object.

Private Sub cmdFormat_Click()
   ' First, change the chart type to a 3D chart to
   ' see all the parts of the plot.
   MSChart1.chartType = VtChChartType3dArea

   ' Color the backdrop light blue using
   ' the Plot object.
   With MSChart1.Plot.Backdrop
      ' No color will appear unless you set the style
      ' property to VtFillStyleBrush.
      .Fill.Style = VtFillStyleBrush
      .Fill.Brush.FillColor.Set 100, 255, 200
      ' Add a border.
      .Frame.Style = VtFrameStyleThickInner
      ' Set style to show a shadow.
      .Shadow.Style = VtShadowStyleDrop
   End With

   ' Color the wall of the plot yellow.
   With MSChart1.Plot
      ' Set the style to solid.
      .Wall.Brush.Style = VtBrushStyleSolid
      ' Set the color to yellow.
      .Wall.Brush.FillColor.Set 255, 255, 0
   End With

   With MSChart1.Plot ' Color the plot base blue.
      .PlotBase.BaseHeight = 200
      .PlotBase.Brush.Style = VtBrushStyleSolid
      .PlotBase.Brush.FillColor.Set 0, 0, 255
   End With
End Sub

Formatting Fonts

A very basic task with fonts might be to set the text of the chart's title. In order to do this, use the Title object's Text property:

MSChart1.Title.Text = "Year End Summary"

This is simple enough. The next question is how to change the font's attributes.

In order to format any text attribute on the chart, you must use the VtFont object. For example, to format the title, the following code will work:

With MSChart1.Title.VtFont
   .Name = "Algerian"
   .Style = VtFontStyleBold
   .Effect = VtFontEffectUnderline
   .Size = 14
   .VtColor.Set 255, 0, 255
End With

You can use the same technique with other parts of chart. The only difference lies in the object model. For example, to format the text attributes of the Legend area use the following code:

MSChart1.Legend.VtFont.Size = 18

Change the Scale Using the Type Property

To change the scale of the plot, you must specify that the y axis of the chart is going to be changed (changing the x axis has no visible effect). A convenient way to change the scale is to use a ComboBox control, as shown in the following code:

Private Sub Form_Load()
   ' Configure a ComboBox control named cmbScale.
   With cmbScale
      .AddItem "Log"
      .AddItem "Percent"
      .AddItem "Linear"
      .ListIndex = 0
   End With
End Sub

Private Sub cmbScale_Click()
   ' The ComboBox has three items: Log, Percent,
   ' and Linear (the default scale).

   Select Case cmbScale.Text
   Case "Log"
      MSChart1.Plot.Axis(VtChAxisIdY) _
      .AxisScale.Type = VtChScaleTypeLogarithmic

      ' You must specify a LogBase to be used when
      ' switching the scale to Log. The base can be
      ' set to any value between 2 and 200.
      MSChart1.Plot.Axis(VtChAxisIdY).AxisScale _
      .LogBase = 10

   Case "Percent"
      MSChart1.Plot.Axis(VtChAxisIdY).AxisScale _
      .Type = VtChScaleTypePercent
      ' Set the PercentBasis to one of six types. For
      ' the sake of expediency, only one is shown.
      MSChart1.Plot.Axis(VtChAxisIdY).AxisScale _
      .PercentBasis = VtChPercentAxisBasisMaxChart

   Case "Linear"
      MSChart1.Plot.Axis(VtChAxisIdY).AxisScale _
      .Type = VtChScaleTypeLinear
   End Select
End Sub

Change the Color of MSChart Objects Using the CommonDialog Control

You may wish to allow the user to pick the colors assigned to chart elements (such as the color of series) using the CommonDialog control. In that case, you can use the following functions that mask the bytes returned by the CommonDialog control's Color property to return individual red, green, and blue values required by the Set method.

' Paste these functions into the Declarations section
' of the Form or Code Module.
Public Function RedFromRGB(ByVal rgb As Long) _
   As Integer
   ' The ampersand after &HFF coerces the number as a
   ' long, preventing Visual Basic from evaluating the
   ' number as a negative value. The logical And is
   ' used to return bit values.
   RedFromRGB = &HFF& And rgb
End Function

Public Function GreenFromRGB(ByVal rgb As Long) _
   As Integer
   ' The result of the And operation is divided by
   ' 256, to return the value of the middle bytes.
   ' Note the use of the Integer divisor.
   GreenFromRGB = (&HFF00& And rgb) \ 256
End Function

Public Function BlueFromRGB(ByVal rgb As Long) _
   As Integer
   ' This function works like the GreenFromRGB above,
   ' except you don't need the ampersand. The
   ' number is already a long. The result divided by
   ' 65536 to obtain the highest bytes.
   BlueFromRGB = (&HFF0000 And rgb) \ 65536
End Function

Using the functions in the preceding examples, you can take the Long value returned by the CommonDialog object, and set the color of MSChart objects. The example below allows the user to change the color of a Series by double-clicking it:

Private Sub MSChart1_SeriesActivated(Series As _
   Integer, MouseFlags As Integer, Cancel As Integer)
   ' The CommonDialog control is named dlgChart.
   Dim red, green, blue As Integer
   With dlgChart ' CommonDialog object
      .ShowColor
      red = RedFromRGB(.Color)
      green = GreenFromRGB(.Color)
      blue = BlueFromRGB(.Color)
   End With

   ' NOTE: Only the 2D and 3D line charts use the
   ' Pen object. All other types use the Brush.

   If MSChart1.chartType <> VtChChartType2dLine Or _
   MSChart1.chartType <> VtChChartType3dLine Then
      MSChart1.Plot.SeriesCollection(Series). _
         DataPoints(-1).Brush.FillColor. _
         Set red, green, blue
   Else
      MSChart1.Plot.SeriesCollection(Series).Pen. _
         VtColor.Set red, green, blue
   End If
End Sub


Three-Dimensional Features of the MSChart Control

You can use the MSChart control's three-dimensional chart features to lend a certain sparkle to a report.

Rotate the Chart

You can manually rotate a 3D chart by using the CTRL key and the mouse. To do this, hold down the CTRL key, click on the chart, then hold down the mouse as you drag across the chart image. The following figures show the same chart, before and after rotation.

Before Rotation

After Rotation

Rotating the Chart Programmatically

You can also rotate a 3D chart by using the Set method of the View3D object.

MSChart1.Plot.View3D.Set 10,100

Seeing the Light

In addition to rotating a 3D chart, you can also use the Light feature to control how a virtual light shines on the chart. In practice, the appearance of a chart changes as it rotates because its surfaces reflect steady light in continually changing angles.

By default, the chart comes with one LightSource. The following code will set the four parameters that affect the LightSource. These will be explained later. In the meantime, paste the code into a standard EXE project, and run it.

Private Sub Form_Load()
   With MSChart1
      .chartType = VtChChartType3dArea
      .Plot.Light.LightSources(1).Set 10, 10, 10, 1
   End With
End Sub

This code results in a chart that appears to have a light source shining on it from the lower left of the screen. As the chart is rotated, surfaces which face the light directly get brighter.

Light Basics

The LightSource feature is comprised of two parts:

  • Ambient light: As its name suggests, this is light that has no specific source and thus no directionality.

  • LightSources: a directional light that can be shone on the chart from any angle, with variable intensity. More than one light source can be created.

Ambient Light

Ambient light has a noticeable effect when you are using the LightSources. Ambient light simply bathes the chart evenly in a light that comes from no particular direction. You can set the AmbientIntensity property to any value between 0 and 1, with 0 turning off the light, and 1 increasing it to its maximum. For instance, to turn the ambient on to one quarter of its brightness, set the AmbientIntensity property to .25.

Tip As you experiment with the LightSources, it is useful to set the property to 0. With no ambient light, the effect of the LightSource is accentuated.

The EdgeVisible and EdgeIntensity properties work together to highlight the edges of the chart. When the AmbientIntensity is set to a low value, you can set the EdgeIntensity property to a high value. The result is that the edges seem to glow, as shown in the following figure:

LightSources

Each LightSource in the LightSources collection has an Intensity property which — like the AmbientIntensity property — you can set to a value between 0 and 1. This property sets the brightness of the individual LightSource.

Each LightSource also can be positioned in the virtual space that surrounds the chart by setting the X, Y, and Z properties. By varying these properties, you can specify the direction from which the light will shine on the chart.

Using the Add Method to Add a LightSource

By default, the MSChart contains one LightSource member in the LightSources collection. You can, however, add LightSource members using the Add method, as shown below:

MSChart1.Plot.Light.LightSources.Add 10, 10, 10, 1

The parameters for the Add method include values for X, Y, and Z, which allow you to specify exactly the angle from which the light will shine, and a value for Intensity of the light.


Using the Multimedia MCI Control

Using the Multimedia Control

The Multimedia control allows you to manage Media Control Interface (MCI) devices. These devices include: sound boards, MIDI sequencers, CD-ROM drives, audio players, videodisc players, and videotape recorders and players.

This Multimedia control contains a set of push buttons that issue MCI commands which resemble the commands (functions) you would expect to see on a typical compact disc player or videotape recorder.

The Multimedia control

The buttons are defined, from left to right, as Prev, Next, Play, Pause, Back, Step, Stop, Record, and Eject.

Possible Use

  • To manage the recording and playback of MCI devices.

Multimedia Requirements and Supported Device Types

Which buttons you use, and the functions provided to you by the Multimedia control, depend on the hardware and software configurations of a particular machine. For instance, if your application uses specific multimedia devices and drivers, they must be installed on the user's machine.

Driver support for many multimedia devices (audio and video files, for instance) is provided for in the Windows 95 and Windows NT operating systems. Other devices, such as digital audio tape players or image scanners require separate drivers, are usually provided by the manufacturer.

Devices are considered to be of two types: simple and compound. Simple multimedia devices do not require a data file for playback. For example, when Videodisc and CD audio players are opened, you play, rewind, or forward through 'tracks'. Compound devices, however, require a data file for playback.

The following table lists some of the devices supported by the Multimedia control and the string required by the DeviceType property to use the device. Those listing an accompanying file type are compound devices.

Device type | String | File Type | Description
CD audio | CDAudio |  | CD audio player
Digital Audio Tape | DAT |  | Digital audio tape player
Digital video | DigitalVideo |  | Digital video in a window (not GDI-based)
Other | Other |  | Undefined MCI device
Overlay | Overlay |  | Overlay device
Scanner | Scanner |  | Image scanner
Sequencer | Sequencer | .mid | Musical Instrument Digital Interface (MIDI) sequencer
Vcr | VCR |  | Video cassette recorder or player
AVI | AVIVideo | .avi | Audio Visual Interleaved video.
Videodisc | Videodisc |  | Videodisc player
Wave audio | Waveaudio | .wav | Wave device that plays digitized waveform files.

MCI Commands

The Multimedia control uses a set of high-level, device-independent commands, known as Media Control Interface commands, that control various multimedia devices. Many of these commands correspond directly to a button on the Multimedia control. For instance, the Play command corresponds to the Play button.

The Multimedia control is essentially a Visual Basic interface to this command set. Commands like Play or Close have equivalents in the MCI command structure of the Win32® API. For instance, Play corresponds to MCI_PLAY. The following table lists the MCI commands used by the Multimedia control, along with their Win32 equivalents:

Command | MCI Command | Description
Open | MCI_OPEN | Opens a MCI device.
Close | MCI_CLOSE | Closes a MCI device.
Play | MCI_PLAY | Plays a MCI device.
Pause | MCI_PAUSE or MCI_RESUME | Pauses playing or recording.
Stop | MCI_STOP | Stops a MCI device.
Back | MCI_STEP | Steps backward through available tracks.
Step | MCI_STEP | Steps forward through available tracks.
Prev | MCI_SEEK | Goes to the beginning of the current track using the Seek command. If executed within three seconds of the previous Prev command, goes to the beginning of the previous track or to the beginning of the first track if at the first track.
Next | MCI_SEEK | Goes to the beginning of the next track (if at last track, goes to the beginning of the last track) using the Seek command.
Seek | MCI_SEEK | Seeks track forward or backward.
Record | MCI_RECORD | Records MCI device input.
Eject | MCI_SET | Ejects Audio CD from CD drive.
Save | MCI_SAVE | Saves an open file.

In Visual Basic, these commands are initiated using the Multimedia control's Command property. For example:

MMControl1.Command = "Open"

While the Multimedia control's implementation of the MCI command set is sufficient for most uses, directly utilizing the Win32 API can provide advanced programming functions and techniques.

For More Information For additional information on the MCI commands see the Microsoft Multimedia Development Kit Programmer's Workbook or the Microsoft Windows Software Development Kit Multimedia Programmer's Reference. These references, and a wealth of other information on the MCI commands, are available on the Microsoft Developer Network CD.

Programming the Multimedia Control

The Multimedia Control can be either visible or invisible at run time by setting the Enabled and Visible properties. By default, the Enabled and Visible properties are set to True and the control is visible at run time.

If you do not want the user to interact directly with the buttons on the Multimedia control but want to use the control for its multimedia functionality, set the Visible property to False. An application can control MCI devices with or without user interaction.

To enable or make individual buttons visible or invisible, you set each button's visible and enabled properties. For example, the Back button contains the properties BackEnabled and BackVisible. Each of the nine push buttons have corresponding properties.

In most cases, the default functionality of the individual buttons is sufficient to manage MCI devices. However, the Multimedia control contains run-time properties which allow you to augment or redefine the button commands.

The Notify, NotifyMessage, and NotifyValue properties provide valuable feedback on the failure or completion of a command.

Opening the MCI Device

After you place the Multimedia control on a form, whether it is set to be visible or not, the first step is accessing the MCI device. To do this, you set a number of run-time properties. For example:

'Set initial property values of the media device
MMControl1.Notify = False
MMControl1.Wait = True
MMControl1.Shareable = False
MMControl1.DeviceType = "CDAudio"

The Notify property, if set to True, generates a Done event when next command is completed. The Done event provides useful feedback indicating the success or failure of the command. The Wait property determines whether or not the Multimedia control waits for the next command to complete before returning control to the application. The Shareable property either restricts or allows use of the media device by other applications or processes. The DeviceType property is used to specify the type of MCI device.

Finally, the Open command is used to open the MCI device.

'Open the media device
MMControl1.Command = "Open"

When the control is visible, setting these properties and issuing the Open command enables the push buttons of the Multimedia control that are inherently supported by the MCI device. For instance, opening the cdaudio device enables the Prev, Next, Play, and Eject buttons. When Play is pressed, the Stop and Pause buttons are enabled.

Multiple instances of the Multimedia control can be added to a form to provide concurrent control of several MCI devices. You use one control per device.

Managing Multimedia Resources

To properly manage multimedia and system resources, you should close those MCI devices that are open before exiting your application. You can place the following statement in the Form_Unload procedure to close an open MCI device when the form containing the Multimedia control is unloaded.

Private Sub Form_Unload (Cancel as Integer)
   Form1.MMControl1.Command = "Close"
End Sub

Using the Error and ErrorMessage Properties

You can handle errors encountered by the Multimedia control using the Error and ErrorMessage properties. You can test for an error condition after each command. For example, following the Open command, you check the value of the Error property to test for the existence of a CD drive. If the CD drive is not available, an error message is returned. For example:

If Form1.MMControl1.Error Then
   MsgBox Form1.MMControl1.ErrorMessage,vbCritical, "CD Player not installed or not working properly"
End If

For More Information See "ErrorMessage Property (Multimedia MCI Control)" and "Error Property (Multimedia MCI Control)".


Multimedia Control Scenario: Creating a Simple Audio CD Player

Using a few of the techniques discussed in the section, "Using the Multimedia Control," you can create a simple audio CD player application that illustrates features of the Multimedia control. This scenario uses one form and one instance of the Multimedia control and assumes that both an audio CD-compatible CD-ROM drive and a sound card are installed in the computer.

To begin, start a new project in Visual Basic. Add an instance of the Multimedia control to the form. In the form's Form_Load event procedure add the following code:

'Set initial property values of the media device
Form1.MMControl1.Notify = False
Form1.MMControl1.Wait = True
Form1.MMControl1.Shareable = False
Form1.MMControl1.DeviceType = "CDAudio"

 'Open the media device
Form1.MMControl1.Command = "Open"

Opening the MCI device in the Form_Load procedure initializes the device at run time. If an audio CD is loaded into the CD drive, the appropriate buttons on the Multimedia control are enabled.

A simple audio CD player

The Multimedia control recognizes what actions are relevant for the current state of a device. Therefore, the appropriate buttons are automatically enabled when supported MCI devices are opened with the Multimedia control. In this example, when the Play button is pressed, the Stop and Pause buttons are enabled. If the CD is removed from the CD drive while the application is running, all the buttons become disabled.

Sample Application: MCITest.vbp

The MCITest.vbp sample application, which is listed in the Samples directory, builds on this scenario and provides you with advanced programming techniques using the Multimedia control.


Using the PictureClip Control

The PictureClip control stores multiple images that can be used by other Visual Basic controls. All images are contained in a single bitmap. Selected regions can then be 'clipped' from the bitmap and used with a PictureBox control to create animations, or with multiple PictureBox controls to create a toolbox, for instance.

The PictureClip control can be used with any control that assigns a Picture object to a Picture property, such as the PictureBox, Image, and CommandButton controls.

Storing multiple images in a single PictureClip control conserves Windows resources and speeds up retrieval of the images. The PictureClip control is similar to the ImageList control in this way; however, they differ in that all image resources in the PictureClip control must be contained in a single bitmap whereas the ImageList control is a collection of separate bitmaps.

Possible Uses

  • To create an image resource bitmap which stores all the images needed for an animation or a toolbox.

Creating a PictureClip Image

The first step in using a PictureClip control is creating the image resource bitmap. The PictureClip control only supports 16-color bitmap (.bmp) images. When creating a set of images, collect all the separate images and cut and paste them into a single bitmap, as shown below.

A PictureClip control containing a resource bitmap

Note You need to make sure that each image is the same size as the others so that when individual images are retrieved into a PictureBox control, for example, they fill the space uniformly. In the example above, each image of the spinning top is a square of equal size.

Loading the Resource Bitmap into PictureClip

After creating an image resource bitmap, load it into the PictureClip control. The PictureClip control has a Property Pages dialog box which allows the bitmap to be loaded into the control and the grid set by specifying the number of columns and rows.

To load a resource bitmap into the PictureClip control at design time

  1. Start a new project in Visual Basic.

  2. Add a PictureClip control to the form.

  3. From the Properties window of the PictureClip control select the Custom option or right-click on the control and select Properties. The Property Pages dialog box will open.

  4. Select the Picture tab and then click the Browse button to locate the bitmap. The selected bitmap is displayed in the Preview window.

Loading a resource bitmap into the PictureClip control

Loading the Resource Bitmap at Run Time

The resource image bitmap can also be loaded into the PictureClip control at run time using the Picture property, as in the following example:

PictureClip1.Picture = LoadPicture("c:\Program _
   Files\Microsoft Visual _
   Basic\Samples\PicClip\Redtop.bmp")
' If you have installed the Visual Basic sample applications, the PicClip
' project can be found in the \samples\VB98\ directory

Retrieving Images from the PictureClip Control

Once you've created and loaded an image resource bitmap into the PictureClip control, you can determine how you want to retrieve each image.

You can randomly select any portion of the image resource bitmap as the clipping region using the ClipX and ClipY properties to determine the upper-left corner of the clipping region and the ClipHeight and ClipWidth properties to define the area of the clipping region. The Clip property then contains the clipped region.

You can divide the image resource bitmap into a specified number of rows and columns. The rows and columns create cells which can then be accessed using an index number. The cells are indexed beginning at 0 and increase from left to right and top to bottom using the GraphicCell property.

Setting Rows and Columns at Design Time

Using the Property Pages dialog box, you can set the number of rows and columns at design time. In the example above, there are 18 red top images divided into 3 rows and 6 columns.

To set the number of rows and columns at design time

  1. From the Properties window of the PictureClip control select the Custom option or right-click on the control and select Properties. The Property Pages dialog box will open.

  2. Select the General Properties tab.

  3. Select the number of rows and columns that correspond to the images in the resource bitmap. Click OK.

Setting the number of rows and columns

Selecting Cells at Run Time

You use the GraphicCell property to specify which cell in the image resource bitmap of the PictureClip control you want to load into a PictureBox control. The following example loads a single image (one cell) into a PictureBox control in the Form_Load event procedure:

Private Sub Form_Load()
   PictureClip1.Picture = LoadPicture("c:\Program _
      Files\Microsoft Visual _
      Basic\Samples\PicClip\Redtop.bmp")
   PictureClip1.Cols = 6
   PictureClip1.Rows = 3
   Picture1.ScaleMode = vbPixels
   Picture1.Picture = PictureClip1.GraphicCell(2)
End Sub

Note In Visual Basic, the default ScaleMode for forms and picture boxes is twips. Set ScaleMode to vbPixels for all controls that display pictures from a PictureClip control.

If you previously defined the numbers of rows and columns using the PictureClip Property Pages, you can simply load the image into the PictureBox control with the GraphicCell property. If not, you must specify the number of rows and columns by using the Cols and Rows properties. To use the GraphicCell property you must define at least one column and row.

Sample Application: Redtop.vbp

The Redtop.vbp sample application, which is listed in the Samples directory, builds on some of the examples shown above and demonstrates how to create a simple animation using the PictureClip control.


Using the ProgressBar Control

A ProgressBar control allows you to graphically represent the progress of a transaction. The control consists of a frame that is filled as the transaction occurs. The Value property determines how much of the control has been filled. The Min and Max properties set the limits of the control.

Use the ProgressBar whenever an operation will take more than a few seconds to complete. You must also know how long the process will take by using a known end point and setting it as the Max property of the control.

Tip Giving the user visual feedback on how much time remains on a lengthy operation gives the perception of improved performance.

Possible Uses

  • To inform the user of progress as a file transfer occurs over a network.

  • To reflect the state of a process that takes more than a few seconds.

  • To inform the user of progress as a complex algorithm is being calculated.

Use the Value Property With the Min and Max Properties to Show Progress

To show progress in an operation, the Value property is continually incremented until a maximum — defined by the Max property — is reached. Thus the number of chunks displayed by the controls is always a ratio of the Value property to the Min and Max properties. For example, if the Min property is set to 1, and the Max property is set to 100, a Value property of 50 will cause the control to display 50 percent of the chunks, as shown:

Set the Max Property to a Known Limit

To program the ProgressBar, you must have a limit to which the Value property will climb. For example, if you are performing downloading a file, and your application can determine how large the files is in kilobytes, you can set the Max property to that number. As the file is downloaded, your application must also have some way of determining how many kilobytes have been downloaded; set the Value property to that number.

In cases where a Max property can't be determined in advance, you may want to use the Animation control to continuously show an animation until a Stop method is invoked in a terminate event.

For More Information For information on using the Animation control, see "Using the Animation Control."

Hide the ProgressBar with the Visible Property

A progress bar usually doesn't appear until an operation has begun, and disappears again after the operation ends. Set the Visible property to True to show the control at the start of an operation, and reset the property to False to hide the control when the operation finishes.


ProgressBar Scenario 1: Inform User of TreeView Population Status

If you are using a TreeView control to view a database, populating the tree may take some time. In that instance, you can use the ProgressBar control to inform the user of the status of the operation.

In this scenario, the Biblio.mdb database is loaded into the TreeView control using a Do Until loop. Before the loop begins the ProgressBar control is unhidden. Through each iteration of the loop, the Value property of the ProgressBar control is updated with the PercentPosition property of the Recordset object. When the loop finishes, the ProgressBar control is hidden again.

Sample Application: DataTree.vbp

The code examples in this topic are taken from the DataTree.vbp sample application, which is listed in the Samples directory.

Objects Used

The following example uses the following objects:

  • Form named "frmTreeView"

  • ProgressBar Control named "prgLoad"

  • TreeView control named "tvwDB"

  • CommandButton control named "cmdLoad"

To show a ProgressBar while a TreeView control is being populated

  1. In the Form object's Load event. hide the ProgressBar and set its Max property to 100.
  2. In the TreeView population code, populate the tree with a Do Until statement.

  3. When the loop ends, hide the ProgressBar.

In the Form Object's Load Event, Hide the ProgressBar and Set the Max Property to 100

A progress bar is most effective when it is visible only for the duration of the time-intensive process. To this end, you can use the Form object's Load event to set the Visible property of the control to False.

In this scenario, the Recordset object's PercentPosition property will be used. Since this property returns a number representing a percentage between 0 and 100, the Max property should be set to 100 when the form is initialized. (By default, the Min property is set to 0.) The following code hides the ProgressBar control and sets its Max property to 100.

Private Sub Form_Load()
   prgLoad.Visible = False
   prgLoad.Max = 100
End Sub

Populate the Tree with a Do Until Statement

Note The following code builds upon the code found in "TreeView Scenario1: Binding the TreeView to the Biblio.MDB Database." The working code can also be found in the DataTree.vbp sample application.

To populate a TreeView control from a database, you can use a Do Until loop. Schematically, the code might look like this:

' Presuming a Recordset object variable named
' "rsTitles" has been set to a valid database
' table.
Do Until rsTitles.EOF
   ' Using the current record, create a Node
   ' object.
   Set mNode = TreeView.Nodes.Add()
   ' Set properties of the Node.
   mNode.Text = rsTitles!Fields(1).Value
   ' Move to the next record.
   rsTitles.MoveNext
Loop

To update the ProgressBar control, use the PercentPosition property of the Recordset object. This property returns the position of the current recordset as a percentage of the total number of records. The code to update the ProgressBar should then be placed somewhere within the loop, as shown:

Do Until rsTitles.EOF
   ' Update the ProgressBar control.
   prgLoad.Value = rsTitles.PercentPosition

   Set mNode = TreeView.Nodes.Add()
   ' Set properties of the Node.
   mNode.Text = rsTitles!Fields(1).Value
   ' Move to the next record.
   rsTitles.MoveNext
Loop

When the Loop Ends, Hide the ProgressBar

Once the TreeView control has been entirely loaded with Node objects, you can hide the ProgressBar control, as shown:

Private Sub cmdLoad_Click()
   ' Show the ProgressBar control.
   prgLoad.Visible = True

Do Until rsTitles.EOF
      ' Update the ProgressBar control.
      prgLoad.Value = rsTitles.PercentPosition

      Set mNode = TreeView.Nodes.Add()
      ' Set properties of the Node.
      mNode.Text = rsTitles!Fields(1).Value
      ' Move to the next record.
      rsTitles.MoveNext
   Loop
   ' Hide the ProgressBar control.
   prgLoad.Visible = False
End Sub


ProgressBar Control Scenario 2: Using the ProgressBar to Show a TimeOut Interval

Applications that create processes over networks often have a "TimeOut" interval. This is a predetermined period of time after which the user will be presented with the choice of canceling a process, or continuing to wait. One way of graphically representing the TimeOut interval is with the ProgressBar control.

The following example uses the following objects:

  • Form Object named "frmTimer"

  • ProgressBar control named "prgBar1"

  • Timer control named "tmrTimer"

  • CommandButton control named "cmdBegin"

To create a progress bar that reflects a TimeOut interval

  1. In the Form's Load event, set a Timer control's Interval property to 1000.

  2. Set the ProgressBar control's Max property to the TimeOut Interval.

  3. Begin the Timer with the Enabled property.

  4. In the Timer control's Timer event, declare a Static variable to count the number of intervals.

  5. Set the ProgressBar's value to the variable.

  6. Test to see if the ProgressBar's value is the Max property.

In the Form's Load event, Set the Timer Control's Interval Property to 1000

In the Form object's Load event, configure the Timer control's Interval property. Because it's more useful to time a process in seconds, set the Interval to 1000 (milliseconds, or 1 second). Thus, at one second intervals, the ProgressBar control's Value property is updated.

tmrTimer.Interval = 1000

In the Form Load event, Set the ProgressBar Control's Max Property to the TimeOut Interval

The Load event is also where you set the Max property of the ProgressBar. The value of the Max property should be the number of seconds you want the Timer to continue before being disabled. However, to accurately reflect the number of seconds that must elapse, the ProgressBar's Min property should be set to 1.

The Load event can also be used to hide the ProgressBar by setting its Visible property to False. The following code shows the entire Load event with the previous code included.

Private Sub Form_Load()
   prgBar1.Visible = False
   tmrTimer.Interval = 1000
   prgBar1.Max = 10 ' Timer will go for 10 seconds.
End Sub

Begin the Timer with the Enabled Property

To start the timer, you must use the Enabled property. When you begin to time any process, you should also show the ProgressBar, as shown:

Private Sub cmdBegin_Click()
   prgBar1.Visible = True
   tmrTimer.Enabled = True
End Sub

In the Timer Event, Declare a Static Variable and Set it to 1

In the Timer event, declare a static variable. This allows you to efficiently increment the variable every time the Timer event occurs. But as we don't wish to count from 0, we must also set the variable to 1, using the IsEmpty function, as shown:

Static intTime
If IsEmpty(intTime) Then intTime = 1

Set the ProgressBar's Value to the Variable

Each time the Timer event occurs, the ProgressBar's Value property must be set to the value of the static variable:

prgBar1.Value = intTime

Test to See If the ProgressBar's Value is the Max Property.

After the ProgressBar's Value property has been updated, the variable must be tested to see if the TimeOut limit has occurred. If it has been reached, the variable must be reset to 1, the ProgressBar control hidden and its Value property reset to 1, and the Timer control disabled. If the limit hasn't been reached, then the variable is incremented by one. These steps are all implemented with an If statement, in the Timer event, as shown:

Private Sub tmrTimer_Timer()
   Static intTime ' Declare the static variable.
   ' The first time, the variable will be empty.
   ' Set it to 1 if it is an empty variable.
   If IsEmpty(intTime) Then intTime = 1

   prgBar1.Value = intTime ' Update the ProgressBar.

   If intTime = prgBar1.Max Then
      Timer1.Enabled = False
      prgBar1.Visible = False
      intTime = 1
      prgBar1.Value = prgBar1.Min
   Else
      intTime = intTime + 1
   End If
End Sub

The Complete Code

Here is the complete code for the example described in this topic:

Private Sub Form_Load()
   prgBar1.Visible = False
   tmrTimer.Interval = 1000
   prgBar1.Max = 10 ' Timer will go for 10 seconds.
End Sub

Private Sub cmdBegin_Click()
   prgBar1.Visible = True
   tmrTimer.Enabled = True
End Sub

Private Sub tmrTimer_Timer()
   Static intTime ' Declare the static variable.
   ' The first time, the variable will be empty.
   ' Set it to 1 if it is an empty variable.
   If IsEmpty(intTime) Then intTime = 1

   prgBar1.Value = intTime ' Update the ProgressBar.

   If intTime = prgBar1.Max Then
      Timer1.Enabled = False
      prgBar1.Visible = False
      intTime = 1
      prgBar1.Value = prgBar1.Min
   Else
      intTime = intTime + 1
   End If
End Sub


Using the RichTextBox Control

The RichTextBox control allows the user to enter and edit text while also providing more advanced formatting features than the conventional TextBox control.

The RichTextBox control provides a number of properties you can use to apply formatting to any portion of text within the control. Using these properties, you can make text bold or italic, change its color, and create superscripts and subscripts. You can also adjust paragraph formatting by setting both left and right indents, as well as hanging indents.

Possible Uses

  • As a "bottomless" text box, to allow an application to read extremely large text files.

  • To implement a full-featured text editor into any application.

Features

  • Open and save files in both the RTF format and ASCII text format. You can use methods of the control (LoadFile and SaveFile) to directly read and write files, or use properties of the control, such as SelRTF and TextRTF, in conjunction with Visual Basic's file input/output statements.

  • Load the contents of an .rtf file into the RichTextBox control simply by dragging the file (from the Windows 95 Explorer for example), or a highlighted portion of a file used in another application (such as Microsoft Word), and dropping the contents directly onto the control.

  • Set the FileName property to load the contents of an .rtf or .txt file into the control.

  • Print all or part of the text in a RichTextBox control using the SelPrint method.

  • Bind the RichTextBox control with a Data control to a Memo field in a Microsoft Access database or a similar large capacity text field in other databases (such as a TEXT data type field in SQL Server).

  • Programmatically add embedded objects such as bitmaps, icons, application icons, into the control at run time using the OLEObject collection's Add method. At both design time and run time, drag and drop any embedded object, including documents such as Microsoft Excel spreadsheets and Microsoft Word documents, into the control.

Set Scrollbars at Design Time

By default, the RichTextBox doesn't include scrollbars. At run time, if a large file is loaded into the control, the end user will not be able to see all that is in the file. To allow the user to scroll easily, set the ScrollBars property to 1 (horizontal), 2 (vertical), or 3 (both). This must be done at design time, because the ScrollBars property is read-only at run time.

Open and Save Files with the LoadFile and SaveFile Methods

You can easily open or save an RTF file with the RichTextBox control by using the LoadFile and SaveFile methods. To open a file, use a CommonDialog control to supply the path name as shown:

Private Sub OpenFile()
   ' The RichTextBox control is named "rtfData."
   ' The CommonDialog is named "dlgOpenFile."
   ' Declare a String variable for file name.
   ' Show the Open File dialog box, and set the
   ' variable to the filename.
   Dim strOpen As String
   dlgOpenFile.ShowOpen
   strOpen = dlgOpenFile.FileName
   ' Use LoadFile method to open the file.
   rtfData.LoadFile strOpen
End Sub

To save a file is just as easy, using the SaveFile method:

Private Sub SaveFile()
   Dim strNewFile As String
   dlgOpenFile.ShowSave
   strNewFile = dlgOpenFile.FileName
   rtfData.SaveFile strNewFile
End Sub

Note If a file contains RTF codes that aren't supported by the control, the affected text only will not appear in the RichTextBox control.

Use the SelFontName, SelFontSize, and SelFontColor to Set Font Attributes

To change the font attributes of text in the RichTextBox control, use the SelFontName, SelFontSize, and SelFontColor properties.

The ComboBox control is often used to display a range of choices for these properties. Subsequently, the ComboBox control's Click event can be used to change the property. The following code first populates a ComboBox control named "cmbFonts" in the Form object's Load event. The Click event is then used to change the SelFontName property of a RichTextBox control:

Private Sub Form_Load()
   Dim i As Integer
   With cmbFonts
      For i = 0 to Screen.Fonts.Count - 1
      .AddItem Screen.Fonts(i).Text
   End With
End Sub

Private Sub cmbFonts_Click()
   rtfData.SelFontName = cmbFonts.Text
End Sub

Important These properties only affect selected text, or, if no text is selected, the text that is typed after the current cursor location.

Format Indents, Hanging Indents, and Bulleted Paragraphs

Another feature of the RichTextBox control is its ability to create paragraphs with indents, hanging indents, and bulleted paragraphs. The three styles are shown here:

Indents and bulleted indents (rtf_3ind.bmp)

As with the SelFontName, SelFontSize, and SelFontColor properties, the end user must select a paragraph or range of paragraphs before applying these attributes. Thus, assuming the user had selected the second paragraph only, the code to create an indent and hanging indent would be as follows:

' Assuming the control is named "rtfData."
rtfData.SelIndent = .5
rtfdata.SelHangingIndent = 1.5

Note that the number used to set the SelIndent, SelHangingIndent, BulletIndent and other properties depends upon the ScaleMode property of the container of the RichTextBox control. For example, if the RichTextBox's container is a Form object, and you change the Form object's ScaleMode property from 7 (centimeters) to 1 (twips), the SelIndent property must change from .5 (centimeters) to 283 (twips). This is because one centimeter = 567 twips.

Also note that the SelBullet property of a paragraph must be set to True for a paragraph to have the bullet style.

Use the SelChange Event for Notification of Attribute Changes

To notify the user of the current attributes of any selected text, use the SelChange event, which occurs whenever the insertion point moves, or the selection has changed. The following example uses a Toolbar control to notify the user of changes in the SelBold property.

Private Sub rtfData_SelChange()
   ' Reset the Value property of a Toolbar
   ' Button object. The Toolbar control is
   ' named "tlbRTF."

   ' SelBold returns 0, -1, or Null. If it's Null
   ' then set the MixedState property to True.

   Select Case rtfData.SelBold
   Case 0 ' Not bold.
      tlbRTF.Buttons("bold").Value = tbrUnpressed
   Case -1 ' Bold.
      tlbRTF.Buttons("bold").Value = tbrPressed
   Case Else ' Mixed state.
      tlbRTF.Buttons("bold").MixedState = True
   End Select
End Sub

Use the SelPrint Method to Print the RichTextBox Contents

To print from the RichTextBox control, use the SelPrint method. This method requires one argument, the hDC property of the Printer object that will print the RichTextBox control's contents. If any text is selected by the user, only the selected text will be printed. If no text is selected, the entire contents of the control will be printed.

The following code uses the CommonDialog control to display a Printer dialog box when the user clicks a CommandButton control. The Flags property disables page numbers, and allows the user to choose various options on the dialog box:

Private Sub cmdPrint_Click()
   ' The CommonDialog control is named "dlgPrint."

   dlgPrint.Flags = cdlPDReturnDC + cdlPDNoPageNums
   If rtfData.SelLength = 0 Then
      dlgPrint.Flags = dlgPrint.Flags + cdlPDAllPages
   Else
      dlgPrint.Flags = dlgPrint.Flags + cdlPDSelection
   End If
   dlgPrint.ShowPrinter
   rtfData.SelPrint dlgPrint.hDC
End Sub

For More Information For more information on using the CommonDialog control, see "Using the CommonDialog Control", in the Programmer's Guide.

Display Embedded Objects in the Control

At run time, the end user can drag and drop any embedded objects into the RichTextBox control. There are two kinds of objects that can be embedded into the control: objects which display as icons, and objects which display as data. As an example of the first type, the following figure shows a RichTextBox with an embedded file object.

When clicked, the embedded object will behave as expected — starting up the application associated with the file. When the data in the control is saved as an RTF file (using the SaveFile method), the information in the embedded object will also be saved.

However, if the end user embeds a bitmap into the control, the bitmap itself will be displayed, not its icon, as shown:

Clicking on the bitmap will start the Paint application, allowing the end user to edit the bitmap.

Add Embedded Objects Using the OLEObjects Collection's Add Method

You can programmatically add embedded objects to the control using the OLEObject collection's Add method, as shown:

Private Sub cmdAddObject_Click()
   ' Add the butterfly bitmap to the control.
   RichTextBox1.OLEObjects.Add , , , "bfly.bmp"
End Sub

Using the Add, Clear, and Remove methods, you can dynamically populate the control with embedded objects at run time.


RichTextBox Scenario 1: Change Fonts, FontSize, and FontColor

RichTextBox Scenario: Change Fonts, FontSize, and FontColor

With the RichTextBox control the end user can change various attributes of text such as font, font size, and font color. In this scenario, three ComboBox controls appear on an application's toolbar to show attributes of text in the RichTextBox control:

Using these ComboBox controls, the user can select a font, font color, and font size to apply to selected text.

The following objects are used in this scenario:

  • Form object named "frmRTF"

  • Toolbar control named "tlbRTF"

  • ComboBox control named "cmbFontColor"

  • ComboBox control named "cmbFontName"

  • ComboBox control named "cmbFontSize"

  • RichTextBox control named "rtfData"

Note The following steps are a general outline of the creation process. Each step is explained in greater detail later in this topic.

To create three combo boxes that display attributes

  1. Set the Form object's ScaleMode to centimeters.

  2. Create a Toolbar control.

  3. Create three ComboBox controls on the Toolbar control.

  4. In the Form object's Load event, populate the ComboBox controls.

  5. In the ComboBox controls' Click event, set the SelFontName, SelFontColor, or SelFontSize property.

  6. In the RichTextBox control's SelChange event, use the SelFontName, SelFontColor, or SelFontSize property to change the appropriate ComboBox control's List property.

Set the Form Object's ScaleMode to Centimeters

The Form object's ScaleMode property determines which measurement unit is used for certain properties. On the RichTextBox control, the SelIndent. SelHangingIndent, and other properties, use the ScaleMode unit. For these properties, it's more convenient to use centimeters rather than twips. In addition, when creating Button objects with the PlaceHolder style, the ScaleMode property determines how the Button object's width is measured.

To set the form object's ScaleMode property to centimeters

  1. Click the Form object.

  2. Press F4 to display the Properties window.

  3. Click the ScaleMode property and select 7 (centimeters).

Create a Toolbar Control

On the Toolbox, double-click the Toolbar control icon. In the Properties window, double-click Name and change the name of the control to tlbRTF.

Create Three ComboBox Controls

Three ComboBox controls will be placed on the Toolbar. Each ComboBox control will be populated with font attributes.

To create three ComboBox controls

  1. On the Toolbox, click the ComboBox control icon.

  2. Draw a ComboBox control on the Toolbar control.

  3. Set the Name property for each of the three ComboBox controls according to the following table:
    ComboBox | Property | Value
    ComboBox1 | Name | cmbFontName
    ComboBox2 | Name | cmbFontSize
    ComboBox3 | Name | cmbFontColor

Populate the ComboBox Controls

The ComboBox controls must also be populated with appropriate values. The following code populates each ComboBox control in the Form_Load event.

Private Sub Form_Load()
   ' Add colors to cmbFontColor.
   With cmbFontColor
   .AddItem "Black"
   .AddItem "Blue"
   .AddItem "Red"
   .AddItem "Green"
   .ListIndex = 0
   End With

   Dim i As Integer
   With cmbFontName
      For i = 0 to Screen.FontCount - 1
         .AddItem Screen.Fonts(i)
      Next i
      ' Set ListIndex to 0.
      .ListIndex = 0
   End With

   With cmbFontSize
      ' Populate the combo with sizes in
      ' increments of 2.
      For i = 8 To 72 Step 2
         .AddItem i
      Next i
      ' Set ListIndex to 0
      .ListIndex = 1 ' size 10.
   End With
End Sub

In the ComboBox Controls' Click event: Set the SelFontName, SelColor, or SelFontSize Property

To set the font, color, and font size for a RichTextBox control, use the SelFontName, SelColor, and SelFontSize properties. For each of the ComboBox controls, set the appropriate property in the Click event. After setting the property, you may want to set the focus back to the RichTextBox control:

Private Sub cmbFontName_Click()
   rtfData.SelFontName = cmbFont
   rtfData.SetFocus
End Sub

Private Sub cmbFontSize_Click()
   rtfData.SelFontSize = cmbFontSize.Text
   rtfData.SetFocus
End Sub

Private Sub cmbFontColor_Click()
   ' Change font colors of text using the
   ' Select Case statement with the ListIndex of the
   ' ComboBox control. Set the colors with
   ' the intrinsic constants for color.
   Me.Show
      With rtfData
   Select Case cmbFontColor.ListIndex
   Case 0
      .SelColor = vbBlack
   Case 1
      .SelColor = vbBlue
   Case 2
      .SelColor = vbRed
   Case 3
      .SelColor = vbGreen
   End Select
   End With
   ' Return focus to the RichTextbox control.
   rtfData.SetFocus
End Sub

In the SelChange Event: Use the SelFontName, SelColor, or SelFontSize property to change the appropriate ComboBox control's List property

The ComboBox control can also be used to notify the user of text attributes as the insertion point is moved in the RichTextBox control. The SelChange event occurs either when the insertion point is moved, or when selected text is changed. Thus, in the SelChange event, check the SelFontName, SelFontColor, and SelFontSize properties and reset the appropriate ComboBox control.

Private Sub rtfData_SelChange()
   ' SelFontSize returns the font size, or Null if
   ' it's mixed.
   If Not IsNull(rtfData.SelFontSize) Then
      cmbFontSize.Text = rtfData.SelFontSize
   End If

   ' Show Font name in the ComboBox.
   cmbFont.Text = rtfData.SelFontName

' Show color of text in the ComboBox. Use the
' intrinsic constants for color to determine
' the color of the text.
   Select Case rtfData.SelColor
      Case vbBlack
         cmbFontColor.ListIndex = 0
      Case vbBlue
         cmbFontColor.ListIndex = 1
      Case vbRed
         cmbFontColor.ListIndex = 2
      Case vbGreen
         cmbFontColor.ListIndex = 3
   End Select
End Sub


Using the Slider Control

A Slider control consists of a scale, defined by the Min and Max properties, and a "thumb," which the end user can manipulate using the mouse or arrow keys. At run time, the Min and Max properties can be dynamically reset to reflect a new range of values. The Value property returns the current position of the thumb. Using events such as the MouseDown and MouseUp events, the Slider control can be used to graphically select a range of values.

Possible Uses

  • To set the value of a point on a graph.

  • To select a range of numbers to be passed into an array.

  • To resize a form, field, or other graphic object.

TickStyle and TickFrequency Properties

The Slider control consists of two parts: the thumb and the ticks, as shown below:

The appearance of the control depends on the TickStyle property. In brief, the ticks can appear along the bottom of the control, as shown above (the default style), along the top, along both top and bottom, or not at all.

In addition to the placement of the ticks, you can also program how many ticks appear on the control by setting the TickFrequency property. This property, in combination with the Min and Max properties, determines how many ticks will appear on the control. For example, if the Min property is set to 0, the Max to 100, and the TickFrequency to 5, there will be one tick for every five increments, for a total of 21. If you reset the Min and Max properties at run time, the number of ticks can be determined by using the GetNumTicks method, which returns the number of ticks on the control.

Set the Min, Max Properties at Design Time or Run Time

The Min and Max properties determine the upper and lower limits of a Slider control, and you can set these properties at either design time or run time. At design time, right-click on the control and click Properties to display the Property Pages dialog box, as shown below:

At run time, you can reset the Min and Max settings to accommodate different ranges. For example, if you are using the Slider to change values in a database, you can use the same control and bind it to different fields depending on what table the user is editing.

SmallChange and LargeChange Properties

The SmallChange and LargeChange properties determine how the Slider control will increment or decrement when the user clicks it. The SmallChange property specifies how many ticks the thumb will move when the user presses the left or right arrow keys. The LargeChange property specifies how many ticks the thumb will move when the user clicks the control or when the user presses the PAGEUP or PAGEDOWN keys.

Selecting Ranges

If the SelectRange property is set to True, the Slider control changes its appearance, as shown below:

To select a range of values, you must use the SelStart and SelLength properties. For a detailed example of this, see "Slider Scenario 2: Select a Range of Values with the Slider" in this chapter.


Slider Scenario 1: Resize a Graph Control Proportionally

One possible use of the Slider is to resize a PictureBox control on a form while keeping its proportions.

The code below uses the following objects:

  • Form named "frmPictureBox"

  • Slider control named "sldResize"

  • PictureBox control named "picPhoto"

To resize a PictureBox control with a Slider control

  1. Create two global variables for the Height and Width properties.

  2. Use the Form’s Load event to set global variables and the Max property.

  3. Resize the height and width of the PictureBox through the Scroll event.

Create Two Global Variables for Height and Width

One simple formula for retaining proportions would be:

picPhoto.Height = sldResize.Value * _
OriginalHeight / 100
picPhoto.Width = sldResize.Value * OriginalWidth / 100

This formula depends on two constant values: the original height and width of the PictureBox control. These values should be set when the form is loaded, and should be available as global variables, as shown below:

Option Explicit
Private gHeight As Long
Private gWidth As Long

Use the Form Load Event to Set Global Values and the Max Property

To set the values for the global variables, use the Form object's Load event. It's also more efficient to calculate the values of OriginalHeight/100 and OriginalWidth/100 once, and store those values in global variables, as shown below:

gHeight = picPhoto.Height / 100
gWidth = picPhoto.Width / 100

The Load event can also be used to set the Max property of the Slider control. The Max property specifies the maximum value the Slider will accommodate. To keep the math simple, set the value of the Max property to 100:

sldResize.Max = 100

The complete code below, then, sets the global variables and the Max property in the Form object's Load event:

Private Sub Form_Load()
   gHeight = picPhoto.Height/100
   gWidth = picPhoto.Width/100
   sldResize.Max = 100
End Sub

Resize the Height and Width of the PictureBox through the Scroll Event

The Slider control has a Scroll event that occurs whenever the Slider's thumb is moved by the user. Use this event when you wish to continually process the Value property of the Slider control. In the present scenario, this means the size of the PictureBox will be dynamically changed as the thumb is moved. (If you don't want the user to be distracted by the dynamically changing control, you should use the Click event. The Click event updates the size of the control after the thumb has been released.) The code below shows the formula within the Scroll event:

Private Sub sldResize_Scroll()
   picPhoto.Height = sldResize.Value * gHeight
   picPhoto.Width = sldResize.Value * gWidth
End Sub

The Complete Code

The complete code is shown below:

Private gHeight As Long
Private gWidth As Long

Private Sub Form_Load()
   gHeight = picPhoto.Height / 100
   gWidth = picPhoto.Width / 100
   sldResize.Max = 100
End Sub

Private Sub sldResize_Scroll()
   picPhoto.Height = sldResize.Value * gHeight
   picPhoto.Width = sldResize.Value * gWidth
End Sub


Slider Scenario 2: Selecting a Range of Values With the Slider

Slider Scenario 2: Select a Range of Values with the Slider

Another feature of the Slider control is the ability to select a range of values. In this implementation, when the user presses the shift key while clicking on the Slider control, the MouseDown event occurs. Code in that event sets the SelectRange and SelStart properties. When the user releases the mouse button, the MouseUp event occurs, and in that code the SelLength property is set, from which a range of values can be extracted.

The code below uses the following objects:

  • Form named "frmSlider"

  • Slider control named "sldSelect"

To select a range of values with the Slider control

  1. Set the Slider control's SelectRange property to True.

  2. In the MouseDown event, test to see if the shift key is down.

  3. In the MouseUp event, set the SelLength property to Value - SelStart.

Set the Slider Control's SelectRange Property to True

To enable the selection of a range of values, the SelectRange property must be set to True. One place to do this is the Form object's Load event, as shown below:

Private Sub Form_Load()
   sldSelect.SelectRange = True
End Sub

Alternatively, you can set the property to True at design time by right-clicking on the control, and clicking on Properties to display the Property Pages dialog box.

MouseDown Event: Test to See if the Shift Key is Down

In order to select a range, the user must hold down the shift key while moving the slider's thumb. The MouseDown event has a shift argument, which allows you to determine if the shift key is being held down. The If statement can be used to test for this possibility, as shown below:

Private Sub sldSelect_MouseDown _
(Button As Integer, Shift As Integer, _
x As Single, y As Single)

   If Shift = 1 Then
      ' If the user has the Shift key down,
      ' handle it here.
   End If
End Sub

MouseDown Event: Set the SelStart and SelLength Properties

If the shift key is being held down by the user, the code then sets the SelStart and SelLength properties to appropriate values. The SelStart property specifies where a selection of values will begin. In the present context, the SelStart property would be set to where the thumb is placed — the Slider control's Value property.

The SelLength property specifies a range of values to select; this property begins at the SelStart value. In the MouseDown event, a new range is being selected, so any previous range must be deselected by setting the SelLength property to 0. This is shown in the code below:

sldSelect.SelStart = SldResize.Value
' Set previous SelLength (if any) to 0.
sldSelect.SelLength = 0

MouseUp Event: Set the SelLength to the Value - SelStart Property

To select a range, the user must hold the shift key down while dragging the mouse. The code to set the new range is therefore found in the MouseUp event, which occurs when the end user releases the slider thumb. The code below sets the SelLength property with a simple formula, the value of the thumb minus the SelStart property:

sldSelect.Value - sldSelect.SelStart

However, it is possible for the user to release the shift key while selecting a range. In that case no selection should occur. Therefore the above code will only execute if the shift key is still down. As with the MouseDown event, an If statement can test for this possibility:

Private Sub sldSelect_MouseUp _
(Button As Integer, Shift As Integer, _
x As Single, y As Single)

   If Shift = 1 Then
   ' If user selects backwards from a point,
   ' an error will occur.
   On Error Resume Next
   ' Else set SelLength using SelStart and
   ' current value.
      sldSelect.SelLength = _
      sldSelect.Value - sldSelect.SelStart
   Else
      'If user lifts SHIFT key, set SelLength
      ' to 0 (to deselect the SelRange) and exit.
      sldSelect.SelLength = 0
   End If
End Sub

The Complete Code

The complete code is shown below:

Private Sub Form_Load()
   sldSelect.SelectRange = True
End Sub

Private Sub sldSelect_MouseDown _
(Button As Integer, Shift As Integer, _
x As Single, y As Single)

   If Shift = 1 Then
      sldSelect.SelStart = sldSelect.Value
      ' Set previous SelLength (if any) to 0.
      sldSelect.SelLength = 0
   End If
End Sub

Private Sub sldSelect_MouseUp _
(Button As Integer, Shift As Integer, _
x As Single, y As Single)

   If Shift = 1 Then
   ' If user selects backwards from a point,
   ' an error will occur.
   On Error Resume Next
   ' Else set SelLength using SelStart and
   ' current value.
      sldSelect.SelLength = _
      sldSelect.Value - sldSelect.SelStart
   Else
      'If user lifts SHIFT key, set SelLength
      ' to 0 (to deselect the SelRange) and exit.
      sldSelect.SelLength = 0
   End If
End Sub


Using the StatusBar Control

A StatusBar control is a frame that can consist of several panels which inform the user of the status of an application. The control can hold up to sixteen frames. Additionally, the control has a "simple" style (set with the Style property), which switches from multi-panels to a single panel for special messages.

The StatusBar control can be placed at the top, bottom, or sides of an application. Optionally, the control can "float" within the application's client area.

Possible Uses

  • To inform the user of a database table's metrics, such as number of records, and the present position in the database.

  • To give the user information about a RichTextBox control's text and font status.

  • To give status about key states (such as the Caps Lock or the Number Lock).

The Panel Object and the Panels Collection

The StatusBar control is built around the Panels collection. Up to sixteen Panel objects can be contained in the collection. Each object can display an image and text, as shown below:

At run time, you can dynamically change the text, images, or widths of any Panel object, using the Text, Picture and Width properties. To add Panel objects at design time, right-click on the control, and click on Properties to display the Property Pages dialog box, as shown in Figure 2.26 below:

Figure 2.26 StatusBar panels page

Using this dialog box, you can add individual Panel objects and set the various properties for each panel.

Use the Set Statement with the Add Method to Create Panels at Run Time

To add Panel objects at run time, use the Set statement with the Add method. First declare an object variable of type Panel, then set the object variable to a Panel created with the Add method, as shown in the code below:

' The StatusBar control is named "sbrDB."
Dim pnlX As Panel
Set pnlX = sbrDB.Panels.Add()

Once you have created a Panel object and set the object variable to reference the new object, you can set the various properties of the Panel:

pnlX.Text = Drive1.Drive
pnlX.Picture = LoadPicture("mapnet.bmp")
pnlX.Key = "drive"

If you plan to have the control respond when the user clicks on a particular Panel object, be sure to set the Key property. Because the Key property must be unique, you can use it to identify particular panels.

Use the Select Case Statement in the PanelClick Event to Determine the Clicked Panel

To program the StatusBar control to respond to user clicks, use the Select Case statement within the PanelClick event. The event contains an argument (the panel argument) which passes a reference to the clicked Panel object. Using this reference, you can determine the Key property of the clicked panel, and program accordingly, as shown in the code below:

Private Sub sbrDB_PanelClick(ByVal Panel As Panel)
   Select Case Panel.Key
   Case "drive"
      Panel.Text = Drive1.Drive
   Case "openDB"
      Panel.Text = rsOpenDB.Name
   Case Else
   ' Handle other cases.
   End Select
End Sub

The Style Property: Automatic Status Functions

One feature of the StatusBar control is its ability to display key states, time, and date with a minimum of code. By simply setting the Style property, any Panel object can display one of the following:

Constant | Value | Description
sbrText | 0 | (Default). Text and/or a bitmap. Set text with the Text property.
sbrCaps | 1 | Caps Lock key. Displays the letters CAPS in bold when Caps Lock is enabled, and dimmed when disabled.
sbrNum | 2 | Number Lock. Displays the letters NUM in bold when the number lock key is enabled, and dimmed when disabled.
sbrIns | 3 | Insert key. Displays the letters INS in bold when the insert key is enabled, and dimmed when disabled.
sbrScrl | 4 | Scroll Lock key. Displays the letters SCRL in bold when scroll lock is enabled, and dimmed when disabled.
sbrTime | 5 | Time. Displays the current time in the system format.
sbrDate | 6 | Date. Displays the current date in the system format.
sbrKana | 7 | Kana. displays the letters KANA in bold when kana lock is enabled, and dimmed when disabled. (enabled on Japanese operating systems only)

The code below creates eight Panel objects, and assigns one of the eight styles to each:

Private Sub MakeEight()
   ' Delete the first Panel object, which is
   ' created automatically.
   StatusBar1.Panels.Remove 1
   Dim i As Integer

   ' The fourth argument of the Add method
   ' sets the Style property.
   For i = 0 To 7
      StatusBar1.Panels.Add , , , i
   Next i

   ' Put some text into the first panel.
   StatusBar1.Panels(1).Text = "Text Style"
End Sub

Bevel, AutoSize, and Alignment Properties Program Appearance

Using the Bevel, AutoSize, and Alignment properties, you can precisely control the appearance of each Panel object. The Bevel property specifies whether the Panel object will have an inset bevel (the default), raised, or none at all. All three bevels are shown in the figure below:

Settings for the Bevel property are:

Constant | Value | Description
sbrNoBevel | 0 | The Panel displays no bevel, and text looks like it is displayed right on the status bar.
sbrInset | 1 | The Panel appears to be sunk into the status bar.
sbrRaised | 2 | The Panel appears to be raised above the status bar.

The AutoSize property determines how a Panel object will size itself when the parent container (either a Form or a container control) is resized by the user. The figure below shows a StatusBar control before being resized:

When the container (the Form on which the StatusBar control is placed) of the control is resized, notice that the first panel retains its width, the second "springs" to fill the extra space, and the third sizes according to its contents (and therefore retains its width):

Settings for the AutoSize property are:

Constant | Value | Description
sbrNoAutoSize | 0 | None. No autosizing occurs. The width of the panel is always and exactly that specified by the Width property.
sbrSpring | 1 | Spring. When the parent form resizes and there is extra space available, all panels with this setting divide the space and grow accordingly. However, the panels' width never falls below that specified by the MinWidth property.
sbrContents | 2 | Content. The panel is resized to fit its contents.

Tip Set the AutoSize property to Content (2) when you want to assure that the contents of a particular panel are always visible.

The Alignment property specifies how the text in a panel will align relative to the panel itself as well as any image in the panel. As with a word processor, the text can be aligned left, center, or right, as shown below:

Settings for the Alignment property are:

Constant | Value | Description
sbrLeft | 0 | Text appears left-justified and to right of bitmap.
sbrCenter | 1 | Text appears centered and to right of bitmap.
sbrRight | 2 | Text appears right-justified but to the left of any bitmap.

Style Property and the SimpleText Property

The StatusBar control features a secondary mode in which the multiple panels are replaced by a single panel that spans the width of the control. This single panel has one property, the SimpleText property which specifies what text is displayed on the panel. To display this single panel, set the Style property of the StatusBar to sbrSimple (1).

One reason for switching to the Simple style and displaying a single panel is to notify the user that a lengthy transaction is occurring. For example, if you are performing a database operation, the Simple style may be used to notify the user of the current state of the transaction, as seen in the code below:

Private Sub GetRecords(State)
   ' The query finds all records which match
   ' the parameter State. While the query
   ' is creating the recordset, show the
   ' SimpleText on the StatusBar control.
   sbrDB.SimpleText = "Getting records …"
   sbrDB.Style = sbrSimple ' Simple style.
   sbrDB.Refresh    ' You must refresh to see the
                  ' Simple text.

   Set rsNames = mDbBiblio.OpenRecordset _
   ("select * from Names Where State= " & _
   State)
End Sub


StatusBar Scenario: Show Various Properties of a Database

When editing records of a database, a possible application of the StatusBar control would be to inform the user of various properties such as the name of the table being edited, its creation date, and the date of the last update.

The code below uses the following objects:

  • Form named "frmDataviewer"

  • StatusBar Control named "sbrData"

  • Data control named "datData"

To add a StatusBar that shows database properties

  1. Use the Add method to create a collection of Panel objects.

  2. Configure each Panel object with the AutoSize property.

  3. Use the Panel object's Text property to display database properties.

  4. Reset the properties using the Select Case statement in the PanelClick event.

Use the Add Method to Create a Collection of Panel Objects

To create a collection of Panel objects at run time, use the Add method. First, declare a variable as type Panel. As you add each Panel object, the variable will contain the reference to the newly created object. The code below uses the Form object's Load event to create three Panel objects.

Private Sub Form_Load()
   Dim pnlX As Panel
   Dim i As Integer
   For i = 1 to 3 ' First panel already exists.
      Set pnlX = sbrData.Panels.Add()
   Next i
End Sub

Note that after adding three Panel objects to the collection, there are really four panels on the control because the control creates one by default.

Configure Each Panel Object with the AutoSize Property

One of the features of the StatusBar control is the ability of panels to automatically resize according to their contents. The example iterates through all the Panel objects, and sets the AutoSize property for each to sbrSpring (1). With this setting, each panel "springs" to share the total width of the control.

Private Sub Form_Load()
   Dim pnlX As Panel
   Dim i As Integer
   For i = 1 to 3 ' First panel already exists.
      Set pnlX = sbrData.Panels.Add()
   Next i

   ' Change the AutoSize of all panels.
   For i = 1 to 4                ' < -- New code
      sbrData.Panels(i).AutoSize = sbrSpring ' New
   Next i                               ' New
End Sub

Use the Panel Object's Text Property to Display Database Properties

To change the information displayed in any panel, set the Text property of the Panel object. The code below displays information about a database that has been opened with Data Access Objects.

In the Form object's Load event, database variables are first created and assigned to an open database ("Biblio.mdb") and a recordset ("Authors"). The code then assigns the Name, DateCreated, LastUpdated, and LockEdit properties to each Panel object's Text property.

' Declare database variables.
Dim myDB As Database, myRs As Recordset
' Set the Database to the BIBLIO.MDB database.
Set myDB = DBEngine.Workspaces(0). _
OpenDatabase("BIBLIO.MDB")
' Set the recordset variable to the Authors table.
Set myRs = _
myDB.OpenRecordset("Publishers", dbOpenTable)
' Set Text properties with recordset properties.
sbrData.Panels(1).Text = "name: " & myRs.Name
sbrData.Panels(2).Text = "Date Created: " & _
myRs.DateCreated
sbrData.Panels(3).Text = "Last Updated " & _
myRs.LastUpdated
sbrData.Panels(4).Text = "Lockedits: " & myRs.LockEdits

Reset Properties Using the Select Case Statement in the PanelClick Event

The StatusBar control can also be used to reset the properties that are being displayed. In the present scenario, a DataGrid control is bound to the Data control. For more information on how to data-bind controls, see "Using the ADO Data Control" in "Using Visual Basic's Standard Controls" of the Programmer's Guide. Among the properties displayed in the StatusBar, only the LockEdits property can be reset. To accomplish this, use a Select Case statement within the PanelClick event to determine which Panel object was clicked. The PanelClick event contains a reference to the Panel that the user clicked. Using this reference, you can reset the Text property of the clicked Panel object.

The code below first creates a variable of type Recordset and sets it to the recordset opened with the Data control. The Select Case statement is then used to test the Panel object's Index property. If the Index is 4, the LockEdits property is toggled between -1 (True) and 0 (False). Finally, the Panel object's Text property is updated with the new information.

Private Sub sbrData_PanelClick(ByVal Panel As Panel)
   Dim myRs As Recordset ' Declare Recordset variable.
   ' The Data control is named "datData"
   Set myRs = datData.Recordset ' Set variable.

   Select Case Panel.Index
   Case 1 to 3
      ' Can't set these panels.
   Case 4 ' Updateable Property is settable.
      ' Toggle the property.
      myRs.LockEdits = Abs(myRs.LockEdits) - 1
      ' Update the Panel object's Text property.
      sbrData.Panels(4).Text = "LockEdits: " _
      & myRs.LockEdits
   End Select
End Sub


Using the SysInfo Control

The SysInfo control can be used to detect system events such as desktop resizing, resolution changes, time changes, or to provide operating system platform and version information. You can also use it to manage changes to AC and battery power or changes in hardware configuration.

In Windows 95, whenever a new device is added to the system it is automatically detected. Your applications can benefit from this ‘plug and play’ technology by using the properties and events of the SysInfo control. Applications written specifically for notebook computers, for instance, can respond when a PC card is inserted or removed from the PC card slot.

The SysInfo control is invisible at run time.

Figure 2.27 The SysInfo control

Possible Uses

  • To determine operating system platform and version

  • To detect desktop and monitor size and resolution changes

  • To detect and manage plug and play devices

  • To monitor battery and power status

SysInfo Control Features

Using the SysInfo control, you can monitor information provided by the operating system as well as respond to system-generated events. The features of this control fall into one of the following groups:

  • Operating system properties (OSVersion and WorkAreaHeight properties, for example).

  • Events tied to changes in the system (DisplayChanged, TimeChanged, and SettingChanged events, for example).

  • Plug and Play events (DeviceArrival, DeviceRemoveComplete events, for example).

  • Power status events and properties (PowerSuspend and PowerResume events and ACStatus and BatteryStatus properties, for example).

The following sections describe the uses of these events and properties in the contexts described above.

Operating System Properties and Events

The operating system properties and events of the SysInfo control can be used to detect the current operating system and version, some global setting changes, or changes to desktop size and resolution.

Operating System Platform and Version Properties

The following table lists the properties which can be used to determine the current operating system platform and version:

Category | Item | Description
Properties | OSPlatform | Returns a value that identifies the operating system under which the application is currently running.
 | OSVersion | Returns a value that identifies the version of the operating system under which the application is currently running.
 | OSBuild | Returns a value that provides the build number of the operating system under which the application is currently running.

For More Information "SysInfo Scenario 1: Detect the Current Operating System and Version" provides examples of how these properties are used.

Desktop Size and Screen Resolution Properties and Events

The following table lists the operating system properties and events that specifically apply to managing desktop and screen resolution changes:

Category | Item | Description
Properties | ScrollBarSize | Returns the system metric for the width of a scroll bar in twips.
 | WorkAreaHeight | Returns the height of the visible desktop adjusted for the Windows 95 and Windows NT 4.0 taskbar.
 | WorkAreaLeft | Returns the coordinate for the left edge of the visible desktop adjusted for the Windows 95 and Windows NT 4.0 taskbar.
 | WorkAreaTop | Returns the coordinate for the top edge of the visible desktop adjusted for the Windows 95 and Windows NT 4.0 taskbar.
 | WorkAreaWidth | Returns the width of the visible desktop adjusted for the Windows 95 and Windows NT 4.0 taskbar.
Events | DisplayChanged | Occurs when system screen resolution changes.
 | SysColorsChanged | Occurs when a system color setting changes, either by an application or through the Control Panel.

The DisplayChanged event is triggered when the user changes the screen resolution. Using the WorkAreaHeight, WorkAreaWidth, WorkAreaRight, and WorkAreaLeft properties with the DisplayChanged event you can determine the current system metrics (the usable screen area) and adjust accordingly. If the taskbar is visible in Windows 95 or Windows NT 4.0, it will be excluded from the calculation of usable screen area.

If the user changes the scroll bar size using the Display settings in the Windows 95 or Windows NT 4.0 Control Panel, is it possible that the display area will need to be updated so that the new scroll bars will be positioned correctly. You can use the ScrollBarSize property with the DisplayChanged event to adjust to this change.

For More Information The section "SysInfo Scenario 2: Adjust to Changes in Screen Size and Resolution" provides examples of how these properties and events are used.

Managing Hardware Configuration and Plug and Play Events

If the operating system supports this feature, the plug and play events provide you with a way to manage changes in a system’s hardware configuration. For instance, if the hardware profile has changed, if the system is docked or undocked, or if a PC card has been added to or removed from a PC slot.

The following table lists the SysInfo control events that specifically apply to managing hardware configuration and Plug and Play devices.

Events | Description
ConfigChanged | Occurs when the hardware profile on the system has changed.
ConfigChangeCancelled | Occurs when the operating system sends a message that a change to the hardware profile was cancelled.
DeviceArrival | Occurs when a new device is added to the system.
DeviceEventOther | A notification event that does not map onto the general events.
DevModeChange | Occurs when the user changes device mode settings.
DeviceQueryRemove | Occurs just before a device is removed from the system.
DeviceQueryRemoveFailed | Occurs if code in the DeviceQueryRemove event cancelled the removal of a device.
DeviceRemoveComplete | Occurs after a device is removed.
DeviceRemovePending | Occurs after all applications have given approval to remove a device and the device is about to be removed.
QueryChangeConfig | Occurs on a request to change the current hardware profile.

The ConfigChanged event notifies you when the hardware profile in Windows 95 has changed. Notebook computers often have separate hardware profiles for their docked and undocked configurations. When docked, the notebook may include a network connection, an external monitor, or access to a compact disc drive, for example.

The device-specific events of the SysInfo control give you feedback on changes to devices within a hardware configuration. You can write your application to dynamically make use of hardware when it’s added to the system.

Using Power Status Properties and Events

The power status events and properties can be used to monitor AC and battery power states on a desktop or notebook computer. Features like power suspend, power resume, and battery status are more likely to be implemented on a notebook computer, however, they have application across all types of computers.

The following table lists the events and properties of the SysInfo control which pertain to battery and power states:

Category | Item | Description
Properties | ACStatus | Returns a value that indicates whether the system is using AC or battery power.
 | BatteryFullTime | Returns a value that indicates the full charge life of the battery.
 | BatteryLifePercent | Returns the percentage of full battery power remaining.
 | BatteryLifeTime | Returns a value that indicates the remaining life of the battery.
 | BatteryStatus | Returns a value that indicates the status of the battery’s charge.
Events | PowerQuerySuspend | Occurs when system power is about to be suspended.
 | PowerResume | Occurs when the system comes out of suspend mode.
 | PowerStatusChanged | Occurs when there is a change in the power status of the system.
 | PowerSuspend | Occurs immediately before the system goes into suspend mode.

The power events are especially useful in providing your application with a safety net should power be suspended. The PowerSuspend event notifies your application that power is about to be suspended. You can then store the application state or any unsaved files before this occurs.

The PowerStatusChanged event informs you of changes in the AC and battery power status. If battery power is running dangerously low you can still save your data.

For More Information The section "SysInfo Scenario 3: Monitor Battery Power Status" provides examples of how some of these properties and events are used to monitor and manage changes in battery and AC power.


SysInfo Scenario 1: Detecting the Current Operating System and Version

SysInfo Scenario 1: Detect the Current Operating System and Version

You can detect and display the current operating system platform, version, and build number by using a CommandButton control and the OSPlatform, OSVersion, and OSBuild properties. You can test this by adding a CommandButton control to a form and then adding the following code the cmdDetectOS_Click event procedure:

Private Sub cmdDetectOS_Click()
   Dim MsgEnd As String
   Select Case sysDetectOS.OSPlatform
      Case 0
         MsgEnd = "Unidentified"
      Case 1
         MsgEnd = "Windows 95, ver. "& _
CStr(sysDetectOS.OSVersion) & "(" & _
CStr(sysDetectOS.OSBuild) & ")"
      Case 2
         MsgEnd = "Windows NT, ver. " & _
CStr(sysDetectOS.OSVersion) & "(" & _
CStr(sysDetectOS.OSBuild) & ")"
   End Select
   MsgBox "System: " & MsgEnd
End Sub

For More Information See OSPlatform, OSVersion, and OSBuild properties.


SysInfo Scenario 2: Adjusting to Changes in Screen Size and Resolution

SysInfo Scenario 2: Adjust to Changes in Screen Size and Resolution

Using a few of the operating system properties and events provided by the SysInfo control, you can detect and respond to changes in screen size and resolution.

The following example tests the size of the active form after a change in screen resolution and adjusts the size of the form if it exceeds the visible screen area.

To resize a form after screen resolution changes, use the DisplayChanged event with the WorkAreaWidth, WorkAreaLeft, and WorkAreaTop properties.

To resize a form after screen resolution changes

  1. Create a new project in Visual Basic.

  2. Add a SysInfo control to the form.

  3. Add the following code to the SysInfo control’s DisplayChanged event procedure:
    Private Sub sysDetectOS_DisplayChanged()
If Screen.ActiveForm.Width > _
sysDetectOS.WorkAreaWidth Then
Screen.ActiveForm.Left = _
sysDetectOS.WorkAreaLeft
Screen.ActiveForm.Width = _
sysDetectOS.WorkAreaWidth
End If
If Screen.ActiveForm.Height > _
sysDetectOS.WorkAreaHeight Then
Screen.ActiveForm.Top = _
sysDetectOS.WorkAreaTop
Screen.ActiveForm.Height = _
sysDetectOS.WorkAreaHeight
End If
End Sub


SysInfo Scenario 3: Monitoring Battery Power Status

SysInfo Scenario 3: Monitor Battery Power Status

Using just one of the power status properties, you can monitor the charge status of a battery. The BatteryLifePercent property is used to provide a percentage value for a ProgressBar control. A Timer control sets the interval at which the power status is tested.

To create a simple battery meter using the BatteryLifePercent property

  1. Create a new project in Visual Basic.

  2. Add a SysInfo, a Timer, and a ProgressBar control to the form.

  3. Set the Interval property of the Timer control to 5000.

  4. Add the following code to the Timer control’s Timer event:
    Private Sub tmrBattery_Timer()
If sysBattery.BatteryLifePercent <> 255 Then
prgBattery.Value =  _ sysBattery.BatteryLifePercent
prgBattery.Enabled = True
Else
prgBattery.Value = 0
prgBattery.Enabled = False
End If
End Sub

When the application is run, the SysInfo control queries the operating system for the existence of a battery and then displays its current charge level using the BatteryLifePercent property.

Figure 2.28 A simple battery meter

This example can be enhanced by using one of the power status events to detect when a change occurs to the system’s power status. The following example adds a Label control to the form and uses the PowerStatusChanged event to notify the user when the power status changes.

To update changes to battery power status

  1. Add a Label control to the form.

  2. Add the following code to the Sysinfo control’s PowerStatusChanged event:
    Private Sub sysBattery_PowerStatusChanged()
Select Case sysBattery.BatteryStatus
Case 1
lblStatus.Caption = "Battery OK"
Case 2
lblStatus.Caption = "Battery Low"
Case 4
lblStatus.Caption = "Battery Critical"
Case 8
lblStatus.Caption = "Battery Charging"
Case 128, 255
lblStatus.Caption = "No Battery Status"
End Select
End Sub

Figure 2.29 The battery meter, enhanced with the PowerStatusChanged event

The BatteryStatus property is used in a Select Case statement to return a value that indicates the current battery status setting. Whenever the PowerStatusChange event is fired, the Label control is updated with the current status. For example, if the battery runs down or is removed from the computer.


Using the Tabbed Dialog Control

The Microsoft Tabbed Dialog control provides an easy way of presenting several dialogs or screens of information on a single form using the same interface seen in many commercial Microsoft Windows applications.

The Tabbed Dialog control provides a group of tabs, each of which acts as a container for other controls. Only one tab is active in the control at a time, displaying the controls it contains to the user while hiding the controls in the other tabs.

Figure 2.30 The Tabbed Dialog control

Possible Uses

  • To create a Microsoft Office or Windows 95 style tabbed dialog box.

  • To create a custom tabbed dialog box.

Setting Tabs and Tab Rows

Before setting the number of tabs you want to add to the Tabbed Dialog control, you should first decide what your dialog box will contain and how it will be organized.

While you can set the numbers of tabs at both design time and run time, you will more than likely find that creating your tabbed dialog at design time is much quicker and easier. You can set properties at design time using the Tabbed Dialog Property Pages which is available by right-clicking the control and then selecting Properties.

Figure 2.31 Setting properties at design time

You set the Tab and TabsPerRow properties to define the number of tabs and rows in your dialog box. For example, if you want to a create a tabbed dialog that contains twelve tabs you set the Tab Count option to ‘12’ and the TabsPerRow option to ‘4’ — this creates a tabbed dialog containing three rows of four tabs each. By default, the number of tabs is set at 3.

When the number of tabs and rows has been set, each tab is indexed and can then be selected individually. For example, you can select individual tabs in the Current Tab option to change the TabCaption property. Tabs are indexed beginning at zero (0).

At run time, the user can navigate through the tabs by either clicking on them, by pressing ctrl+tab, or by using mnemonics defined in the caption of each tab. For example, if you wanted to create a tab called ‘Print’ and allow the user to access the tab with the keyboard combination alt+p, you set the TabCaption property to ‘&Print’.

Adding Controls to Tabs

Each tab in the Tabbed Dialog control is essentially a container for other controls. When you create a tabbed dialog box, you group controls together that perform similar functions, such as printing a document or setting display options for your application. Once you’ve determined how many tabs you need in your tabbed dialog box, you add whichever controls you need to perform those functions.

To add controls to individual tab pages at design time, first select the tab by clicking on it, then draw the controls on the tab page.

Note You should not add controls to individual tab pages using the double-click method. Double-clicking a control from the toolbox onto a tab page places that control on every tab in the Tabbed Dialog control.

For More Information See "Containers for Controls" in "Using Visual Basic's Standard Controls" in the Programmer’s Guide .

Enabling and Disabling Tabs at Run Time

Depending upon the functionality of your application or a particular tabbed dialog box you create, you may want to disable some tabs in certain situations. You use the TabEnabled property to enable and disable individual tabs. When a tab is disabled, the text on the tab is grayed out and the user cannot select that tab. For example:

SSTab1.TabEnabled(2) = False

The TabEnabled property specifies the tab number, then disables it by setting the value to False.

Note Use the Enabled property to enable or disable the entire Tabbed Dialog control.

Tab Customizing Properties

Using the Tabbed Dialog control properties, you can customize the look and functionality of your tabbed dialog box. You can set these properties at design time using the control’s Property Pages or at run time in code.

The Style Property

The Style property gives you two options for setting the style of your tabbed dialog box. By default, the Style property is set to display as a Microsoft Office tabbed dialog. The tabs that appear when this option is selected look like those in Microsoft Office for Microsoft Windows 3.1 applications. By default, the selected tab caption text is displayed in bold.

Figure 2.32 The Microsoft Office tabbed dialog style

The second option available is the Windows 95 Property Pages tabbed dialog style. This tabbed dialog style conforms to the user interface standards developed for Windows 95. The selected tab caption text, unlike the Microsoft Office style, is not displayed as bold.

Figure 2.33 The Windows 95 Property Pages tabbed dialog style

To set this property at run time use the Visual Basic constants ssStyleTabbedDialog or ssStylePropertyPage to specify either style.

The TabOrientation Property

The TabOrientation property allows you to locate the tabs of your tabbed dialog box on either of the four sides (top, bottom, left, right). For example:

Figure 2.34 Tab orientation set to left

When you set the orientation of the tabs to something other than top or bottom, you must also change the font style of the tabs. Setting the tabs to left or right rotates the text vertically and only TrueType® fonts display vertically in the Tabbed Dialog control. Change the font style using the Font property or by selecting the Font tab in the control’s Property Pages. Arial is a common substitute for the Windows default sans serif font.

You can set this property at run time using the following Visual Basic constants: ssTabOrientationTop, ssTabOrientationBottom, ssTabOrientationLeft, and ssTabOrientationRight.

The Picture Property

You can add pictures (bitmaps, icons, or metafiles) to any or all of the tabs in the Tabbed Dialog control. For example:

Figure 2.35 Adding a picture to a tab

At design time, you set the Picture property for a tab by clicking that tab and then setting the property in the Properties window. At run time, you can set the Picture property using the LoadPicture function or the Picture property of another control or of a Form object.

Note When setting the Picture property at design time, the graphic is saved and loaded with the Form object containing the Tabbed Dialog control. If you create an executable file, the file contains the image. When you load a graphic at run time, the graphic isn't saved with the application.

Setting the Picture property affects the value of the TabPicture property for the current tab as well as displays the picture in the active tab.

Adjusting Tab Height and Width

Depending upon its size, you may need to resize the tab height to accommodate the image. Use the TabMaxHeight property to adjust the height of the tabs. The TabMaxHeight property sets the height for every tab in the control — individual tabs cannot be adjusted separately.

If you’re using the Microsoft Office style tabbed dialog, you may also need to set the TabMaxWidth property to allow both the image and the text to fit on the tab. If you’re using the Windows 95 style tabbed dialog, the TabMaxWidth property is ignored and the width of each tab adjusts to the combined width of the image and the length of the text in its caption.

Setting the WordWrap Property

When using the Microsoft Office style tabbed dialog, you may specify the exact width of the tabs using the TabMaxWidth property. Having done so, you can then allow tab captions to wrap to the next line, if necessary, by setting the WordWrap property to True. Although caption text will wrap, you are still limited by the height and width of the tabs. In other words, you may still get clipped or hidden caption text if the height and width are insufficient.

The Windows 95–style tabbed dialog sets tab width based on the length of the caption text; the TabMaxWidth and WordWrap property settings are ignored.

Completing Your Custom Tabbed Dialog Box

Once you’ve completed adding controls to each tab, you can finish designing your custom tabbed dialog box to resemble a standard tabbed dialog box. For example, if you want it to display like a standard Windows 95 Property Pages dialog box, follow these steps:

  • Resize the underlying form so that the Tabbed Dialog control is centered and equal amounts of space remain on the top, right and left edges. Leave a larger amount of space below the control.

  • Add two command buttons — an OK and a Cancel button. Align them below the right edge of the Tabbed Dialog control.

  • Set the form’s BorderStyle property to Fixed Dialog.


Using the TabStrip Control

A TabStrip acts like the dividers in a notebook or the labels on a group of file folders. By using a TabStrip control, you can define multiple pages for the same area of a window or dialog box in your application.

Possible Uses

  • To create a tabbed dialog that sets various text attributes for a RichTextBox control.

  • To create a tabbed dialog that sets preferences for an application.

The Tabs Collection

The control consists of one or more Tab objects in a Tabs collection. At both design time and run time, you can affect the Tab object's appearance by setting properties, and at run time, by invoking methods to add and remove Tab objects.

Associate the ImageList Control with the TabStrip Control

To identify a tab's function, you can assign an image from the ImageList control to the Tab object. You must first associate an ImageList control with the TabStrip control, and this can be accomplished at either design time or run time.

To associate an ImageList control with a TabStrip control at design time:

  1. Populate the ImageList control with images for the tabs.

  2. Right-click on the TabStrip control and click Properties to open the TabStrip Property Page dialog box.

  3. On the General tab, click the ImageList box and select the ImageList control you have populated.

  4. To associate an ImageList control with the control at run time, simply set the ImageList property to the name of the ImageList control, as shown in the example below:
Private Sub Form_Load()
   ' The TabStrip control is named "tabRTF," and the
   ' ImageList control is named "imlRTF."
   tabRTF.ImageList = imlRTF
End Sub

Create Tabs at Design Time or Run Time

You can create Tab objects at both design and run time. To create Tab objects at design time, use the Property Pages dialog box.

To create Tab objects at design time

  1. Right-click the TabStrip control and click Properties to display the Property Pages dialog box.

  2. Click Tabs to display the Tabs page, as shown in Figure 2.36, below:

Figure 2.36 TabStrip Property Pages

Create Tab Objects at Run Time Using the Add Method

To create Tab objects at run time, use the Add method for Tab objects.

Note One Tab object is created for you by default.

To create a collection of Tab objects at run time

  1. Declare a variable as type Tab. As you add each Tab object, the variable will contain the reference to the newly created object. Use this reference to set various properties of the new Tab object.

  2. Using the Set statement with the Add method, set the object variable to the new Tab object.

  3. Using the object variable, set the properties of the new Tab object.

The code below uses the Form object's Load event to create two Tab objects, then sets the Caption, Image, and Key properties of the new Tab object.

Private Sub Form_Load()
   ' The TabStrip control is named "tabData"
   ' Declare a variable, then use the Set
   ' statement with the Add method to create a new
   ' Tab object, while setting the object variable to
   ' the new Tab. Use the reference to set properties.
   Dim tabX As Tab
   ' Tab 1: Find text.
   Set tabX = tabData.Tabs.Add()
   tabX.Key = "find"
   tabX.Caption = "Find"
   tabX.Image = "Find" ' Assuming this image exists.

   ' Tab 2: Draw objects.
   Set tabX= tabData.Panels.Add()
   tabX.Key = "draw"
   tabX.Caption = "Draw"
   tabX.Image = "draw" ' Assuming this image exists.
End Sub

Tip Using the Add method without setting the object variable is more efficient than setting the properties with the object variable. In this case, the code above could be rewritten as:

tabData.Tabs.Add , "find", "Find", "find"
tabData.Tabs.Add , "draw", "Draw", "draw"

Use the Client Area to Position Container Controls

The TabStrip control is commonly used to create tabbed dialog boxes. Each page in the dialog box consists of a tab and a client area, as seen in the figure below:

At run time, when the user clicks on a tab, you must program the client area to be reconfigured with a different set of container controls (discussed below in "Managing Tabs and Container Controls").

At design time, draw a container control, such as the PictureBox or Frame control, on the form. If you use a Frame control, you can set its BorderStyle property to be invisible at run time. Copy and paste the same control to create an array of controls; create one control for each Tab object you have created.

On each container control, draw the controls that should appear on a tab. Your form may appear something like Figure 2.37, below:

Figure 2.37 TabStrip at design time with two PictureBox controls

After you have created the container controls, there is one additional technique required to position them over the TabStrip control's client area: use the Move method with the ClientLeft, ClientTop, ClientWidth, and ClientHeight properties of the Tabstrip control, as shown in the code below:

Private Sub Form_Load()
   ' The name of the TabStrip is "tabRTF."
   ' The Frame control is named "fraTab."
   For i = 0 To fraTab.Count - 1
   With fraTab(i)
      .Move tabRTF.ClientLeft, _
      tabRTF.ClientTop, _
      tabRTF.ClientWidth, _
      tabRTF.ClientHeight
   End With
   Next i
End Sub

Managing Tabs and Container Controls

A tabbed dialog box contains more than one Tab object. As seen above, a Frame control (or other container control) should be associated with each Tab object. To efficiently manage the numerous Tab objects and container controls, the following strategy can be used:

  1. At design time, create all the Tab objects you need.

  2. Create a control array of container controls, one member for each Tab object.

  3. On each container control, draw the controls that you want to have on a tab page.

  4. At run time, use the control's SelectedItem property to determine the Index of the clicked Tab object.

  5. Use the ZOrder method to bring the appropriate container control to the front.

The code to bring the proper container control to the front would then resemble the code below:

Private Sub tabRTF_Click()
   picRTF(tabRTF.SelectedItem.Index - 1).ZOrder 0
End Sub

Tip At design time, you can set the Index property of the control array to become a 1-based array. Because the Tabs collection is also a 1-based collection, the above code would then be rewritten:

picRTF(TabRTF.SelectedItem.Index).ZOrder 0

For More Information For an example of code implementing the strategy outlined above, see "TabStrip Scenario: Create a Tabbed Dialog Box."

Tab Style Property: Buttons or Tabs

The Style property determines whether the TabStrip control looks like

notebook tabs (Tabs),

or push buttons (Buttons).

The advantages of each are outlined below:

Style | Possible Use
Tabs | Use the Tabs style to create Tabbed dialog boxes. With this style, the complete tabbed dialog, including the client area, is drawn for you. Your code must manage what appears in the client area.
Buttons | The Buttons style can be used to create a toolbar or task bar — in other words, when you do not need the client area, but prefer to have only the buttons as an interface element. Alternatively, you may wish to use the Buttons style when you do not need a well-defined client area drawn for you.

Multi-Row Tabs

Another feature of the TabStrip control is the MultiRow property. When this property is set to True, a large number of Tab objects appear in rows, as seen in the figure below:

If the MultiRow property is set to False, the same set of tabs appears in a single row, with a pair of scroll buttons at the rightmost end:

The TabWidthStyle property determines the appearance of each row, and, if TabWidthStyle is set to Fixed, you can use the TabFixedHeight and TabFixedWidth properties to set the same height and width for all tabs in the TabStrip control.


Tabstrip Scenario: Create a Tabbed Dialog Box

The TabStrip control is used to create dialog boxes which contain a number of tabs. Each tab usually has some relationship to a larger theme, and is therefore related to other tabs in the same dialog box. In this scenario, we create a tabbed dialog box which sets the fonts, and indents of a RichTextBox.

The following objects are used in the code below:

  • Form object named "frmRTF"

  • RichTextBox control named "rtfData"

  • TabStrip control named "tabRTF"

  • Form object named "frmTab"

  • Frame control named "fraTab"

  • ComboBox named "cmbFonts"

  • OptionButton control named "optNormal"

  • OptionButton control named "optBullet"

To create a Tabbed Dialog Box

  1. Create two forms, one named "frmRTF" to contain the RichTextbox, and a second named "frmTab" to contain the TabStrip control.

  2. At design time, create two Tab objects on the TabStrip control.

  3. Create a Frame control array named "fraTab" on frmTab.

  4. Draw the ComboBox on fraTab(0) and two OptionButton controls on fraTab(1).

  5. Use the Move method in the Load event to position the Frame controls.

  6. In the TabStrip control's Click event, use the SelectedItem property to determine the Index of the clicked Tab.

  7. Use the Index with the ZOrder method to bring the right Frame to the front.

Create two Forms, One Named "frmRTF" to Contain the RichTextbox, and a Second Named "frmTab" to Contain the TabStrip Control

This scenario requires two forms: the first is named "frmRTF," and contains the RichTextBox control, the second, named "frmTab", contains the TabStrip control.

To create two Form objects

  1. On the File menu, click New Project to display the New Project dialog box.

  2. Double-click the Standard EXE Project icon, and a new form named Form1 will be created for you.

  3. If the Properties window is not showing, press f4 to display it.

  4. Click the Name box and type "frmRTF."

  5. Draw a RichTextBox control on the form.

    Note You must have the RichTextBox (RichTx32.ocx) loaded into the Toolbox. See "Loading ActiveX Controls" for more information.

  6. On the Properties page window, click the Name box and type "rtfData"

  7. On the Project Explorer window, click Add Form to display the Add Form dialog box.

  8. Double-click the Form icon to insert another form into the project.

  9. On the Properties window, click the name box and type "frmTab."

  10. Draw a TabStrip control on frmTab, and name it "tabRTF."

You must also have some code that shows the second form. A quick way to do this would be to place a Show method in the DblClick event of the first Form object (frmRTF), as shown below:

Private Sub Form_DblClick()
   frmTab.Show
End Sub

At Design time, Create Two Tab Objects on the TabStrip Control

You can create Tab objects at design time and at run time. In this scenario, you should create the two tabs at design time. Right-click on the TabStrip control and click Properties to display the Property Pages dialog box. Then click the Tabs tab and click Insert Tab twice. Be sure to give the tabs appropriate captions — "Fonts," and "Indents."

Create a Control Array Named "fraTab" on frmTab

A TabStrip control functions by managing Tab objects. Each Tab object is associated with a container control that appears in the tab's client area. It's most efficient to use a control array to create the container controls. In this scenario, draw a Frame control on the same form as the TabStrip control, and name it "fraTab."

To create a control array

  1. Draw a Frame control on frmTab.

  2. Click the Name box on the Properties window and type "fraTab."

  3. Click the Frame control and copy it to the clipboard by either pressing ctrl+c or clicking Copy from the Edit menu.

  4. Paste the same control back on the form by pressing ctrl+v. A dialog box will ask you if you want to create a control array. Click Yes.

Draw the ComboBox on fraTab(0) and Two OptionButton controls on fraTab(1)

On the control named fraTab(0), draw a ComboBox control, and name it "cmbFonts." To populate the ComboBox with all available fonts on your system, use the following code:

Private Sub Form_Load()
   Dim i   ' Declare variable.
   ' Determine number of fonts.
   For i = 0 To Printer.FontCount - 1
      ' Put each font into list box.
      cmbFonts.AddItem Printer.Fonts(I)
   Next i
   cmbFonts.ListIndex = 0
End Sub

To set the SelFontName property of the RichTextBox control, use the following code:

Private Sub cmbFonts_Click()
   frmRtf.rtfData.SelFontName = cmbFonts.Text
End Sub

Draw two OptionButton controls on the second Frame control named fraTab(0). Name the first OptionButton control "optNormal," and change its Caption property to "Normal." Name the second control "optBullet," and set its Caption property to "Bullet." The code for these controls sets the SelBullet property to True or False. The code for each is shown below:

Private Sub optBullet_Click()
   ' The Form object's ScaleMode is set to Twips.
   frmRTF.rtfData.BulletIndent = 500
   frmRTF.rtfData.SelBullet = True
End Sub

Private Sub optNormal_Click()
   frmRTF.rtfData.SelBullet = False
End Sub

Use the Move Method in the Load Event to Position the Frame Controls

To position the Frame controls over the client area, use the Move method in the Form object's Load event, as shown below:

Private Sub Form_Load()
   ' The name of the TabStrip is "tabRTF."
   ' The Frame control is named "fraTab."
   For i = 0 To    fraTab.Count - 1
   With fraTab(i)
      .Move tabRTF.ClientLeft, _
      tabRTF.ClientTop, _
      tabRTF.ClientWidth, _
      tabRTF.ClientHeight
   End With
   Next I

   ' Bring the first fraTab control to the front.
   fraTab(0).ZOrder 0
End Sub

In the TabStrip Control's Click Event, Use the SelectedItem Property to Determine the Index of the Clicked Tab

To determine which Tab object, use the SelectedItem property. This property returns a reference to the clicked tab. However, the Tabs collection is a 1-based collection (the collection index begins with 1), and the fraTab array is a 0-based collection. To make sure the two are synchronized, subtract 1 from the Index, as shown below.

Private Sub tabRTF_Click()
fraTab(tabRTF.SelectedItem.Index - 1).ZOrder 0
End Sub

Tip At design time, you can set the Index property of the Frame control array to become a 1-based array. Thus the code above would read:

fraTab(tabRTF.SelectedItem.Index).ZOrder 0

The Complete Code

The complete code is shown below:

Private Sub Form_Load()
   Dim i As Integer' Declare variable.
   ' Determine number of fonts.
   For i = 0 To Printer.FontCount - 1
      ' Put each font into list box.
      cmbFonts.AddItem Printer.Fonts(i)
   Next i

   cmbFonts.ListIndex = 0

   ' The name of the TabStrip is "tabRTF."
   ' The Frame control is named "fraTab."
   For i = 0 To fraTab.Count - 1
   With fraTab(i)
      .Move tabRTF.ClientLeft, _
      tabRTF.ClientTop, _
      tabRTF.ClientWidth, _
      tabRTF.ClientHeight
   End With
   Next i

   ' Bring the first fraTab control to the front.
   fraTab(0).ZOrder 0
End Sub

Private Sub cmbFonts_Click()
   frmRTF.rtfData.SelFontName = cmbFonts.Text
End Sub

Private Sub optBullet_Click()
   frmRTF.rtfData.BulletIndent = 500
   frmRTF.rtfData.SelBullet = True
End Sub

Private Sub optNormal_Click()
   frmRTF.rtfData.SelBullet = False

End Sub

Private Sub tabRTF_Click()
   fraTab(tabRTF.SelectedItem.Index - 1).ZOrder 0
End Sub

This following code goes into the form named "frmRTF."

Private Sub Form_DblClick()
   frmTab.Show
End Sub


Using the Toolbar Control

A Toolbar control contains a collection of Button objects used to create a toolbar you can associate with an application.

Typically, a toolbar contains buttons that correspond to items in an application's menu, providing a graphic interface for the user to access an application's most frequently used functions and commands. The Toolbar control can also contain other controls, such as ComboBox or TextBox controls.

To create a toolbar, you must add Button objects to a Buttons collection; each Button object can have optional text and/or an image, supplied by an associated ImageList control. Set text with the Caption property, and an image with the Image property for each Button object. At design time, you can add Button objects to the control with the Toolbar Property Pages dialog box. At run time, you can add or remove buttons from the Buttons collection using Add and Remove methods.

To add other controls at design time, simply draw the desired controls on the toolbar. Alternatively, you can create a Button object with a Placeholder style and position the desired control over the button in a Resize event.

Double-clicking a toolbar at run time invokes the Customize Toolbar dialog box, which allows the user to hide, display, or rearrange toolbar buttons. To enable or disable the dialog box, set the AllowCustomize property. You can also invoke the Customize Toolbar dialog box by invoking the Customize method. If you wish to save and restore the state of a toolbar, or allow the end user to do so, use the SaveToolbar and RestoreToolbar methods.

Possible Uses

  • Provide a consistent interface between applications with matching toolbars.

  • Place commonly used functions, such as File operations, in an easy to access place.

  • Provide a graphical, intuitive interface for your application.

The Buttons Collection

The Toolbar control consists of one or more Button objects in a Buttons collection. At both design time and run time, you create Button objects. Each button can have an image, a caption, a ToolTip, or all three, as shown below:

Each button object also has a Style property (discussed below) that determines how the button will behave.

Associate the ImageList Control with the Toolbar Control

Toolbars usually feature icons that represent a function of the application. For example, an icon of a floppy disk is generally understood to represent a "Save File" function. To get your toolbar to display such images, you must first associate an ImageList control with the Toolbar control, and this can be accomplished at either design time or run time.

To associate an ImageList control with a Toolbar control at design time

  1. Populate the ImageList control with images for the Toolbar.

  2. Right-click on the Toolbar control and click Properties to open the Property Pages dialog box.

  3. On the General tab, click the ImageList box and select the ImageList control you have populated.

To associate an ImageList control with the Toolbar control at run time, simply set the ImageList property to the name of the ImageList control, as shown in the example below:

Private Sub Form_Load()
   ' The Toolbar control is named "tlbTools," and the
   ' ImageList control is named "imlTools."
   tlbTools.ImageList = imlTools
End Sub

Create Buttons at Design Time or Run Time

To create Button objects at design time

  1. Right-click on the Toolbar control and click Properties to display the Toolbar Property Pages.

  2. Click the Buttons tab to display the dialog box shown in Figure 2.38, below:

    Figure 2.38 Toolbar control property pages

  3. Click Insert Button to insert a new Button object.

  4. Set appropriate properties, such as Key, Caption, Image, and ToolTipText.

  5. Set the Button object's Style property by clicking the Style box and selecting a style.

To create a collection of Button objects at run time

  1. Declare an object variable of type Button. As you add each Button object, the variable will contain the reference to the newly created object. Use this reference to set various properties of the new Button object.

  2. Using the Set statement with the Add method, set the object variable to the new Button object.

  3. Using the object variable, set the properties of the new Button object.

The code below uses the Form object's Load event to create one Button object, then sets the Key, Caption, ToolTipText, and Style properties of the new Button object.

Private Sub Form_Load()
   ' Declare a variable, then set using the Set
   ' statement with the Add method, create a new
   ' Button object, and set the object variable to
   ' the new Button. Use the reference to set
   ' properties.
   Dim myButton As Button
   Set myButton = tlbTools.Buttons.Add()
   myButton.Key = "left"
   myButton.Caption = "Align Left"
   myButton.ToolTipText = "Align Left"
   myButton.Style = tbrSeparator
End Sub

Tip Using the arguments of the Button collection's Add method is more efficient than setting the properties with the object variable. In this case, the code above could be rewritten as:

tlbTools.Buttons.Add , "left", "Align Left", _
tbrSeparator

Button Style Property Determines Button Behavior

An important property of the Button object is the Style property. The Style property determines how a button behaves — and the function assigned to the button can have a bearing on which style is applied to it. The five button styles are listed below, with their possible uses:

Constant | Value | Possible Use
tbrDefault | 0 | Use the Default button style when the function it represents has no dependence on other functions. For example, a Save File operation can be performed at any time. Further, when the button is depressed, it springs back again when the function is finished.
tbrCheck | 1 | The Check style should be used when the function it represents is a toggle of some kind. For example, when using a RichTextBox control, selected text can be either bold or not. Thus, when the button is depressed, it stays depressed until it is pressed again.
tbrButtonGroup | 2 | Use the ButtonGroup style when a group of functions are mutually exclusive. That is, only one of the functions represented by the group of buttons can be on at a time. For example, text in a RichTextBox control can only be left-aligned, center-aligned, or right-aligned — it cannot be more than one style at a time. Note: although only one button at a time can be depressed, all buttons in the group can be unpressed.
tbrSeparator | 3 | The separator style has no function except to create a button that is eight pixels wide. Use the separator style to create a button that separates one button from another. Or use it to enclose the group of buttons with the ButtonGroup style.
tbrPlaceholder | 4 | The placeholder style functions as a "dummy" button: use this button to create a space on the Toolbar control where you want to have another control (such as a ComboBox or ListBox control) appear.

Placing Controls on the Toolbar

You can easily place other controls, such as the ComboBox, TextBox, or OptionButton control, on the Toolbar control at design time.

To place other controls on the Toolbar control at design time

  1. Create Button objects and assign appropriate properties.

  2. Create a space on the toolbar where you want the other control to appear, then add a button with the Placeholder style, and set the Width property to an appropriate value.

  3. Draw the other control in the space occupied by the placeholder button.

Reposition Other Controls in the Resize Event

If the Wrappable property is set to True, the Toolbar control wraps automatically when the end user resizes the form. While Button objects wrap automatically, controls placed on them do not. To enable controls to wrap, first create a Button object with the Placeholder style, and draw the control in the space created by the button (as shown in the above topic). Then reposition the control over the button using the Move method in the Form object's Resize event, as shown below:

Private Sub Form_Resize()
   ' The Toolbar is named "tlbRTF"
   ' The Button object's Key is "btnFonts"
   ' The Combobox is named "cmbFonts"

   ' The ComboBox is placed over the position of the
   ' Button object using the Move method.
   With tlbRTF.Buttons("btnFonts")
      cmbFonts.Move .Left, .Top, .Width
      cmbFonts.ZOrder 0
   End With
End Sub

Use the Select Case Statement in the ButtonClick Event to Program Button Functionality

The ButtonClick event occurs whenever a button (except buttons with the placeholder or separator style) is clicked. You can identify the button that was clicked by its Index property or its Key property. Using either of these properties, use the Select Case statement to program the button's function, as shown in the example code below:

Private Sub tlbRTF_Click(ByVal Button As Button)
   Select Case Button.Key
   Case "OpenFile"
      ' Call a user-defined function to open a file.
      OpenFile
   Case "Bold"
      ' Call a user-defined function to bold text.
      BoldText
   Case Else
      ' Handle other cases.
   End Select
End Sub

Use the MixedState Property to Signify Indeterminate States

In some cases, a function of your application may return an indeterminate state — a state that is a combination of two or more states. For example, if the user selects text in a RichTextBox, and some of the text is italicized, the button that represents italicized text cannot be either checked or unchecked; the text in the selection is both. To signify this indeterminate state, set the MixedState property to True. This dithers the image on the button to create a third state of the button's image.

Set ToolTip Text with the ToolTipText Property

A ToolTip is the text that appears above a button whenever the cursor hovers (without clicking) over a Button object.

You can add a ToolTip to any button at design time by typing the text you want to appear in the ToolTipText box of the Toolbar control's Property Pages.

At run time, you can dynamically change the ToolTip by setting the ToolTipText property for the Button object. The following code occurs in a CommandButton control that changes the Key and ToolTipText property of one button:

Private Sub cmdChangeButton_Click()
   ' The name of the toolbar is "tlbFunctions"
   ' Reset the Key and ToolTipText properties of
   ' a button with Key property value "1 funct"
   tlbfuncts.Buttons("1 funct"). _
   ToolTipText = "Function 7"

   tlbfuncts.Buttons("1 funct").Key = "7 funct"
End Sub

Allowing End Users to Customize the Toolbar

If you set the AllowCustomize property to True, the end user can customize the toolbar by double-clicking it. Double-clicking the toolbar displays the Customize Toolbar dialog box, shown in Figure 2.39 below:

Figure 2.39 Customize Toolbar dialog box

Alternatively, you can display the dialog box by invoking the Customize method.

Using the SaveToolbar and RestoreToolbar Methods

If you allow your end user to reconfigure the Toolbar control, you can save and restore the toolbar by using the SaveToolbar and RestoreToolbar methods. For example, if several users use the same application but have different toolbar preferences, use the SaveToolbar method to allow each person to create a customized toolbar. Then create a login procedure that identifies each user, and uses that information to restore the user's personalized toolbar with the RestoreToolbar method.

The SaveToolbar method saves the current state of the Toolbar control to the system registry. The method requires three arguments. The first argument, key, must be an integer. The second and third arguments, subkey and value, must be strings. To store different versions of the same toolbar in the same subkey, set the value argument to a different string.

The code below uses two constants to define the first two arguments. In the Form object's Load event, the code invokes a procedure named "Login" that returns the password of the user. The code then uses that value to restore a previously stored toolbar. The command button named "cmdSaveToolbar" saves the current state using the same three values.

' Declarations: SaveToolbar method constants.
Const SaveKey = 1
Const SaveSubKey = "MyToolbar"
Dim mSaveVal As String ' Module variable that
                       ' identifies user.

Private Sub Form_Load()
   ' Run a login procedure that identifies the
   ' user. Use the user's password to identifiy the
   ' user's toolbar.
   mSaveVal = LogIn()

   ' Restore state of Toolbar1 using Constants.
   Toolbar1.RestoreToolbar SaveKey, SaveSubKey, _
   mSaveVal
End Sub

Public Function LogIn()
   ' Ask the user for a password.
   LogIn = InputBox("Password")
End Function

Private Sub cmdSaveToolbar_Click()
   ' Save the toolbar using the same constants.
   Toolbar1.SaveToolbar SaveKey, SaveSubKey, mSaveVal
End Sub


Toolbar Scenario 1: Provide an RTF Text Editor with an OpenFile Button

With the RichTextBox control, you can create applications that incorporate an RTF text editor to allow end users to edit documents without starting up a second application. When you incorporate such a text editor into the application, you can provide a toolbar that gives the end user access to common operations such as opening a file and changing font characteristics. This simple scenario populates the Toolbar control with only one Button object. When the user clicks the button, the Open File dialog box is opened.

The following objects are used in this scenario:

  • Form object named "frmRTF"

  • Toolbar control named "tlbRTF"

  • RichTextBox control named "rtfData"

  • ImageList control named "imlToolbar"

  • CommonDialog control named "dlgOpenFile"

To incorporate a toolbar into your application

  1. At design time, populate an ImageList control with appropriate images.

  2. Associate the ImageList control with the Toolbar control.

  3. Create buttons for the toolbar at design time.

  4. Create a File menu with an Open menu item named "mnuOpen."
  5. In the Form_Load event, configure a CommonDialog control to open Rich Text Format (RTF) files.
  6. In the mnuOpen_Click event, use the CommonDialog control to open a file.
  7. In the Toolbar control's Button_Click event, use the Select Case statement with the Key property to identify the clicked Button object and invoke the mnuOpen_Click event.

Populate an ImageList Control with Appropriate Images

At design time, populate the ImageList control with the images you will need to populate your Toolbar control. This is easily accomplished.

To populate an ImageList at design time

  1. Right-click on the ImageList control, then click Properties to open the Property Pages dialog box.

    –or–

  2. Click the ImageList control, then press f4 to get the Properties Window, and double-click the Custom property.

  3. Click the Images tab.

  4. Click Insert Picture. In the Select Picture dialog box, find the image you wish to have appear in the Toolbar control. Repeat this until you have all the images you require. (The sample application uses the bitmap named "open.bmp.")

Associate the ImageList Control with the Toolbar Control

Also at design time, after you have populated the ImageList control, you must associate it with the Toolbar control.

To associate the ImageList control with the Toolbar control at design time

  1. Right-click on the ImageList control, then click Properties to open the Property Pages dialog box.

    –or–

  2. Click the ImageList control, then press f4 to get the Properties Window, and double-click the Custom property.

  3. On the General tab, click the ImageList box and select the ImageList control that you have just populated.

Create Buttons for the Toolbar at Design Time

Once you have populated the ImageList control and associated it with the Toolbar control, you can begin to create the Button objects themselves. In this scenario, we will create two buttons, one with the Placeholder style, and a second with the Default style.

To add buttons to the Toolbar Control at design time

  1. Right-click on the Toolbar control, then click Properties

  2. In the Property Pages dialog box, click the Buttons tab.

  3. Click Insert Button.

  4. Click the Style box and select Separator.

  5. Click Insert Button again — the style is set to Default automatically.

  6. In the Key box, type openFile.

  7. In the Description box, type Open File.

  8. In the Image box and type 1.

  9. Click OK.

Create a File Menu with an Open Menu Item Named "mnuOpen"

Because the Open File operation is commonly invoked from a menu bar, we must create a menu first.

To create a menu

  1. Select the form "frmRTF."

  2. Press ctrl+e to display the Menu Editor dialog box.

  3. In the Caption box, type File.

  4. In the Name box, type mnuFile.

  5. Click the Next button.

  6. In the Caption box, type Open.

  7. In the Name box, type mnuOpen.

  8. Click the right-pointing arrow to indent the menu item.

Configure a CommonDialog Control to Open Rich Text Format (RTF) Files

Use the CommonDialog control to create an Open File dialog box. This can be done in the Form object's Load event, as shown below:

Private Sub Form_Load()
   ' Configure dlgOpenFile for opening and
   ' saving files.
   With dlgOpenFile
      .DefaultExt = ".rtf"
      .Filter = "RTF file (*.RTF) | *.RTF"
   End With
End Sub

Use the CommonDialog Control to Open a File

In the present scenario, the Toolbar control's single button simply represents a common operation, an Open File function, that is also found on the menu bar. Thus, the code for the Open File operation should be placed in the mnuOpen object's Click event, as shown below:

Private Sub mnuOpen_Click()
   ' Declare a string variable to hold the file name.
   ' Then invoke the ShowOpen method to show
   ' the dialog box. Set the variable to the Filename
   ' property. Finally, load the RichTextBox control.
   Dim strOpen As String
   dlgOpenFile.ShowOpen
   strOpen = dlgOpenFile.Filename
   rtfData.LoadFile strOpen, rtfRTF
End Sub

Use the Select Case Statement with the Key Property to Identify the Clicked Button Object and Invoke the mnuOpen_Click Event

When any Button object on the Toolbar control is clicked, the ButtonClick event occurs. To determine which Button object was clicked, use the Select Case statement with either the Key property or the Index property.

The example below uses the Key property of the Button object.

Private Sub tlbRTF_ButtonClick _
(ByVal Button As Button)
   Select Case Button.Key
   ' User clicked "open file" button.
   Case "openFile"
      mnuOpen_Click ' Invoke the mnuOpen Click event
   End Select
End Sub

You can now run the project and open an RTF file by clicking the button on the toolbar.


Toolbar Scenario 2: Set the Text Alignment of a RichTextBox Control Using Grouped Buttons

This scenario builds upon the previous scenario, where a Toolbar control is used to open a file loaded into a RichTextBox control. In this scenario, you will add three buttons to the Toolbar control with icons for left-aligned, center-aligned, and right-aligned.

The three buttons are all a part the ButtonGroup (tbrButtonGroup) style which allows only one of the three to be pressed at any time.

Important To specify which buttons are part of a button group, you must "enclose" the group with two other buttons which have the Separator (tbrSeparator) style. For example, if there are three buttons in a group, and they are the fourth through the sixth buttons, then the third and seventh buttons must have the Separator style to enclose the group.

When the user clicks in the RichTextBox control, the SelChange event occurs. In that event, the code checks the SelAlignment property which returns the alignment of the current selection. That value is used to depress the appropriate button in the Toolbar control.

For More Information See "Style Property (Button Object)" for more information about the ButtonGroup style.

The following objects are used in this scenario:

  • Form object named "frmRTF"

  • Toolbar control named "tlbRTF"

  • RichTextBox control named "rtfData"

  • ImageList control named "imlToolbar"

To create a toolbar that sets text alignment

(The following operations are described at a high-level. For more specific information, see "Using the Toolbar Control.")

  1. At design time, insert the appropriate images into an ImageList control associated with the Toolbar control.

  2. Create the Button objects using the Property Pages dialog box.
  3. In the ButtonClick event, use the Select Case statement to determine which button was pressed, and set the alignment appropriate to the button.
  4. In the RichTextBox control's SelChange event, use the Select Case statement to reflect alignment.

Insert the Appropriate Images into an ImageList Control Associated with the Toolbar Control

As in the previous Toolbar Control Scenario, you must first insert the appropriate images for the button group into an ImageList control.

To add to the ImageList control at design time

  1. Right-click on the ImageList control and click Properties.

  2. In the Property Pages dialog box, click the Images tab.

  3. Click Insert Picture to display the Select Picture dialog box which allows you to find the images you need. This scenario uses images found in the Tools\Bitmaps\Tlbr_W95 directory. The images used are named: Lft.bmp, Cnt.bmp, and Rt.bmp.

  4. For each image you insert, type an appropriate key for the image in the Key box. In this scenario, the images and their keys are as follows:
    Image | Key
    Lft.bmp | "left"
    Cnt.bmp | "center"
    Rt.bmp | "right"

  5. Associate the ImageList with the Toolbar control by right-clicking the Toolbar control and clicking Properties, then selecting the ImageList control from the drop-down box ImageList.

Create the Button Objects Using the Property Pages Dialog Box

You can create Button objects at design time by using the Toolbar control's Property Pages dialog box. For specific information on how to do this, see "Toolbar Control Scenario 1: Provide an RTF Text Editor an OpenFile Button."

To create a group of three buttons, you must actually create five buttons, three of which are enclosed by two Button objects with the Separator style. The Style, Key, ToolTipText, and Image properties for the five buttons should be set as shown in the table below:

Button | Style | Key | ToolTipText | Image
1 | tbrSeparator | [none] | [none] | [none]
2 | tbrButtonGroup | left | Align Left | 1
3 | tbrButtonGroup | center | Center | 2
4 | tbrButtonGroup | right | Align Right | 3
5 | tbrSeparator | [none] | [none] | [none]

Use the Select Case Statement to Determine Which Button was Pressed, and Set the Alignment Appropriate to the Button

The ButtonClick event occurs when the user clicks on any Button object (except buttons with the Separator or Placeholder styles). Use the Select Case statement with the Key property to determine which button was clicked, and set the alignment of the RichTextBox using the SelAlignment property of the RichTextBox control. After setting the alignment, return the focus to the control by using a SetFocus method, as shown in the code below:

Private Sub tlbRTF_ButtonClick(ByVal Button _
As Button)
   ' Use the Select Case statement with the Key
   ' property to determine which button was clicked.
   Select Case Button.Key
   Case "left"
      rtfData.SelAlignment = rtfLeft
      rtfData.SetFocus ' Return focus to RichTextBox.
   Case "center"
      rtfData.SelAlignment = rtfCenter
      rtfData.SetFocus
   Case "right"
      rtfData.SelAlignment = rtfRight
      rtfData.SetFocus
   Case Else
      ' Handle other cases here.
   End Select
End Sub

Use the Select Case Statement to Reflect Alignment

When the user clicks on a line in the RichTextBox control, the Toolbar buttons will reflect the alignment of the line. This can be accomplished using the RichTextBox control's SelChange event, as shown in the code below.

Private Sub rtfData_SelChange()
   ' When the insertion point changes, set the Toolbar
   ' buttons to reflect the attributes of the line
   ' where the cursor is located.
   Select Case rtfData.SelAlignment
   Case rtfLeft ' 0
      tlbRTF.Buttons("left").Value = tbrPressed
   Case rtfRight ' 1
      tlbRTF.Buttons("right").Value = tbrPressed
   case Else ' Null—no buttons are shown pressed.
      tlbRTF.Buttons("left").Value = tbrUnpressed
      tlbRTF.Buttons("center").Value = tbrUnpressed
      tlbRTF.Buttons("right").Value = tbrUnpressed
   End Select
End Sub

Running the Code

You can now run the project. Use the Open File button (created in "Toolbar Scenario 1: Provide an RTF Text Editor with an OpenFile Button") to open a file. You can also click the three grouped buttons to change the alignment of the text.


Using the TreeView Control

The TreeView control is designed to display data that is hierarchical in nature, such as organization trees, the entries in an index, the files and directories on a disk.

Figure 2.40 Typical TreeView

Possible Uses

  • To create an organization tree that can be manipulated by the user.

  • To create a tree that shows at least two or more levels of a database.

Setting Node Object Properties

A "tree" is comprised of cascading branches of "nodes," and each node typically consists of an image (set with the Image property) and a label (set with the Text property). Images for the nodes are supplied by an ImageList control associated with the TreeView control. For more information on using the ImageList control with other controls, see "Using the ImageList control."

A node can be expanded or collapsed, depending on whether or not the node has child nodes — nodes which descend from it. At the topmost level are "root" nodes, and each root node can have any number of child nodes. The total number of nodes is not limited (except by machine constraints). Figure 2.41 shows a tree with two root nodes; "Root 1" has three child nodes, and "Child 3" has a child node itself. "Root 2" has child nodes, as indicated by the "+" sign, but is unexpanded.

Figure 2.41 Root and child nodes

Each node in a tree is actually a programmable Node object, which belongs to the Nodes collection. As in other collections, each member of the collection has a unique Index and Key property which allows you to access the properties of the node. For example, the code below uses the Index of a particular node ("7") to set the Image and Text properties:

tvwMyTree.Nodes(7).Image = "closed"
tvwMyTree.Nodes(7).Text = "IEEE"

However, if a unique key, for example "7 ID" had been assigned to the node, the same code could be written as follows:

tvwMyTree.Nodes("7 ID").Image = "closed"
tvwMyTree.Nodes("7 ID").Text = "IEEE"

Node Relationships and References to Relative Nodes

Each node can be either a child or a parent, depending on its relationship to other nodes. The Node object features several properties which return various kinds of information about children or parent nodes. For example, the following code uses the Children property to return the number of children — if any — a node has:

MsgBox tvwMyTree.Nodes(10).Children

However, some of the properties do not return information, as the Children property does, but instead return a reference to another node object. For example, the Parent property returns a reference to the parent of any particular node (as long as the node is not a root node). With this reference, you can manipulate the parent node by invoking any methods, or setting properties, that apply to Node objects. For example, the code below returns the Text and Index properties of a parent node:

MsgBox tvwMyTree.Nodes(10).Parent.Text
MsgBox tvwMyTree.Nodes(10).Parent.Index

Tip Use the Set statement with an object variable of type Node to manipulate references to other Node objects. For example, the code below sets a Node object variable to the reference returned by the Parent property. The code then uses the object variable to return properties of the relative node:

Dim tempNode As Node ' Declare object variable.
' Set object variable to returned reference.
Set tempNode = tvwMyTree.Nodes(10).Parent
MsgBox tempNode.Text ' Returns parent's Text.
MsgBox tempNode.Index ' Returns parent's Index.

Adding Node Objects to the Nodes Collection

To add a Node to the tree, use the Add method (Nodes collection). This method includes two arguments, relative and relationship, which can determine where the node will be added. The first argument relative names a node; the second argument relationship specifies the relationship between the new node and the node named in relative.

For example, the following code adds a node named "11 node" as a child of another node named "7 node." The intrinsic constant tvwChild specifies that the new node is a child of the node named in the previous argument. The third argument assigns the Key property to the new node.

tvwMyTree.Nodes.Add "7 node", tvwChild, "11 node"

Other possible relationships include:

Constant | Value | Description
tvwLast | 1 | The Node is placed after all other nodes at the same level of the node named in relative.
tvwNext | 2 | The Node is placed after the node named in relative.
tvwPrevious | 3 | The Node is placed before the node named in relative.
tvwChild | 4 | The Node becomes a child node of the node named in relative.

For example, suppose there were three existing nodes, and you wished to place a fourth node between the second and the third nodes, the code would be:

' Assuming the second node's Key value is "2 node".
tvwMyTree.Nodes.Add "2 node", tvwNext

Other arguments of the Add method are key, text, and image. Using these arguments, you can assign the Key, Text, and Image properties as the Node object is created.

For More Information For more information about the Nodes collection's Add method See "Add Method" by typing "Add Method" in the Index search and clicking "Add Method (Nodes Collection)."

A second way of adding nodes is to declare an object variable of type Node, and then use the Set statement with the Add method. The Set statement sets the object variable to the new node. You can then use the object variable to set the node's properties, as shown below:

Dim nodX As Node
Set nodX = tvwMyTree.Nodes.Add("10 node", tvwChild)
nodX.Key = "11 node"
nodX.Text = "IEEE"
nodX.Image = "closed"

Tip Using the Set statement with the Add method makes reading and debugging your code easier. However, using the Add method and its arguments to add nodes creates faster code.


TreeView Control Scenario: Binding the TreeView to the Biblio.MDB Database

TreeView Control Scenario: Bind the TreeView to the Biblio.mdb Database

Sample Application: DataTree.vbp

The code examples in this chapter are taken from the DataTree.vbp sample application which is listed in the Samples directory.

It is possible to bind the data from a database to a TreeView control. The following example binds a TreeView control to the Biblio database which is found on the Visual Basic CD. The scenario uses the Publishers table as the first level of tree nodes. If a publisher has one or more book titles, those titles are added to the tree as child nodes of the specific publisher.

Figure 2.42 Data-bound TreeView control

The following objects are used in the code below:

  • Data Access Object Library (3.5)

  • Form object named "frmDataTree"

  • TreeView control named "tvwDB"

  • CommandButton control named "cmdLoad"

To bind the Biblio.mdb Database to the TreeView control

  1. Add a reference to the Data Access Objects (DAO 3.0) to your project.

  2. Create module-level variables for the Database and Node objects.
  3. In the Form Load event, set the Database object variable to the Biblio database using the OpenDatabase statement.

  4. Create the top level node object using the Nodes collection’s Add method.
  5. In a CommandButton's Click event, create two Recordset variables and set them to the Publishers and Titles tables.

  6. Use the "Do Until" statement to create a Node object for each publisher in the table.

  7. For each publisher, check the PubID field in the Titles recordset for matches; add a child node for each match.

Add a Reference to the Data Access Objects (DAO 3.5) to Your Project

To bind a database to the TreeView control, you must first add a reference to the current version of Data Access Objects (DAO).

Create Module-Level Variables for the Database Object and Node Object

Because you will want to access the Biblio.mdb database several times during a single session, it's more efficient to keep a single copy of the database open by creating a module-level Database object. Thereafter, you can access the database without reopening it. In the Declarations section of the form, write:

Private mDbBiblio As Database

If you want the database to be used by other modules, use the Public statement, and rename the variable to reflect its global status, i.e., gDbBiblio.

When creating Node objects, use the Set statement (shown below) with a variable of type Node.

Dim TempNode As Node
Set TempNode = tvwDB.Nodes.Add()

While you can create the variable whenever you add Node objects, it is more efficient to declare a single module-level Node object variable once and use it to create all Node objects. Again in the Declarations section, write:

Private mNode As Node

Set the Database object variable to the Biblio database using the OpenDatabase statement

The Form object's Load event can be used to initialize the Database variable. The code for this would be:

Set mDbBiblio = DBEngine.OpenDatabase("BIBLIO.MDB")

After you have successfully initialized the Database object variable, you can freely access it from anywhere within the code module.

Form Load Event: Create the Top Level Node Object Using the Nodes Collection's Add Method

Now that the Database object variable has been initialized with the Biblio database, you may want to create the first node in the tree and assign it the name of the open database. You must first use the Node collection's Add method to create the first Node object. You should also use the Set statement with the mNode object variable, as shown below:

Set mNode = tvwDB.Nodes.Add() ' Create the first node.
mNode.Text = mDbBiblio.Name

Notice that in the code above, using the Set statement simultaneously created the Node while assigning it to the mNode object variable. Since the mNode variable now contains the newly created Node object, you can assign various properties to the new Node. In the above case, the name of the Database (i.e. the Database object's Name property) has been assigned to the new node's Text property.

CommandButton Click Event: Create Two Recordset Variables and Set Them to the Publishers and Titles Tables

The present scenario assumes that a button called "cmdLoad" exists, and that when the user clicks it, the TreeView control is populated with two tables from the Biblio database. To accomplish this task, you must first declare two DAO object variables in the button's Click event. The first variable, rsPublishers, will contain the Publishers table. The second, rsTitles, will contain the Titles table. The code below declares the two variables then uses the OpenRecordSet method to assign the tables to the variables:

Dim rsPublishers As Recordset
Dim rsTitles As Recordset

Set rsPublishers = mDbBiblio. _
OpenRecordset("Publishers", dbOpenDynaset)
Set rsTitles = mDbBiblio. _
OpenRecordset("titles", dbOpenDynaset)

Use the Do Until Statement to Create a Node Object for Each Publisher in the Table

Now that you have two open recordsets, you can iterate through each recordset, create a Node object, and assign an appropriate value to the object’s Text property. First, you must iterate through the Publishers table and create a Node object for each Publisher in the table.

The simplified code below can be stated as, "Do until the End of the Recordset: create a Node object and assign its Text property the value of the Title field; move to the next record and repeat":

Do Until rsPublishers.EOF
   Set mNode = tvwDB.Nodes.Add(1, tvwChild)
   mNode.Text = rsPublishers!Name
   rsPublishers.MoveNext
Loop

Note that in the Add method above, we used two arguments. The first argument (1) is the Index property of the Node to which we want to add a Child node. That is, we want all the Publisher nodes to be children of the first (root) node (which was created in the Form's Load event). The second argument uses a constant (tvwChild) that specifies the new Node will be a child node of the Node with index "1."

For Each Publisher, Check the PubID Field in the Titles Recordset for Matches; Add a Child Node for each Match

The code above will populate the first level of the TreeView with the contents of the Publishers table. However, we wish to go one level deeper and add child nodes to each Publisher node. Each child node will then represent a book that the Publisher prints.

To accomplish this, while we have the reference to the newly created Publisher node (mNode), we need to iterate through the Titles recordset and check every record's PubID field. If that field corresponds to the PubID field in the Publishers recordset, the book is published by our present publisher. But before we can add a node to mNode, we must first assign a variable (intIndex) the value of mNode's Index property, as shown below:

intIndex = mNode.Index

We can now use this variable in the Add method, which requires the Index property of the Node object to which a child node is being added:

Set mNode = tvwDB.Nodes.Add(intIndex, tvwChild)

The simplified code below can be stated as, "Do until the End of the Recordset: create a child Node object and assign its Text property the value of the Title field; move to the next record and repeat":

Do Until rsTitles.EOF
   If rsPublishers!PubID = rsTitles!PubID Then
      Set mNode = tvwDB.Nodes.Add(intIndex, tvwChild)
      mNode.Text = rsTitles!Title ' Text property.
   End If
Loop

Completing the Code

The code above shows the basic strategy for populating a table with two related tables. The complete code is shown below:

' Be sure to set References to DAO 3.5
' In the Declarations section, declare module-level
' object variables:
Private mDbBiblio As Database
Private mNode As Node

Private Sub Form_Load()
   ' In Form_Load event, set object variable and
   ' create first Node object of TreeView control.

   Set mDbBiblio = DBEngine.Workspaces(0). _
   OpenDatabase("BIBLIO.MDB")

   tvwDB.Sorted = True
   Set mNode = tvwDB.Nodes.Add()
   mNode.Text = "Publishers"
   mNode.Tag = mDbBiblio.Name   ' Set Tag property.
   mNode.Image = "closed"         ' Set Image
                                 ' property.
End Sub

Private Sub cmdLoad_Click()
   ' Declare DAO object variables and assign
   ' recordsets to them.
   Dim rsPublishers As Recordset
   Dim rsTitles As Recordset
   Set rsPublishers = mDbBiblio. _
   OpenRecordset("Publishers", dbOpenDynaset)
   Set rsTitles = mDbBiblio. _
   OpenRecordset("titles", dbOpenDynaset)

   ' Go to the first record.
   rsPublishers.MoveFirst

   Dim intIndex As Integer ' Variable for index.

   ' Do until the last record (EOF): add a Node
   ' object and use the Name field as the
   ' new Node object's text.
   Do Until rsPublishers.EOF
      Set mNode = tvwDB.Nodes.Add(1, tvwChild)
      mNode.Text = rsPublishers!Name
      mNode.Tag = "Publisher"   ' Tag identifies the
                                 ' table.
      ' Assign a unique ID to the Key
      mNode.Key = CInt(rsPublishers!PubID) & " ID"
      ' Set the variable intIndex to the Index
      ' property of the newly created Node. Use this
      ' variable to add child Node objects to the
      ' present Node.
      intIndex = mNode.Index
      ' While on this record, search the Title table
      ' for any occurence of the same PubID in the
      ' Titles recordset. If one is found, add a Node
      ' object to the TreeView control, and set the
      ' new Node object properties with the found
      ' record's Title, ISBN and Author fields.
      Do Until rsTitles.EOF
         If rsPublishers!PubID = rsTitles!PubID Then
            Set mNode = tvwDB.Nodes. _
            Add(intIndex, tvwChild)
            mNode.Text = rsTitles!Title   ' Text.
            mNode.Key = rsTitles!ISBN   ' Unique ID.
            mNode.Tag = "Authors"      ' Table name.
            mNode.Image = "leaf"      ' Image.
         End If
      rsTitles.MoveNext ' Next record in Titles.
      Loop
      ' Reset rsTitles to first Titles record.
      rsTitles.MoveFirst
      ' Move to next Publisher record.
      rsPublishers.MoveNext
   Loop
End Sub

Enhancing the Code

The example can be improved by using a SQL statement to create a smaller "Titles" recordset. The code below creates a recordset of only the records which have the same PubID value:

Set rsTitles = mDbBiblio.OpenRecordset _
("select * from Titles Where PubID = " & _
rsPublishers!PubID)

The code can then iterate through this smaller recordset more efficiently. The modified code is shown below:

Private Sub cmdLoad_Click()
   Dim rsPublishers As Recordset
   Dim rsTitles As Recordset
   Set rsPublishers = mDbBiblio. _
   OpenRecordset("Publishers", dbOpenDynaset)
   Dim intIndex
   Do Until rsPublishers.EOF
      Set mNode = tvwDB.Nodes.Add(1, tvwChild)
      mNode.Text = rsPublishers!Name
      mNode.Tag = "Publisher" ' Identifies the table.
      mNode.Key = rsPublishers!PubID & " ID"
      mNode.Image = "closed"
      intIndex = mNode.Index
      ' While on this record, create a recordset
      ' using a query that finds only titles that have
      ' the same PubID. For each record in the
      ' resulting recordset, add a Node object to the
      ' TreeView control, and set the new Node object
      ' properties with the record's Title, ISBN and
      ' Author fields.
      Set rsTitles = mDbBiblio.OpenRecordset _
      ("select * from Titles Where PubID = " & _
      rsPublishers!PubID)
      Do Until rsTitles.EOF
         Set mNode = tvwDB.Nodes. _
         Add(intIndex, tvwChild)
         mNode.Text = rsTitles!TITLE   ' Text.
         mNode.Key = rsTitles!ISBN      ' Unique ID.
         mNode.Tag = "Authors"      ' Table name.
         mNode.Image = "smlBook"      ' Image.
         ' Move to next record in rsTitles.
         rsTitles.MoveNext
      Loop
      ' Move to next Publishers record.
      rsPublishers.MoveNext
   Loop
End Sub


Using the UpDown Control

An UpDown control is a pair of arrow buttons that the user can click to increment or decrement a value, such as a scroll position or a number displayed in a buddy control. The buddy control can be any other kind of control, as long as it has a property that can be updated by the UpDown control.

To the user, an UpDown control and its buddy control often look like a single control. You can specify that an UpDown control automatically position itself next to its buddy control and that it automatically set the value of the buddy control to its current value. For example, you can use an UpDown control with a Textbox control to prompt the user for numeric input. The illustration below shows an UpDown control with a Textbox control as its buddy control, a combination that is sometimes referred to as a spinner control.

Possible Uses

  • To increment or decrement the value of a field in an application.

Basic Operation

To use the UpDown control, you must first set the BuddyControl property to another control, and the BuddyProperty to a scrollable property on the other control. The Min and Max properties determine the limits of the control, and the Wrap property determines if the value will wrap when the end user scrolls past the Min or Max values. The Increment property specifies the amount by which the Value property will be changed when either the up or down arrow is clicked.

Use the AutoBuddy Property to Set a Buddy Control Automatically

At design time, setting the AutoBuddy property to True causes the UpDown control to automatically "buddy" with the previous control in the TabOrder.

To automatically set a buddy control

  1. Draw the buddy control on the form.

  2. Draw the UpDown control on the form.

  3. Right-click the UpDown control and click Properties to display the Property Pages dialog box.

  4. Click Buddy to display the Buddy tab, as seen in Figure 2.43 below.

  5. Click the AutoBuddy check box to set the AutoBuddy property to True.

  6. Click the SyncBuddy check box to set the SyncBuddy property to True.

Figure 2.43 Buddy Tab on the UpDown control property pages

Determining the Scroll Behavior

The UpDown control includes several events and properties that allow you to determine how the control scrolls.

The Increment and Wrap Properties

The Wrap property determines how the control will behave when the end user holds down either the up or down button. If set to False, when the Max or Min value is reached, the controls ceases to scroll, and stops at the Max or Min value. When set to True, the control will return to either the Min or Max value and continue to increment (or decrement) through the values as determined by the Min and Max properties.

The Increment property specifies the amount by which the Value property will be changed when either the up or down arrow is clicked. This value cannot be a negative number. One use of this is to toggle a field or OptionButton between True (-1) and False (0), as shown in the code below:

Private Sub Form_Load()
   ' The UpDown control is named "updToggle."
   ' An OptionButton control is named "optToggle."
   With updToggle
      .BuddyControl = optToggle
      .Min = -1
      .Max = 0
      .Increment = 1
      .Wrap = True
   End With
End Sub

' Change the Value of the OptionButton with
' the UpDown control's Value property.
Private Sub updToggle_Change()
   optToggle.Value = updToggle.Value
End Sub

The UpClick and DownClick Events

Using the UpClick and DownClick events, you can control exactly how the UpDown control scrolls through a series of values. For example, if you want to allow the end user to scroll rapidly upward through the values, but slower going down through the values, you can set reset the Increment property to different values, as shown below:

Private Sub updScroll_UpClick()
   ' The name of the UpDown controls is "updScroll"
   updScroll.Increment = 5
End Sub

Private Sub updScroll_DownClick()
   ' When the user clicks the down button, the
   ' Increment switches to 1 for finer resolution.
   updScroll.Increment = 1
End Sub


Using the Winsock Control

A WinSock control allows you to connect to a remote machine and exchange data using either the User Datagram Protocol (UDP) or the Transmission Control Protocol (TCP). Both protocols can be used to create client and server applications. Like the Timer control, the WinSock control doesn't have a visible interface at run time.

Possible Uses

  • Create a client application that collects user information before sending it to a central server.

  • Create a server application that functions as a central collection point for data from several users.

  • Create a "chat" application.

Selecting a Protocol

When using the WinSock control, the first consideration is whether to use the TCP or the UDP protocol. The major difference between the two lies in their connection state:

  • The TCP protocol control is a connection-based protocol, and is analogous to a telephone — the user must establish a connection before proceeding.

  • The UDP protocol is a connectionless protocol, and the transaction between two computers is like passing a note: a message is sent from one computer to another, but there is no explicit connection between the two. Additionally, the maximum data size of individual sends is determined by the network.

The nature of the application you are creating will generally determine which protocol you select. Here are a few questions that may help you select the appropriate protocol:

  1. Will the application require acknowledgment from the server or client when data is sent or received? If so, the TCP protocol requires an explicit connection before sending or receiving data.

  2. Will the data be extremely large (such as image or sound files)? Once a connection has been made, the TCP protocol maintains the connection and ensures the integrity of the data. This connection, however, uses more computing resources, making it more "expensive."

  3. Will the data be sent intermittently, or in one session? For example, if you are creating an application that notifies specific computers when certain tasks have completed, the UDP protocol may be more appropriate. The UDP protocol is also more suited for sending small amounts of data.

Setting the Protocol

To set the protocol that your application will use: at design-time, on the Properties window, click Protocol and select either sckTCPProtocol, or sckUDPProtocol. You can also set the Protocol property in code, as shown below:

Winsock1.Protocol = sckTCPProtocol

Determining the Name of Your Computer

To connect to a remote computer, you must know either its IP address or its "friendly name." The IP address is a series of three digit numbers separated by periods (xxx.xxx.xxx.xxx). In general, it's much easier to remember the friendly name of a computer.

To find your computer's name

  1. On the Taskbar of your computer, click Start.

  2. On the Settings item, click the Control Panel.

  3. Double-click the Network icon.

  4. Click the Identification tab.

  5. The name of your computer will be found in the Computer name box.

Once you have found your computer's name, it can be used as a value for the RemoteHost property.

TCP Connection Basics

When creating an application that uses the TCP protocol, you must first decide if your application will be a server or a client. Creating a server means that your application will "listen," on a designated port. When the client makes a connection request, the server can then accept the request and thereby complete the connection. Once the connection is complete, the client and server can freely communicate with each other.

The following steps create a rudimentary server:

To create a TCP server

  1. Create a new Standard EXE project.

  2. Change the name of the default form to frmServer.

  3. Change the caption of the form to "TCP Server."

  4. Draw a Winsock control on the form and change its name to tcpServer.

  5. Add two TextBox controls to the form. Name the first txtSendData, and the second txtOutput.

  6. Add the code below to the form.
    Private Sub Form_Load()
' Set the LocalPort property to an integer.
' Then invoke the Listen method.
tcpServer.LocalPort = 1001
tcpServer.Listen
frmClient.Show ' Show the client form.
End Sub

Private Sub tcpServer_ConnectionRequest _
(ByVal requestID As Long)
' Check if the control's State is closed. If not,
' close the connection before accepting the new
' connection.
If tcpServer.State <> sckClosed Then _
tcpServer.Close
' Accept the request with the requestID
' parameter.
tcpServer.Accept requestID
End Sub

Private Sub txtSendData_Change()
' The TextBox control named txtSendData
' contains the data to be sent. Whenever the user
' types into the  textbox, the  string is sent
' using the SendData method.
tcpServer.SendData txtSendData.Text
End Sub

Private Sub tcpServer_DataArrival _
(ByVal bytesTotal As Long)
' Declare a variable for the incoming data.
' Invoke the GetData method and set the Text
' property of a TextBox named txtOutput to
' the data.
Dim strData As String
tcpServer.GetData strData
txtOutput.Text = strData
End Sub

The procedures above create a simple server application. However, to complete the scenario, you must also create a client application.

To create a TCP client

  1. Add a new form to the project, and name it frmClient.

  2. Change the caption of the form to TCP Client.

  3. Add a Winsock control to the form and name it tcpClient.

  4. Add two TextBox controls to frmClient. Name the first txtSend, and the second txtOutput.

  5. Draw a CommandButton control on the form and name it cmdConnect.

  6. Change the caption of the CommandButton control to Connect.

  7. Add the code below to the form.

Important Be sure to change the value of the RemoteHost property to the friendly name of your computer.

Private Sub Form_Load()
' The name of the Winsock control is tcpClient.
' Note: to specify a remote host, you can use
' either the IP address (ex: "121.111.1.1") or
' the computer's "friendly" name, as shown here.
tcpClient.RemoteHost = "RemoteComputerName"
tcpClient.RemotePort = 1001
End Sub

Private Sub cmdConnect_Click()
' Invoke the Connect method to initiate a
' connection.
tcpClient.Connect
End Sub

Private Sub txtSendData_Change()
tcpClient.SendData txtSend.Text
End Sub

Private Sub tcpClient_DataArrival _
(ByVal bytesTotal As Long)
Dim strData As String
tcpClient.GetData strData
txtOutput.Text = strData
End Sub

The code above creates a simple client-server application. To try the two together, run the project, and click Connect. Then type text into the txtSendData TextBox on either form, and the same text will appear in the txtOutput TextBox on the other form.

Accepting More than One Connection Request

The basic server outlined above accepts only one connection request. However, it is possible to accept several connection requests using the same control by creating a control array. In that case, you do not need to close the connection, but simply create a new instance of the control (by setting its Index property), and invoking the Accept method on the new instance.

The code below assumes there is a Winsock control on a form named sckServer, and that its Index property has been set to 0; thus the control is part of a control array. In the Declarations section, a module-level variable intMax is declared. In the form's Load event, intMax is set to 0, and the LocalPort property for the first control in the array is set to 1001. Then the Listen method is invoked on the control, making it the "listening control. As each connection request arrives, the code tests to see if the Index is 0 (the value of the "listening" control). If so, the listening control increments intMax, and uses that number to create a new control instance. The new control instance is then used to accept the connection request.

Private intMax As Long

Private Sub Form_Load()
   intMax = 0
   sckServer(0).LocalPort = 1001
   sckServer(0).Listen
End Sub

Private Sub sckServer_ConnectionRequest _
(Index As Integer, ByVal requestID As Long)
   If Index = 0 Then
      intMax = intMax + 1
      Load sckServer(intMax)
      sckServer(intMax).LocalPort = 0
      sckServer(intMax).Accept requestID
      Load txtData(intMax)
   End If
End Sub

UDP Basics

Creating a UDP application is even simpler than creating a TCP application because the UDP protocol doesn't require an explicit connection. In the TCP application above, one Winsock control must explicitly be set to "listen," while the other must initiate a connection with the Connect method.

In contrast, the UDP protocol doesn't require an explicit connection. To send data between two controls, three steps must be completed (on both sides of the connection):

  1. Set the RemoteHost property to the name of the other computer.

  2. Set the RemotePort property to the LocalPort property of the second control.

  3. Invoke the Bind method specifying the LocalPort to be used. (This method is discussed in greater detail below.)

Because both computers can be considered "equal" in the relationship, it could be called a peer-to-peer application. To demonstrate this, the code below creates a "chat" application that allows two people to "talk" in real time to each other:

To create a UDP Peer

  1. Create a new Standard EXE project.

  2. Change the name of the default form to frmPeerA.

  3. Change the caption of the form to "Peer A."

  4. Draw a Winsock control on the form and name it udpPeerA.

  5. On the Properties page, click Protocol and change the protocol to UDPProtocol.

  6. Add two TextBox controls to the form. Name the first txtSend, and the second txtOutput.

  7. Add the code below to the form.
    Private Sub Form_Load()
' The control's name is udpPeerA
With udpPeerA
' IMPORTANT: be sure to change the RemoteHost
' value to the name of your computer.
.RemoteHost= "PeerB"
.RemotePort = 1001   ' Port to connect to.
.Bind 1002                ' Bind to the local port.
End With
frmPeerB.Show                 ' Show the second form.
End Sub

Private Sub txtSend_Change()
' Send text as soon as it's typed.
udpPeerA.SendData txtSend.Text
End Sub

Private Sub udpPeerA_DataArrival _
(ByVal bytesTotal As Long)
Dim strData As String
udpPeerA.GetData strData
txtOutput.Text = strData
End Sub

To create a second UDP Peer

  1. Add a standard form to the project.

  2. Change the name of the form to frmPeerB.

  3. Change the caption of the form to "Peer B."

  4. Draw a Winsock control on the form and name it udpPeerB.

  5. On the Properties page, click Protocol and change the protocol to UDPProtocol.

  6. Add two TextBox controls to the form. Name the TextBox txtSend, and the second txtOutput.

  7. Add the code below to the form.
    Private Sub Form_Load()
' The control's name is udpPeerB.
With udpPeerB
' IMPORTANT: be sure to change the RemoteHost
' value to the name of your computer.
.RemoteHost= "PeerA"
.RemotePort = 1002    ' Port to connect to.
.Bind 1001                ' Bind to the local port.
End With
End Sub

Private Sub txtSend_Change()
' Send text as soon as it's typed.
udpPeerB.SendData txtSend.Text
End Sub

Private Sub udpPeerB_DataArrival _
(ByVal bytesTotal As Long)
Dim strData As String
udpPeerB.GetData strData
txtOutput.Text = strData
End Sub

To try the example, press F5 to run the project, and type into the txtSend TextBox on either form. The text you type will appear in the txtOutput TextBox on the other form.

About the Bind Method

As shown in the code above, you must invoke the Bind method when creating a UDP application. The Bind method "reserves" a local port for use by the control. For example, when you bind the control to port number 1001, no other application can use that port to "listen" on. This may come in useful if you wish to prevent another application from using that port.

The Bind method also features an optional second argument. If there is more than one network adapter present on the machine, the LocalIP argument allows you to specify which adapter to use. If you omit the argument, the control uses the first network adapter listed in the Network control panel dialog box of the computer's Control Panel Settings.

When using the UDP protocol, you can freely switch the RemoteHost and RemotePort properties while remaining bound to the same LocalPort. However, with the TCP protocol, you must close the connection before changing the RemoteHost and RemotePort properties.


Creating ActiveX Components

Component software development cuts programming time and produces more robust applications, by allowing developers to assemble applications from tested, standardized components. The move to component software, sparked by the success of products like Microsoft Visual Basic, is one of the most prominent trends in the software industry.

Microsoft has led the effort to define an open, extensible standard for software interoperability. The Component Object Model (COM), including Automation and the ActiveX specification, makes it possible for software components you create to work smoothly with software components you buy off the shelf.

Visual Basic makes ActiveX component creation happen. ActiveX controls, ActiveX Documents, code components, and applications that provide objects — Visual Basic gives you the tools to rapidly create, debug, and deploy software components.

Getting Started

Before you begin, you should know how to use class modules to define new classes, how to create objects from classes, and how to use ActiveX components. These subjects are discussed in "Programming with Objects" and "Programming with ActiveX Components," in the Visual Basic Programmer’s Guide.

Chapters

ActiveX Components

Outlines what you can do with Visual Basic’s component software features.
Creating an ActiveX DLL

Provides step by step procedures that get you off to a running start with in-process debugging, multiple projects, global objects, object lifetime concepts, and circular references.
Creating an ActiveX Exe Component

Provides step by step procedures that introduce threads, events in class modules, call-backs, and out-of-process debugging.
Creating an ActiveX Control

Provides a series of step by step procedures that create a simple control. Introduces ActiveX control concepts, raising events, debugging design-time behavior, and property pages.
Creating an ActiveX Document

Provides a series of step by step procedures that create a simple ActiveX document, and demonstrate ActiveX document concepts, navigation, properties and methods, menus, and debugging techniques.
General Principles of Component Design

Contains information of importance to all component designers, including terminology, concepts, instancing for class modules, polymorphism, and object models.
Debugging, Testing, and Deploying Components

Contains more general information, such as setting up test projects, debugging features, adding Help, version compatibility, and localization.
Building Code Components

Takes OLE servers into a new world, providing in depth discussions of in-process and out-of-process components, threading, instancing, call-backs, and events.
Building ActiveX Controls

Contains in-depth explanations of how Visual Basic ActiveX controls work, what features you can implement, subtleties of debugging, discussions of implementation techniques, and all the other things you expect of a cool new feature.
Creating Property Pages for ActiveX Controls

Provides in-depth discussion of property pages, including implementation techniques and design guidelines.
Building ActiveX Documents

Provides in-depth discussions of terminology, concepts, Internet features, navigation, debugging, migrating from forms, and in-process vs. out-of-process implementation.
Building Data Sources

Provides in-depth discussion and step-by-step procedures for creating ActiveX components that can act as data sources.

ActiveX Component Standards and Guidelines

Contains updated guidelines for object naming, component shutdown, implementing collections and Application objects, and other object model issues.


ActiveX Components

An ActiveX component is a unit of executable code, such as an .exe, .dll, or .ocx file, that follows the ActiveX specification for providing objects. ActiveX technology allows programmers to assemble these reusable software components into applications and services.

You can buy ActiveX components that provide generic services, such as numerical analysis or user interface elements. You can create components that encapsulate your own business transactions, and combine these with generic components. Reusing tested, standardized code in this fashion is called component software development.

Component software development using ActiveX technology should not be confused with object-oriented programming (OOP). OOP is a way to build object-based software components; ActiveX is a technology that allows you to combine object-based components created using many different tools. To put it another way, OOP is concerned with creating objects, while ActiveX is concerned with making objects work together.

For example, you can use an OOP tool such as Microsoft Visual C++® to construct a set of useful objects. These objects can be used and further extended by other C++ developers. If you package your objects in an ActiveX component, however, they can be used and further extended with any programming tool that supports ActiveX technology.

This chapter introduces the components you can build with Microsoft Visual Basic, and lists the portions of Creating ActiveX Components that apply to each.

Topics

They’re Not Just OLE Servers Any More

You can now create a variety of software components using Visual Basic.
Features of Visual Basic ActiveX Components

Lists key features Visual Basic provides for creating first-class components.
ActiveX Controls

Describes the main features of ActiveX controls created using Visual Basic.
ActiveX Documents

Introduces ActiveX documents, and explains how they are used in Internet-enabled applications.
Code Components

Code components provide objects other applications and components can create and use. This topic describes the features of Visual Basic code components.

Readers of this book should be familiar with the material in "Programming with Objects" and "Programming with ActiveX Components" in the Visual Basic Programmer’s Guide.


They're Not Just OLE Servers Any More

They’re Not Just OLE Servers Any More

Thread-safe DLLs. ActiveX controls. Scalable components for operation on remote computers. ActiveX documents. Native code. If you’ve created OLE servers with Visual Basic in the past, get ready for some surprises.

In addition to developing new kinds of software components, Visual Basic now supports the Component Object Model (COM) vision of component software evolution, by allowing you to implement multiple interfaces on class modules.

In keeping with the broad set of new features outlined in this chapter, Visual Basic terminology is shifting as well. They’re components now, not just OLE servers.

The new terminology may seem disconcerting at first, but in the long run it will make a lot more sense, as Visual Basic becomes the fastest, easiest way to develop robust, interoperable software components. You’ll also find it a lot easier to discuss interactions between components without the overworked word "server."

And if you really want to, you can continue to use the old terminology. Just pronounce "ActiveX component" as oh-lay ser-ver.


Features of Visual Basic ActiveX Components

With Visual Basic you can create components ranging from code libraries to Automation-enabled applications. You can create and distribute ActiveX control packages, with full licensing capability, or Internet applications with ActiveX documents that can display themselves in Internet browsers. With the Enterprise Edition of Visual Basic, you can run code components — such as business rule servers — on remote computers.

Figure 1.1 shows a Visual Basic application that uses several components, all of them built with Visual Basic.

Figure 1.1 Visual Basic application using components

Visual Basic ActiveX components are Internet- and intranet-enabled. Figure 1.2 shows a business analyst viewing a form on his desktop computer at the home office, while a sales representative views the same information through a Web page, using a Visual Basic ActiveX document.

Figure 1.2 Visual Basic activates the Internet

Which Type of Component Should I Build?

With all the different types of ActiveX components to choose from, how do you decide which type of component is best for a given situation? It may help to think of them in terms of functionality:

  • If you need an invisible component that provides a service, you’ll want to build a code component — either an ActiveX Exe or an ActiveX DLL.
    • If you need a component that can run in the same process with your application, you’ll want an ActiveX DLL. An example of this would be a component that performs complex calculations.

    • If you need a component that can serve multiple applications and can run on a remote computer, you’ll want an ActiveX Exe. For example, a business rules server that enforces tax rules would best be implemented as an ActiveX Exe.
  • If you need a visible component that can be dropped into an application at design time, you’ll want to build an ActiveX control. An example of this might be a phone number control that properly formats and validates phone numbers; such a control would undoubtedly be useful in many applications.

  • If you need a visible component that can take over an application window at run time, choose an ActiveX document. The above example shows a sales application that can be viewed in a Web browser, duplicating the functionality of a desktop application.

No matter which type of ActiveX component you choose to build, Visual Basic makes reusability a reality.

Features for Building Components

The following are some of the features Visual Basic provides for creating software components.

  • Components can provide several types of objects, as described in the related topics "ActiveX Controls," "ActiveX Documents," "Code Components," and "Applications that Provide Objects."

  • Objects provided by components can raise events. You can handle these events in a host process or in another application — with the Enterprise Edition, such an application can even be running on a remote computer.

  • Components can be data-aware, binding directly to any source of data without the need for a data control. You can also create an ActiveX component that acts as a data source to which other objects can bind. For example, you could create your own data control (similar to the ADO Data control or the Remote Data control), but instead of binding via ADO or RDO you could bind to a flat file or a proprietary binary data format.

  • Friend functions allow the objects provided by a component to communicate with each other internally, without exposing that communication to applications that use those objects.

  • The Implements keyword lets you add standard interfaces to your objects. These common interfaces enable polymorphic behavior for objects provided by a component, or for objects provided by many different components.

  • You can use enumerations to provide named constants for all component types.

  • You can choose a default property or method for each class of object your component provides.

  • You can allow users of your component to access the properties and methods of a global object without explicitly creating an instance of the object.

These features are discussed in detail in "General Principles of Component Design," along with other subjects relevant to all component types.

For More Information Components that perform computation-intensive tasks may benefit from compilation to native code. This subject is covered in "More About Programming" in the Visual Basic Programmer’s Guide.


ActiveX Controls

ActiveX controls, formerly called OLE controls, are standard user interface elements that allow you to assemble forms and dialog boxes rapidly. ActiveX controls also bring the Internet alive, adding compelling new functionality to World Wide Web pages.

Visual Basic has always featured a variety of controls that you could use in your applications. Now you can create your own controls, for use with Visual Basic and other development tools. With ActiveX controls, your investment in Visual Basic carries forward to the Internet.

Designing an ActiveX control can be as easy as designing a Visual Basic form — you can use familiar Visual Basic graphics commands to paint your control, or create a control group using existing controls.

ActiveX controls can be debugged in process, so you can step directly from the code for your test form into the code for your ActiveX control project.

You can add data-binding to your Visual Basic ActiveX control, so that a user can easily bind the individual fields within the control to the appropriate fields in a database or other source of data. You can also use data-binding to create an ActiveX control that other controls can bind to, similar to the Visual Basic Data control.

Visual Basic makes it easy to create polished ActiveX control packages by adding property pages, named constants, and events to your controls.

You can compile your ActiveX controls directly into your application’s executable, or into .ocx files that can be used with development tools such as Visual Basic and Microsoft Visual C++, with end user products such as Microsoft Office, and on the Internet.

"Creating an ActiveX Control" offers a quick step-by-step introduction to control creation — including the radical idea of running code at design time to debug your control’s design-time behavior.

For More Information "Building ActiveX Controls" and "Creating Property Pages for ActiveX Controls" discuss control creation issues in depth. "General Principles of Component Design" and "Debugging, Testing, and Deploying Components" also contain many topics of interest to control designers.


ActiveX Documents

One of the most exciting features of Visual Basic is the ability to create ActiveX documents, forms that can appear within Internet browser windows. Visual Basic ActiveX documents offer built-in viewport scrolling, Hyperlinks, and menu negotiation.

ActiveX documents are designed the same way you design Visual Basic forms. They can contain insertable objects, such as Microsoft Excel pivot tables. They can also show message boxes and secondary forms.

Visual Basic ActiveX documents can also appear in the Microsoft Office Binder, and you can write code to save your document’s data in Binder data files.

With Internet Explorer 3.0 or later, you can also save properties of your ActiveX document by writing to and reading from the document's data file.

You can package ActiveX documents in either in-process or out-of-process components.

"Creating an ActiveX Document" is a step-by-step introduction to creating ActiveX documents.

For More Information ActiveX document design is covered in detail in "Building ActiveX Documents." You can find out more about how to use documents in your Internet applications in the Building Internet Applications section of the Component Tools Guide. Topics of general interest to component designers can be found in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components."


Code Components

Code components are like libraries of objects. A client application uses a code component by creating an object from one of the classes the component provides, and invoking the object’s properties, methods, and events.

Note In earlier documentation, code components were referred to as OLE Automation servers.

You might create a code component that provides extended financial functions for spreadsheet users, or you might encapsulate business rules used by your organization, so that all applications process the same data consistently.

With Visual Basic, you can build code components to run in process, allowing faster access to their objects, or out of process, so that they have separate threads of execution from their clients.

You can use code components to provide standard libraries of modal and modeless dialogs — although frequently a code component will have no user interface.

Visual Basic allows you to suppress all forms of user interaction, even system alert messages, and to record such events in the system log. This unattended execution capability is extremely useful for code components designed to run on remote computers.

In an out-of-process code component that has been designed for unattended execution, you can mark a class as multithreaded. Each object created from such a class can be on a separate thread of execution within the out-of-process component.

To simplify the creation of reusable code libraries that can also be used with desktop tools (such as the Microsoft Office suite), Visual Basic allows you to mark objects in a code component as global, so that their methods can be invoked without explicitly creating an instance of the global object.

Whatever you used to call them, it’s a whole new ball game.

For More Information A step-by-step introduction to in-process code components, including in-process debugging, can be found in "Creating an ActiveX DLL." "Creating an ActiveX Exe Component" gives a similar introduction to the creation and debugging of out-of-process code components. Advanced topics are covered in "Building Code Components." Topics of general interest to component designers can be found in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components."


Creating an ActiveX DLL

Components provide reusable code in the form of objects. An application that uses a component’s code, by creating objects and calling their properties and methods, is referred to as a client.

Components can run either in-process or out-of-process with respect to the clients that use their objects. An in-process component, or ActiveX DLL, runs in another application’s process. The client may be the application itself, or another in-process component that the application is using.

Figure 2.1 In-process components are used by applications or other in-process components

The series of step-by-step procedures in this chapter builds an in-process component called ThingDemo, with class modules that demonstrate object lifetime, global objects, and objects that control modal and modeless dialog boxes.

You’ll also see how to debug an ActiveX DLL in process, by running the DLL and a test project together in the Microsoft Visual Basic development environment.

The procedures for creating the ThingDemo component build on each other. The sequence in which you perform the procedures is therefore important.

Topics

Creating the ThingDemo DLL Project

Shows how to set the project options for an in-process component.
Creating Properties and Methods for the Thing Class

Adds two properties and a method.
Creating the TestThing Test Project

Creates a project group, consisting of ThingDemo and a test project, so you can debug the DLL project in process.
Creating and Testing Thing Objects

Adds code to the TestThing project to create instances of the Thing object and invoke its properties and methods.
Running the TestThing Test Application

Demonstrates the life cycle of object supplied by an in-process component.
Circular References and Object Lifetime

Explores object life cycles further, illustrating the effects of circular references on component shutdown.
Adding a Form to the ThingDemo Project

Adds code to show a form as either a modal or modeless dialog, using a global class to control the form.
Using the Global Object in TestThing

Tests the global object and dialogs, and explains more about debugging DLLs in-process.
Compiling and Testing the ThingDemo DLL

Compiles the project. Shows how to test the .dll file with the test project, and how to include it in another project.
Circular References and Component Shutdown

Shows how Visual Basic unloads an in-process component after the client has released all references to its objects. Also shows how circular references can prevent this unloading.
ActiveX DLL Creation Summary

Outlines the typical process of designing and creating an in-process component.

These procedures will be easier to follow if you set up your development environment as described below.

Before You Begin

  1. On the View menu, click Toolbox to open the Toolbox.

  2. On the View menu, click Project Explorer to open the Project Explorer window. The Project Explorer window will be used extensively to switch between project files.

  3. If the Project Explorer window is in Folder view, as shown below, click the Toggle Folders button on the Project Explorer window toolbar to turn the folders off.

  4. On the View menu, click Properties Window to open the Properties window.

  5. On the Tools menu, click Options to open the Options dialog box.

    Select the Editor tab, and make sure Require Variable Declaration is checked. This makes it much easier to catch typing errors.

    Select the Environment tab. Make sure Prompt To Save Changes is checked, then click OK. This will make it easy to save changes to the project as you go along.


Creating the ThingDemo DLL Project

How easy is it to create an ActiveX DLL with Visual Basic? If you can declare variables and write procedures, you can create an in-process component.

This section provides step-by-step instructions on how to define a simple class, and demonstrates the life cycle of objects provided by components. You can use objects created from this class with any application that can use Automation to control objects.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To create the ThingDemo project

  1. On the File menu, click New Project.

  2. In the New Project dialog box, double-click the ActiveX DLL icon. Visual Basic automatically adds a class module, Class1, to the new project.

  3. Press F4 to open the Properties window. Double-click the Name property and change it to Thing. This is the name you’ll use to create objects from the class.

    The default value for the Instancing property is MultiUse. This allows clients to create multiple instances of the Thing class. For a full discussion of the Instancing property, see "Instancing for Classes Provided by ActiveX Components," in "General Principles of Component Design."

  4. On the Project menu, click Project1 Properties to open the Project Properties dialog box. Select the General tab, fill out the information shown below, then click OK.

    The project name, ThingDemo, is also used as the name of the component’s type library. It can be combined with the name of each class the component provides, to produce unique class names.

    If two components each provide a Thing class, the fully qualified class name lets you specify which component’s Thing class you want to use, for example, ThingDemo.Thing.

  5. On the Project menu, click Add Module to open the Add Module dialog box. Double click the Module icon to add a module to the project.

    Note If you’ve used the Options dialog box (accessed from the Tools menu) to disable the Add Module dialog box, you’ll just get the module. This is okay.

  6. In the Code window for the module, add the following code:
    Option Explicit
Public gdatServerStarted As Date

Sub Main()
   ' Code to be executed when the component starts,
   '   in response to the first object request.
   gdatServerStarted = Now
   Debug.Print "Executing Sub Main"
End Sub

' Function to provide unique identifiers for objects.
Public Function GetDebugID() As Long
   Static lngDebugID As Long
   lngDebugID = lngDebugID + 1
   GetDebugID = lngDebugID
End Function
  7. On the File menu, click Save Project to save the project files, using the following names. Visual Basic will provide the extensions automatically.
    File | File name | Extension
    Module | ThingDemo_Module1 | .bas
    Class module | ThingDemo_Thing | .cls
    Project | ThingDemo | .vbp

For More Information See "Choosing a Project Type and Setting Project Properties" in "General Principles of Component Design."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Creating Properties and Methods for the Thing Class
Start from the beginning | Creating an ActiveX DLL.


Creating Properties and Methods for the Thing Class

You create properties for a class by adding public variables and property procedures to the class module. You create methods for a class by adding Public Sub and Public Function procedures to the class module. The following step-by-step procedures create two properties and one method for the Thing class.

  • The Name property is a string that can be retrieved and set by client applications.

  • The read-only DebugID property returns a sequence number that shows the order in which Thing objects were created. This is useful for debugging.

  • The ReverseName method reverses the order of the letters in the Name property.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To create the Name property

  • Add the following code to the Declarations section of the Thing class module:
    Option Explicit
Public Name As String

The variable Name becomes a property of the Thing class because it’s declared Public.

Important Don’t confuse the Name property you’re creating here with the Name property of the class module. The Name property of the class module allows you to specify the class name (Thing) at design time; it’s not available at run time.

To create the read-only DebugID property

  1. Add the following code to the Declarations section of the Thing class module:
    ' To store the value of the DebugID property.
Private mlngDebugID As Long
  2. From the Tools menu, choose Add Procedure to open the AddProcedure dialog box. In the Name box, type DebugID. Click Property and Public, then click OK.

  3. In the Code window, delete the Property Let procedure and change the Property Get procedure as follows:
    Public Property Get DebugID() As Long
   DebugID = mlngDebugID
End Property

    The purpose of a Property Let procedure is to allow users to assign a new value to the DebugID property. Deleting it makes the property read-only.

Tip Because property procedures come in two parts, you may find it easier to work with them in Full Module View. You can toggle between Procedure View and Full Module View using the buttons in the bottom left corner of the Code window.

The variable mlngDebugID is a private data member which is used to store the value of the DebugID property. Because it’s declared Private, it’s not visible to client applications, and thus cannot be changed by clients. This is an example of encapsulation, discussed in detail in "Classes: Putting User-Defined Types and Procedures Together," in the Visual Basic Programmer’s Guide.

The Property Get procedure returns the value of the private variable, allowing clients to read the property value using code like the following:

Private Sub Command1_Click()
   Dim t As ThingDemo.Thing
   Set t = New ThingDemo.Thing
   MsgBox t.DebugID
End Sub

To create the ReverseName method

  1. On the Tools menu, click Add Procedure to get the Add Procedure dialog box. In the Name box, type ReverseName. Click Sub and Public, then click OK.

  2. Enter the following code in the new Sub procedure of the Thing class module:
    Public Sub ReverseName()
   Dim intCt As Integer
   Dim strNew As String
   For intCt = 1 To Len(Name)
      strNew = Mid$(Name, intCt, 1) & strNew
   Next
   Name = strNew
End Sub

Note Remember that the Name property you’ve just created — and used in the ReverseName method — is not the same as the Name property of the class module. Unlike the Name property you created, the class module’s Name property is not available at run time.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Adding Code for Initialize and Terminate Events
Start from the beginning | Creating an ActiveX DLL.


Adding Code for Initialize and Terminate Events

Class modules have two built-in events: Initialize and Terminate. The code you place in the Initialize event procedure is the first code executed when the object is created, before any properties are set or any methods are executed.

The code you place in the Terminate event is executed when all references to the object have been released, and the object is about to be destroyed.

The following procedure adds code to support the DebugID property, and Debug.Print methods that will display the object’s properties when it’s being created and destroyed.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To add code to the Initialize and Terminate events of the Thing class

  1. In the Object box of the Thing class module, select Class. The Initialize event appears in the Procedure box, and the Code window displays the code template for the event procedure. Add the following code to the event procedure:
    Private Sub Class_Initialize()
   ' Get a debug ID number that can be returned by
   '   the read-only DebugID property.
   mlngDebugID = GetDebugID
   Debug.Print "Initialize Thing " & DebugID _
      & ", Name=" & Name
End Sub
  2. In the Procedure box of the class module, select Terminate. Add the following code to the event procedure:
    Private Sub Class_Terminate()
   On Error Resume Next
   Debug.Print "Terminate Thing " & DebugID _
      & ", Name=" & Name
End Sub

    Important You should always handle errors in the Class_Terminate event procedure. Errors in Class_Terminate cannot be handled by applications that use your component, and will therefore be fatal to the application.

    By contrast, unhandled errors in the Initialize event are raised at the point where the application created the object, and thus can be handled by the application.

Normally, the Initialize event procedure contains any code that needs to be executed at the moment the object is created, such as providing the time stamp for the DebugID property. The Terminate event contains any clean-up code you need to execute when the object is being destroyed.

Important The Initialize and Terminate events should never interact with the user. For demonstration purposes, this example uses the two events to give you a visual indication that a Thing object is being created or destroyed.

For More Information See "Coding Robust Initialize and Terminate Events" in "General Principles of Component Design."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Creating the TestThing Test Project
Start from the beginning | Creating an ActiveX DLL.


Creating the TestThing Test Project

In order to test the ThingDemo component, you need a test project. The test project creates instances of the classes a component provides, and exercises their properties, methods, and events.

To enable debugging of in-process components, Visual Basic allows you to load two or more projects into a project group. In addition to enabling in-process debugging, the project group makes it easier to load your component project and test project.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To add a test project to the project group

  1. On the File menu, click Add Project to open the Add Project dialog box.

    Important Do not click Open Project or New Project, as these will close your ActiveX DLL project.

  2. Double-click the Standard EXE icon to add an ordinary EXE project. You can now see both projects in the Project Explorer window, and the caption of the Project Explorer window shows the default project group name.

    Notice that ThingDemo appears in bold-face type in the Project Explorer window. This means ThingDemo is the Startup project — that is, the project that will be run when you press f5.

  3. In the Project Explorer window, right-click on Project1 to open the context menu. Click Set as Start Up to make the EXE project the Startup project for the project group.

    Important The test project must be the Startup project for the group. If the DLL project is the Startup project for the group, the test project will never be run.

  4. On the Project menu, click References to open the References dialog box. Check ThingDemo, then click OK.

    If ThingDemo doesn’t appear in the list of available references, click Cancel to close the dialog box. In the Project Explorer window click Project1 to make it the active project, and then repeat Step 3.

    When working with project groups, make sure you have the right project active before clicking the Project menu.

  5. Press F2 to open the Object Browser. In the Project/Library list, select ThingDemo to show its modules and classes. You can use the Object Browser to examine the Thing object and its properties and methods.

    Click Close to close the Object Browser when you’re done.

  6. On the File menu, click Save Project Group to save the test project and the project group. Name the files as shown below. Name the test project ThingTest. Name the project group ThingDemo. Visual Basic will give the project group file a .vbg extension.
    File | File name | Extension
    Form | ThingTest_Form1 | .frm
    Project | ThingTest | .vbp
    Project group | ThingDemo | .vbg

For More Information Test projects for DLLs are discussed in more detail in "Creating a Test Project for an In-Process Component" in "Debugging, Testing, and Deploying Components."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Creating and Testing Thing Objects
Start from the beginning | Creating an ActiveX DLL


Creating and Testing Thing Objects

To test a class — in this case, the Thing class — your test project must request that the component create an object from the class.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

Resize Form1 in the test project and place five command buttons and a check box on it, as shown in Figure 2.2. If you have never created a form in Visual Basic, see "Developing an Application in Visual Basic" in the Visual Basic Programmer’s Guide.

Figure 2.2 The test form for the Thing class

The following table lists the property values you need to set.

Object | Property | Setting
Form | Caption | Thing Demo
Command1 | Caption | Create New Thing
Command2 | Caption | Show the Thing
Command3 | Caption | Reverse the Thing’s Name
Command4 | Caption | Release the Thing
Command5 | Caption | Temporary Thing
Check1 | Caption | Stuck on itself

To add code to create a Thing and call its properties and methods

  1. Place the following code in the Declarations section of the test application’s form module:
    Option Explicit
' Reference to a Thing object.
Private mthTest As Thing
  2. In the Object box, select Command1. Add the following code to the Click event procedure:
    ' Button "Create New Thing".
Private Sub Command1_Click()
   Set mthTest = New Thing
   mthTest.Name = _
      InputBox("Enter a name for the Thing", _
      "Test Thing")
End Sub
  3. In similar fashion, add the following code to the Click event procedures of Command2, Command3, and Command4:
    ' Button "Show the Thing".
Private Sub Command2_Click()
   MsgBox "Name: " & mthTest.Name, , _
      "Thing " & mthTest.DebugID
End Sub

' Button "Reverse the Thing's Name".
Private Sub Command3_Click()
   mthTest.ReverseName
   ' Click "Show the Thing" by setting its Value.
   Command2.Value = True
End Sub

' Button "Release the Thing".
Private Sub Command4_Click()
   Set mthTest = Nothing
End Sub

You don’t need to add code for Command5 or the check box yet.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Running the TestThing Test Application
Start from the beginning | Creating an ActiveX DLL.


Running the TestThing Test Application

The procedures in this topic illustrate the key events in the lifetime of an object provided by an in-process component, including what happens when the DLL is unloaded forcefully by clicking the End button.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To run the TestThing test application

  1. Press CTRL+F5 to run the project group. Notice that no messages have appeared in the Immediate window yet.

    Note By default the Compile On Demand option is checked on the General tab of the Options dialog box (accessed from the Tools menu). When you’re debugging a component with Compile On Demand checked, you may find it useful to use CTRL+F5 (or Start with Full Compile, on the Run menu) to compile all the projects in the group before entering run mode. Compile errors usually require resetting the project, which means returning to design mode.

  2. On the Thing Demo dialog box, click Create New Thing. Before you respond to the InputBox requesting a name for the Thing, notice that the Immediate window now contains two debug messages:

    When the Command1_Click event procedure creates the test Thing object, two things happen. First, before the object is created, the code in Sub Main is executed. Only after Sub Main executes is the Thing object created.

    Notice that the Name property has no value yet.

    Important The Sub Main procedure of an ActiveX component is executed when the component receives the first request for one of the objects it provides, before the component creates the object. You should not put lengthy tasks in the Sub Main procedure, because a request to create an object may time out while waiting for Sub Main to execute. This is discussed in "Starting and Ending a Component" in "General Principles of Component Design."

  3. Type First Thing in the InputBox and click OK. The code you put in the Command1_Click event procedure assigns the value to the Name property of the Thing object.

  4. Click Show the Thing to display the Thing object’s properties. Notice that the Name property now has a value. Click OK to dismiss the message box.

  5. Click Reverse the Thing’s Name to call the ReverseName method of the Thing object. After the method returns, Show the Thing (Command2) is clicked by setting its Value property to True, and the MsgBox statement in Command2_Click() displays the updated property values. Click OK to dismiss the message box.

  6. Click Create New Thing again, to destroy the existing Thing object and create a new one.

    In the Immediate window, you will see a debug message from the Initialize event of the new Thing, as the New operator causes the object to be created. This is followed by the Terminate message from the original Thing, the one you named First Thing.

    The original Thing object is destroyed when the reference to the new Thing object is placed in the variable mthTest. At that point, there are no variables holding references to the original Thing, so it must be destroyed.

  7. Type Second Thing in the InputBox, then click OK.

  8. Click the Close box on Thing Demo to return to design mode. Do not use the End button on the Visual Basic toolbar.

    Before the program closes, the Terminate message for the Thing object you named Second Thing is displayed in the Immediate window.

    When you close a program, Visual Basic cleans up all the variables that still contain object references. As each variable is set to Nothing, the object it referred to is destroyed.

When a program is ended abruptly, with the End button or by an End statement in code, Visual Basic reclaims any memory and resources the program was using. However, the cleanup is done as if the program had suffered a fatal error. Objects will not get their Terminate events.

To observe the effects of the End button

  1. Run the program again. Create a new Thing, and name it anything you like.

  2. This time, end the program using the End button on the toolbar. The Terminate message for the form’s Thing object is not displayed in the Immediate window.

    Important Remember that ending your program with the End button, or with an End statement in your code, halts the program immediately, without executing the Terminate events of any objects. It’s always better to shut your program down by unloading all the forms.

For More Information The rules of object lifetime are introduced in "Programming with Objects" in the Visual Basic Programmer’s Guide.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Circular References and Object Lifetime
Start from the beginning | Creating an ActiveX DLL.


Circular References and Object Lifetime

In normal program operation, an object is not destroyed until all references to it have been released. This has implications for object lifetime. The procedures in this topic illustrate this by allowing Thing objects to indulge in a little Narcissism.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To add the StuckOnMyself property to the Thing class

  1. In the Project Explorer window, double-click Thing to bring the code window for the class module to the front.

  2. Add the following code to the Declarations section:
    ' Private data for the StuckOnMyself property.
Private mthStuckOnMyself As Thing
Private mblnStuckOnMyself As Boolean
  3. On the Tools menu, click Add Procedure to open the Add Procedure dialog box. In the Name box, type StuckOnMyself. Click Property and Public, then click OK.

    Modify the property procedures as follows:

    Public Property Get StuckOnMyself() As Boolean
   StuckOnMyself = mblnStuckOnMyself
End Property

Public Property Let StuckOnMyself(ByVal NewValue _
      As Boolean)
   mblnStuckOnMyself = NewValue
   If mblnStuckOnMyself Then
      Set mthStuckOnMyself = Me
   Else
      Set mthStuckOnMyself = Nothing
   End If
End Property

    This code illustrates the power of property procedures. When a user requests the value of the StuckOnMyself property, the Property Get procedure is called, and simply returns the value of the module-level Boolean variable mblnStuckOnMyself.

    When the user sets the value of the StuckOnMyself property, the Property Let procedure is called. After assigning the new value to the module-level Boolean variable mblnStuckOnMyself, the Property Let either places a reference to the Thing object (Me) in the module-level variable mthStuckOnMyself, or sets the variable to Nothing.

For More Information Property procedures are the best way to implement properties for objects provided by components. See "Implementing Properties in Components" in "General Principles of Component Design."

What Are Circular References?

A circular reference occurs when two objects hold references to each other — or when an object holds a reference to itself, as in the StuckOnMyself property. You can read more about circular references in "Dealing with Circular References" in "General Principles of Component Design."

The next procedure adds code to TestThing to demonstrate circular references by selectively exercising the StuckOnMyself property. It also illustrates another way to create an object.

To exercise the StuckOnMyself property from TestThing

  1. In the Project Explorer window, double-click Form1 to bring the form to the front.

  2. Double-click the Temporary Thing button to open the code window, and add the following code to the Command5_Click event procedure:
    ' Button "Temporary Thing".
Private Sub Command5_Click()
   Dim thTemp As New Thing
   thTemp.Name = InputBox( _
      "Enter a name for the temporary Thing", _
      "Temporary Thing")
   ' Create a circular reference if the check box
   '   captioned "Stuck on itself" is checked.
   If Check1.Value = vbChecked Then
      thTemp.StuckOnMyself = True
   End If
End Sub

No code is needed for the check box. If Check1 is checked, the Command5_Click event procedure sets the StuckOnMyself property of the new Thing object.

Notice that instead of explicitly creating a new Thing with the New operator, the Temporary Thing button uses a variable declared As New, allowing implicit creation of the object. The next procedure demonstrates this. It also shows how object lifetime is affected by variable scope, and illustrates the effects of circular references on object lifetime.

To demonstrate circular references in TestThing

  1. Press CTRL+F5 to run the project group.

  2. Click Create New Thing. In the InputBox, type the name Long Term Thing, and then click OK. In the Immediate window, notice the messages from Sub Main and the Thing’s Initialize event.

  3. Click Temporary Thing to create a very short-lived Thing. Because the object variable thTemp that holds the reference to this Thing is a procedure-level variable, its lifetime — and hence the lifetime of the object — is limited to the execution of the procedure.

    You will first see an InputBox, because Visual Basic must evaluate the code to the right of the equal sign before assigning the result to the new Thing object’s Name property.

    Before you enter a name in the input box, look in the Immediate window. There hasn’t been an Initialize message from the new Thing object, because it has not yet been created. Because the variable thTemp was declared As New, a Thing object will be created the moment one of its properties or methods is invoked — and not a moment sooner.

  4. Type any name you like in the InputBox and then click OK.

    You’ll see two messages in the Immediate window, an Initialize message and a Terminate message. The Initialize event occurs the moment the name from the InputBox is assigned to thTemp.Name. Visual Basic finds that thTemp contains Nothing, creates a Thing object, and places a reference to the object in thTemp.

    Although the value of the DebugID property has already been set — it’s what happens first in the Initialize event — the Name property is still blank. This underscores the fact that the Initialize event occurs before any other code is executed, or any properties are set.

    Only after all this has happened can Visual Basic assign the value from the InputBox function to the Thing’s Name property. That’s a lot of activity for one line of code.

    But wait, there’s more. As soon as the Thing is created, the Command5_Click event procedure ends. The variable thTemp goes out of scope, which is exactly the same as if it had been set to Nothing. There are no more references to the temporary Thing, so it is destroyed. Its Terminate event displays its properties, including the name you assigned to it.

  5. Press Ctrl+Break to enter Break mode, then press f8 to enter Single Step mode. Click Temporary Thing to enter the Command5_Click event procedure.

    Press F8 once more, to move to the line of code that sets the Thing’s name:

    Before you go on to the next step, you might take a guess at what the next line of code will be.

  6. Press F8 to execute the InputBox statement. Type any name you like in the InputBox, and then click OK.

    Because you’re debugging the ThingDemo component in the same environment as the test program, Visual Basic can step directly into the component’s code from the test program.

    Continue pressing F8 through the code for the Initialize event, the DebugID property, and the Terminate event. When you reach the last line of Class_Terminate(), press F5 to return to Run mode.

    In-process debugging is a powerful tool for learning about the order in which events occur in a component.

  7. Check Stuck on itself, and then click Temporary Thing again, to create a Thing with a reference to itself. Type the name Renegade in the InputBox, and then click OK.

    In the Immediate window you will see an Initialize message for the Thing, but no Terminate message. The object was not destroyed when the variable thTemp went out of scope, because there was still a reference to the object — in the mthStuckOnMyself variable belonging to the StuckOnMyself property.

    When will this erstwhile temporary Thing be destroyed? If you could set the Renegade Thing’s StuckOnMyself property to False, there would be no more references — but TestThing can’t do this, because the program no longer has a reference to use to call StuckOnMyself!

    The object is orphaned, and will go on taking up memory until the DLL is unloaded. As you’ll see in a later procedure, the circular reference in the Renegade Thing’s StuckOnMyself property will keep the entire DLL in memory.

  8. Click Release the Thing to destroy the Thing you named Long Term Thing, observing its Terminate message in the Immediate window, and then click Temporary Thing several times. Enter a new name for each "temporary" object.

    As with the first Renegade object, you won’t see any Terminate messages for these objects in the Immediate window, because the circular references will keep them from being destroyed.

  9. On the Thing Demo dialog box, click the Close box to return to design mode. Presto! Like magic, a whole series of Terminate messages appear in the Immediate window.

    When Visual Basic shuts down TestThing, it also shuts down ThingDemo. Being the tidy sort, it clears all of ThingDemo’s object variables — including the self-references being held by the Renegade Thing and its cohorts.

As you can see, it’s important to avoid keeping extra object references in an in-process component. A client may create and release hundreds of objects while using a component. If they all persist in memory the way the renegade Things did, performance will eventually degrade.

For More Information Circular references are discussed in "Dealing with Circular References," in "General Principles of Component Design," and in Appendix A, "ActiveX Component Standards and Guidelines."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Adding a Form to the ThingDemo Project
Start from the beginning | Creating an ActiveX DLL.


Adding a Form to the ThingDemo Project

In-process components can serve as libraries of procedures and dialog boxes, saving you programming time and giving your applications a consistent look and feel.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

The procedures in this topic demonstrate the way objects can be used to control modal or modeless dialog boxes. The same form is used for both cases, as shown in Figure 2.3.

Figure 2.3 The dlgDemo dialog box

To add a form to the ThingDemo project

  1. In the Project Explorer window, click ThingDemo to make it the active project.

    Important Whenever you’re working with a project group, make sure the right project is active before adding a new module.

  2. On the Project menu, click Add Form to open the Add Form dialog box. Double-click the Form icon to add a form.

Design the form as you would any Visual Basic form, and save it as ThingDemo_dlgDemo.frm. The following table lists property settings for the objects in the form.

Object | Property | Setting
Form | Name BorderStyle Caption | dlgDemo Fixed Dialog Dialog Box
TextBox | Name Text | txtDemo (Empty)

The dialog box is not invoked directly from the client, because forms are private classes. Clients cannot create instances of private classes, and you should never pass instances of private classes to client applications. This is discussed in "Data Types Used in Properties and Methods" in "General Principles of Component Design."

To display the dialog box, clients will call the ShowDialog method of a global Dialogs object, which will create and show the dlgDemo form.

To add code to the dlgDemo form

  1. Double-click on dlgDemo to open its code window, and add the following code.
    ' Declare an event.
Event NotifyClients(ByVal Data As String)
  2. Add the following code to the Change event of the text box. When the contents of the text box change, the NotifyClients event is raised with the new contents as its argument.
    Private Sub txtDemo_Change()
   RaiseEvent NotifyClients(txtDemo.Text)
End Sub
  3. To prevent the dialog box from being closed without the knowledge of the Dialogs object, place the following code in the QueryUnload event procedure.
    Private Sub Form_QueryUnload(Cancel As Integer, _
      UnloadMode As Integer)
   ' If the Close button was pressed, hide the
   '   dialog box instead of unloading it.
   If UnloadMode = vbFormControlMenu Then
      Cancel = True
      Me.Visible = False
   End If
End Sub

If dlgDemo is shown as a modal dialog, hiding the dialog rather than unloading it allows the ShowDialog method of the Dialogs class to retrieve the value in the text box.

To create the Dialogs class

  1. From the Project menu, choose Add Class Module to open the Add Class Module dialog box. Double-click the Blank Class Module icon to add a class module to the project.

  2. In the Properties window, change the Name property of the class to Dialogs.

  3. In the Properties window, change the Instancing property to GlobalMultiUse, so that you can call the ShowDialog method without explicitly creating a Dialogs object.

  4. To create a WithEvents variable that can handle the NotifyClients event of dlgDemo, and to create an event that Dialogs object can raise for its own clients, add the following code to the Declarations section:
    Private WithEvents mdlg As dlgDemo
Event NotifyClients(ByVal Data As String)
  5. In the Object box, select mdlg to show the event procedure for the NotifyClients event raised by dlgDemo. Add the following code:
    Private Sub mdlg_NotifyClients(ByVal Data As String)
   RaiseEvent NotifyClients(Data)
End Sub

    The Dialogs object receives the NotifyClients event from its dlgDemo form whenever the contents of the dialog’s text box changes. The Dialogs object immediately raises its own NotifyClients event, passing along the data to its own client.

  6. In the code window, select Class from the Object box. In the Procedure box, select Initialize and add the following code to the Class_Initialize event procedure:
    Private Sub Class_Initialize()
   Debug.Print "Dialogs object created"
   Set mdlg = New dlgDemo
End Sub
  7. In the Procedure box, select Terminate and add the following code to the Class_Terminate event procedure:
    Private Sub Class_Terminate()
   Debug.Print "Dialogs object terminated"
   Unload mdlg
   Set mdlg = Nothing
End Sub

    When terminating, an object that controls a form should always unload the form and set its reference to the form to Nothing, to avoid tying up resources with orphaned forms.

  8. On the Tools menu, click Add Procedure to open the Add Procedures dialog box. In the Name box, type ShowDialog. Click Function and Public, then click OK.

    In the code window, change the newly created Function procedure to appear as follows:

    Public Function ShowDialog( _
      Optional ByVal Text As String = "", _
      Optional ByVal Modal As Boolean = True) _
      As String
   With mdlg
      .txtDemo.Text = Text
      If Modal Then
         .Caption = "Modal Dialog Box"
         .Show vbModal
         ShowDialog = .txtDemo.Text
      Else
         .Caption = "Modeless Dialog Box"
         .Show vbModeless
      End If
   End With
End Function

    Typed optional arguments let the compiler catch type mismatch errors, instead of waiting until run-time errors occur. Typed optional arguments are discussed in "Programming Fundamentals" in the Visual Basic Programmer’s Guide.

  9. Save the class module as ThingDemo_Dialogs.cls.

The ShowDialog method that displays the dialog has two optional arguments:

  • The initial text it’s going to display

  • A Boolean argument that determines whether the dialog is modal

The Text argument is assigned to the text box on dlgDemo before the dialog box is shown. The default value of the Modal argument is True, so omitting it causes the dialog to be modal.

If the dialog is shown Modal, the ShowDialog method returns the contents of the txtDemo text box after the dialog has been dismissed by the user.

If the dialog is shown Modeless nothing is returned, because the client received a NotifyClients event whenever the contents of the text box changed.

For More Information Events are discussed in "Adding Events to Classes" in "General Principles of Component Design."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Using the Global Object in TestThing
Start from the beginning | Creating an ActiveX DLL.


Using the Global Object in TestThing

TestThing will call the ShowDialog method to display a modal dialog in its Form_Load event procedure, as the application is starting. It will also show a modeless dialog when the main form is clicked.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To add code to call the ShowDialog method

  1. In the Project Explorer window, click Form1 to select it, then press F7 — or click the Code button on the Project Explorer window toolbar — to open the code window for the form. Place the following code in the Form_Load event.
    Sub Form_Load()
   Me.Caption = ShowDialog(Me.Caption)
End Sub

    Notice that no object variable is declared, and that the ShowDialog method is called as if it were an ordinary function procedure. This is possible because the Dialogs object’s Instancing property is set to GlobalMultiUse.

  2. In the Declarations section, add the following code:
    Private WithEvents mdgs As Dialogs

    The variable is declared WithEvents, so that Form1 can handle the NotifyClients events raised by the Dialogs object.

  3. In the Object box, select mdgs to show the event procedure. Add the following code:
    Private Sub mdgs_NotifyClients(Byval Data As String)
   Me.Caption = Data
End Sub

    Whenever the NotifyClients event is received, Form1 assigns the data to its caption property. The modeless dialog box thus controls the form’s caption.

  4. To show the modeless dialog and start event handling, add the following code to the Form_Click event procedure:
    Private Sub Form_Click()
   If mdgs Is Nothing Then
      Set mdgs = New Dialogs
   End If
   mdgs.ShowDialog Me.Caption, False
End Sub

    Important The Dialogs object created here is not the global instance used to show the modal dialog in the Load event. The global instance will be created automatically when the ShowDialog method is invoked in Form_Load, and will be used for any subsequent method invocations that omit the object variable. The instance used to show the modeless dialog box is explicitly created and assigned to the WithEvents variable, so its NotifyClients event can be handled.

Okay! The Dialogs object creates and manipulates a dlgForm object, and receives its NotifyClients event. Form1 creates two different Dialogs objects, one implicitly (the global instance used to show the modal dialog) and one explicitly (used to show the modeless dialog).

The modeless dialog communicates with the Dialogs object by raising a NotifyClients event. The Dialogs object responds by raising its own NotifyClients event, which Form1 handles by assigning the data to its Caption property. Time to see if it all works!

To see if it all works

  1. Press CTRL+f5 to run the project group. The first thing you’ll see is the dialog box, containing the caption of Form1:

  2. Enter a new caption, and close the dialog box. The main form appears, showing its new caption.

  3. Click on the main form to create another Dialogs object, which will show a modeless dialog box containing the current caption of the main form. Enter another caption, and observe that with each keystroke the main form’s caption changes:

    Each change to the contents of the text box raises a NotifyClients event from dlgDemo to the Dialogs object. The Dialogs object then raises its own NotifyClients event, which the main form receives.

You may find it instructive to run the project again, pressing f8 this time to step through the code one line at a time. You can clearly see that two different Dialogs objects are involved, because you’ll step through their separate Initialize events.

Among the interesting things you’ll notice is that the modal dialog box raises NotifyClients events, just as the modeless dialog box does. Form1 doesn’t have a WithEvents variable containing a reference to the global Dialogs object, so it’s unable to handle these events.

Important The "global" in "global object" simply means that the methods and properties of the object added to the global name space of your project, so that they can be used without first declaring an object variable. It does not mean that there is only one such object, or that multiple client applications can share a single object. An instance of the class will be created for each client that uses methods of the class without qualifying them. Only one such global object will be created for each client.

Client Support for Modeless Forms

The display of modeless forms from in-process components requires communication with the client’s message loop. Not all clients support this. For an explanation of this limitation, see "Displaying Forms from Code Components," in "Building Code Components."

For More Information Global objects are discussed in "Instancing for Classes Provided by ActiveX Components" in "General Principles of Component Design," and in "Building Code Components."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Compiling and Testing the ThingDemo DLL
Start from the beginning | Creating an ActiveX DLL.


Compiling and Testing the ThingDemo DLL

Once an in-process component project is tested and debugged in the development environment, you can compile and test the .dll file.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

An ActiveX component is compiled the same way any Visual Basic project is compiled — by choosing Make from the File menu. The .dll file includes a type library that describes your objects and allows them to be browsed.

Visual Basic makes it easy to add the necessary lines to the Windows Registry. You don’t have to write any code, and you don’t have to understand the format of registry entries. When you make the executable file, Visual Basic automatically registers it on your computer.

Note When you distribute your application as part of an integrated solution, registration is done during setup. If you use the Visual Basic Package and Deployment Wizard, registration of your component will be included in the setup process automatically. This is discussed in "Deploying Components" in "Debugging, Testing, and Deploying Components."

For More Information The Package and Deployment Wizard is described in "Distributing Your Applications" in the Visual Basic Programmer’s Guide.

To compile and test the OLE DLL executable

  1. If the TestThing project is still in run mode, click the Close button on Form1 to return to design mode.

  2. In the Project Explorer window, click ThingDemo to select the project.

  3. On the File menu, click Make ThingDemo.dll to open the Make Project dialog box. Click OK to build the .dll file.

  4. On the File menu, click Remove Project to remove ThingDemo from the project group, so that Visual Basic will use the compiled binary component (.dll file) instead of the project.

    Visual Basic displays a warning message, because the TestThing project contains a reference to ThingDemo. Click Yes to remove ThingDemo anyway.

    When you remove ThingDemo from the project group, Visual Basic looks for ThingDemo.dll in the Windows Registry. If the .dll file exists, Visual Basic automatically updates the reference you set in "Creating the TestThing Project."

    To switch back to using the project instead of the binary component, you can click Add Project on the File menu, and add the ThingDemo project back to the project group.

  5. Press f5 to run TestThing using the .dll file.

You can compile TestThing.exe, and run it with the .dll. You can also test the compiled ThingDemo.dll from other applications.

To use ThingDemo.dll in another copy of Visual Basic

  1. Open a new instance of Visual Basic. In the New Project dialog box, double-click the EXE Project icon to open a new EXE project.

  2. On the Project menu, click References to open the References dialog box. Check ThingDemo ActiveX DLL, and then click OK.

    You can now add code to create Thing and Dialogs objects and invoke their properties and methods.

  3. Press f5 to run the project.

For More Information You can read more about test projects and debugging techniques in "Debugging, Testing, and Deploying Components."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | Circular References and Component Shutdown
Start from the beginning | Creating an ActiveX DLL.


Circular References and Component Shutdown

The following procedure shows how Visual Basic unloads an in-process component after the client has released all references to its objects. It also shows how circular references can prevent this unloading, and highlights an important difference between public and private objects.

The procedure can only be run using the compiled component and compiled test project, because Visual Basic never unloads an in-process component project running in the development environment.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

To observe DLL unloading

  1. Add a new class module to ThingDemo. Name it TellTale, and set its Instancing property to Private. Add the following code to its Terminate event:
    Private Sub Class_Terminate()
   MsgBox "Private object destroyed"
End Sub

    A private object cannot be created by clients, and should never be passed to them. As you’ll see, private objects do not keep in-process components from unloading, and a client that uses a reference to a private object after its component has unloaded will suffer a catastrophic program failure.

  2. Add the following code to Module1 in ThingDemo:
    Option Explicit
Private mtt As TellTale      ' New code.

Sub Main
   Debug.Print "Executing Sub Main"
   Set mtt = New TellTale      ' New code.
End Sub

    Once the projects are compiled, Debug statements can’t be used to show what’s happening internally. Like a canary in a coal mine, the TellTale object will give us an indication of what’s happening in the compiled component.

  3. On the File menu, click Make Project Group to compile both ThingDemo and ThingTest. On the Start menu, click Run to open the Run dialog box, and browse to find and run ThingTest — or use the Windows Explorer.

  4. Close the modal dialog box, and click Create Temporary Thing. Enter any name you like in the input box, and click OK.

    As you saw earlier, the temporary Thing doesn’t hang around long. The only reason to create it now is to load ThingDemo.dll and execute Sub Main. For this procedure, the interesting question is what happens when the Things are all gone.

  5. Wait a few minutes. Go get a cup of coffee. You deserve it. When you come back, after a few minutes have elapsed, you’ll see a message box with the message "Private object destroyed."

    What happened? There were no references to public objects, so after a decent interval (usually about two minutes, although this will vary depending on how frequently it gets idle time) Visual Basic will attempt to unload the in-process component.

    When the DLL is unloaded, Visual Basic frees the memory it was using — including variables containing private object references. As a result the TellTale object is destroyed, and you get a visual cue that the DLL has unloaded.

    If you create a new Thing object at this point, you’ll notice a slight pause as the DLL is reloaded.

  6. Now for the circular references. Check the Stuck on itself box, and then click Create Temporary Thing to create a Thing object with a reference to itself.

  7. Wait a few minutes. Go get another cup of coffee. Drink it. Wait a few more minutes. Do some other work. However long you wait, the DLL will not unload, because the reference to the Thing object keeps Visual Basic from unloading it.

    Visual Basic can’t tell the difference between an internal reference to a public object and an external (client) reference to a public object, so it has no choice but to keep the DLL loaded.

  8. Close ThingTest’s main form. The private object is again destroyed, because unloading the client application also unloads any in-process components it’s using.

For More Information The rules for component shutdown are listed in "Starting and Ending a Component" in "General Principles of Component Design," and discussed in more detail in "ActiveX Component Standards and Guidelines."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX DLL.

To | See
Go to the next step | ActiveX DLL Creation Summary
Start from the beginning | Creating an ActiveX DLL.


ActiveX DLL Creation Summary

In order to introduce new concepts in the most natural order, the procedures in this chapter have not followed the normal sequence of steps for creating an in-process component. When you create a new ActiveX DLL, the steps you’ll generally follow are these:

  • Determine the features your component will provide.

  • Determine what objects are required to divide the functionality of the component in a logical fashion.

  • Design any forms your component will display.

  • Design the interface — that is, the properties, methods, and events — for each class provided by your component.

  • Create a project group consisting of your component project and a test project.

  • Implement the forms required by your component.

  • Implement the interface of each class.

  • As you add each interface element or feature, add features to your test project to exercise the new functionality.

  • Compile your DLL and test it with all potential target applications.

Note This topic is part of a series that walks you through creating a sample ActiveX DLL. It begins with the topic Creating an ActiveX DLL.

For More Information General design issues for ActiveX components are discussed in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components." Issues exclusive to DLLs, including testing, packaging, and deployment, are discussed in "Building Code Components." The step-by-step procedures in "Creating an ActiveX Exe Component" demonstrate other component features, including events, call-backs, unattended operation, and multithreading.


Creating an ActiveX EXE Component

Components provide reusable code in the form of objects. An application that uses a component’s code, by creating objects and calling their properties and methods, is referred to as a client.

Components can run either in-process or out-of-process with respect to the clients that use their objects. An out-of-process component, or ActiveX EXE, runs in its own address space. The client is usually an application running in another process.

Figure 3.1 Client and out-of-process component

The fact that an out-of-process component runs in its own process means that a client can tell it to do something, and then go about its business while the component does the work. When such a system is properly set up, the component can tell the client when the task is done using an asynchronous notification, as explained in "Asynchronous Call-Backs and Events" in "Building Code Components."

Note In-Process and Out-of-Process Code Components," in "General Principles of Component Design," discusses differences between in-process and out-of-process code components.

The step-by-step procedures in this chapter explore the creation and use of out-of-process components by building a simple component called CoffeeWatch. Coffeewatch demonstrates the behavior of modal and modeless dialog boxes, and shows one way to implement asynchronous notifications using events.

You’ll also see how to debug an ActiveX EXE out of process, by running the component and a test project in two separate instances of the Visual Basic development environment.

The procedures for creating the Coffee component build on each other. The sequence in which you perform the procedures is therefore important:

Topics

Creating the Coffee Project

Opens a new ActiveX EXE project, sets the necessary project information, and adds modules required for demonstrating modal and modeless forms.
Showing Forms from the CoffeeMonitor Class

Adds code to show a form either modally or modelessly.
Creating the CoffeeWatch Test Project

Shows how to create a test project in a second instance of the development environment. Adds code to the test project to call the methods of CoffeeMonitor.
How Modal and Modeless Forms Behave Out of Process

Shows that modal and modeless forms display differently when they’re not in the same process as the client’s main form.
Providing an Asynchronous Notification Event

Shows how to set up an object to provide asynchronous notifications to one or more clients.
Receiving an Asynchronous Notification Event

Adds code to the client to request asynchronous notifications, and to take some action when the notification events occur.
Sharing the CoffeeMonitor

Creates a Connector object that clients can use to obtain a reference to a single shared instance of CoffeeMonitor.
Using the Shared CoffeeMonitor

Adds code to the client to request a Connector object, and then call its CoffeeMonitor property to obtain a reference to a shared CoffeeMonitor object.
Creating and Testing the Coffee Executable

Compiles both the Coffee component and the CoffeeWatch test program. Runs both the CoffeeWatch project and several instances of CoffeeWatch.exe using one instance of Coffee.exe.
ActiveX EXE Component Creation Summary

Outlines the typical process of designing and creating an out-of-process component.

Before You Begin

These procedures will be easier to follow if you set up your development environment as described below.

Before You Begin

  1. On the View menu, click Toolbox to open the Toolbox.

  2. On the View menu, click Project Explorer to open the Project Explorer window. The Project Explorer window will be used extensively to switch between project files.

  3. If the Project Explorer window is in Folder view, as shown below, click the Toggle Folders button on the Project Explorer window toolbar to turn the folders off.

  4. On the View menu, click Properties Window to open the Properties window.

  5. On the Tools menu, click Options to open the Options dialog box.

    Select the Editor tab, and make sure Require Variable Declaration is checked. This makes it much easier to catch typing errors.

    Select the Environment tab. Make sure Prompt To Save Changes is selected, then click OK. This will make it easy to save changes to the project as you go along.

Sample applications

Coffee2.vbp, CoffWat2.vbp, MTCoffee.vbp

Coffee2.vbp and CoffWat2.vbp contain expanded source code for this chapter. In addition to asynchronous notifications using events, they include an alternate version using call-back methods on a secondary interface, and an example of all-code timers you can use instead of the Timer control. MTCoffee illustrates some features of multithreading. The sample applications are listed in the Samples directory.


Creating the Coffee Project

This topic creates the Coffee project, with its CoffeeMonitor class and its TestForm form class.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To create the Coffee project

  1. On the File menu, click New Project.

  2. In the New Project dialog box, double-click the ActiveX EXE icon. Visual Basic automatically adds a class module, Class1, to the new project.

  3. Press F4 to switch to the Properties window. Double-click the Name property and change it to CoffeeMonitor. This is the name you’ll use to create objects from the class.

    The default value for the Instancing property is MultiUse. This allows clients to create multiple instances of the CoffeeMonitor class. For a full discussion of the Instancing property, see "Instancing for Classes Provided by ActiveX Components," in "General Principles of Component Design."

  4. On the Project menu, click Project1 Properties to open the Project Properties dialog box. Select the General tab, fill out the information shown below, then click OK.

    The project name, Coffee, is also used as the name of the component’s type library. It can be combined with the name of each class the component provides, to produce unique class names.

    If two components each provide a CoffeeMonitor class, the fully qualified class name lets you specify which component’s CoffeeMonitor class you want to use, for example, Coffee.CoffeeMonitor.

  5. On the Project menu, click Add Form to open the Add Form dialog box. Double click the Form icon to add a form to the project.

    Note If you’ve used the Options dialog box (accessed from the Tools menu) to disable the Add Form dialog box, you’ll just get the form. This is okay.

  6. Press f4 to open the Properties window. Change the following property settings:
    Object Property Setting
    Form (Name)
    Caption     		TestForm
    TestForm - Modal

  7. On the File menu, click Save Project to save the project files, using the following names. Visual Basic will provide the extensions automatically.
    File | File name | Extension
    Form | Coffee_TestForm | .frm
    Class module | Coffee_CoffeeMonitor | .cls
    Project | Coffee | .vbp

For More Information See "Choosing a Project Type and Setting Project Options" in "General Principles of Component Design."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Showing Forms from the CoffeeMonitor Class
Start from the beginning | Creating an ActiveX EXE Component


Showing Forms from the CoffeeMonitor Class

Out-of-process components can show both modal and modeless forms. The first use for the CoffeeMonitor will be to demonstrate this, because the results may not be quite what you expect.

The procedure in this topic adds a ShowForm method to the CoffeeMonitor class, plus public constants for use with the method.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To show forms from the CoffeeMonitor class

  1. In the Project Explorer window, double-click CoffeeMonitor (or select it and then click the Code button on the Project Explorer window toolbar), to open its code window.

  2. In the Declarations section, add the following Public Enum:
    Option Explicit
Public Enum cfeModality
   cfeModal = vbModal
   cfeModeless = vbModeless
End Enum

    An enumeration declared Public in a class module is added to your component’s type library. It will not be associated with the class in which it was defined, but will become part of the global name space.

    Why would you provide your own constants, when Visual Basic includes vbModal and vbModeless? Your component may be used with a development tool that doesn’t provide these constants. Providing constants compatible with Visual Basic is a flexible solution.

    Note Putting the prefix "cfe" in front of the constant names identifies the constants as belonging to the Coffee component, and reduces the chance of name collisions with other components. Some component authors follow the prefix with two or three uppercase letters identifying the Enum; this seems superfluous here. See "Providing Named Constants for Your Component" in "General Principles of Component Design."

  3. On the Tools menu, click Add Procedure to open the Add Procedure dialog box. Type ShowForm in the Name box, click Sub and Public, and then click OK.

    A public Sub or Function procedure in a class module defines a method of the class, while a public property procedure defines a property. See "Adding Properties and Methods to Classes," in "General Principles of Component Design."

  4. Add the following code to the Sub procedure:
    Public Sub ShowForm(Optional Modality As _
      cfeModality = cfeModal)
   Dim frm As New TestForm
   If Modality = cfeModeless Then
      frm.Caption = "TestForm - Modeless"
   Else
      frm.Caption = "TestForm - Modal"
   End If
   frm.Show Modality
End Sub

    The typed optional argument Modality specifies a Modal form if the argument is omitted. Typed optional arguments are discussed in "Passing Arguments to Procedures" in "Programming Fundamentals" in the Visual Basic Programmer’s Guide.

    Because the constants in cfeModality are compatible with the vbModal and vbModeless constants supplied by Visual Basic, you can simply pass the Modality argument to the form’s Show method. The Show method will automatically raise an error if an invalid value is supplied.

  5. On the File menu, click Make Coffee.exe to create a reference executable.

    The reference executable, which you only need to create once, will help your test application keep its connection to this project. See "Creating a Test Project for an Out-of-Process Component" in "Debugging, Testing, and Deploying Components."

  6. Press CTRL+F5 to run the project.

    Note If Compile On Demand is checked (on the General tab of the Options dialog box, accessible from the Tools menu), you should use ctrl+f5 (or select Start with Full Compile from the Run menu) to ensure that your component is fully compiled before you begin testing. A compilation error that occurs after the component is providing objects to the test program can be very awkward. Compile On Demand is checked by default.

    Important You must put your project in run mode before editing or running the test program, as explained in "Creating a Test Project for an Out-of-Process Component" in "Debugging, Testing, and Deploying Components."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Creating the CoffeeWatch Test Project
Start from the beginning | Creating an ActiveX EXE Component


Creating the CoffeeWatch Test Project

"Out of process" means the component is running in a separate process from the client application. Therefore you need a separate process for your test program — which means starting another instance of the Visual Basic development environment.

The following procedure creates the test program and adds a reference to the out-of-process Coffee component.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To create the CoffeeWatch test project

  1. Use the Windows Start menu or the Explorer to open another instance of Visual Basic. On the New Project dialog box, double-click the Standard EXE icon to open a new Standard EXE project.

  2. On the Project menu, click Project1 Properties to open the Project Properties dialog box. Select the General tab, type CoffeeWatch in the Project Name box, then click OK.

  3. On the Project menu, click References to open the References dialog box. Check the entry for Coffee.vbp, as shown below, then click OK.

    You can tell the entries apart by observing the Location in the information panel at the bottom of the dialog box.

    Important You must check the entry for the project file (.vbp) in order to debug the project in the development environment. If you set a reference to the .exe file, visible just above the .vbp entry in the figure above, CoffeeWatch will obtain objects from an instance of the executable instead.

  4. Add two command buttons to Form1, and set their properties as shown in the following table:
    Object | Property | Setting
    Command button (Name)
    Caption     		cmdModal
    Show Modal Form
    Command button (Name)
    Caption     		cmdModeless
    Show Modeless Form
    Form | Caption | CoffeeWatch

  5. In the Declarations section of the code window for Form1, add the following declaration:
    Option Explicit
Private mcmnTest As CoffeeMonitor

    The prefix for the variable begins with the letter ‘m’ to indicate that this is a module-level variable. The letters ‘cmn’ were chosen to represent the CoffeeMonitor class type.

  6. Add the following code to the Load event for Form1:
    Private Sub Form_Load()
   Set mcmnTest = New CoffeeMonitor
End Sub
  7. Add the following code to the Click event procedures for the command buttons:
    Private Sub cmdModal_Click()
   mcmnTest.ShowForm cfeModal
End Sub

Private Sub cmdModeless_Click()
   mcmnTest.ShowForm cfeModeless
End Sub
  8. On the File menu, click Save Project to save the project files, using the following names. Visual Basic will provide the extensions automatically.
    File | File name | Extension
    Form | CoffeeWatch_Form1 | .frm
    Project | CoffeeWatch | .vbp

If you have the Auto List Members option enabled, you’ve noticed that the methods of the CoffeeMonitor class appeared in the list of available classes. This is because the reference you established in step 3 made the Coffee component’s type library information available.

For More Information The Auto List Members option can be set on the Editor tab of the Options dialog box, available from the Tools menu.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | How Modal and Modeless Forms Behave Out of Process
Start from the beginning | Creating an ActiveX EXE Component


How Modal and Modeless Forms Behave Out of Process

As mentioned in "Showing Forms from the CoffeeManager Class," modal and modeless forms displayed by an out-of-process component have a different relationship to a client application’s forms than they would if displayed by an in-process component. Running CoffeeWatch will demonstrate this.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To demonstrate modal and modeless form behavior with the out-of-process Coffee component

  1. Press f5 to run the CoffeeWatch test program.

  2. Click Show Modal Form to display a modal form from the Coffee component.

    Depending on your system configuration, the order programs were started, and so on, the modal form may come up in front of CoffeeWatch — or you may see something like this:

  3. TestForm is not really modal with respect to the CoffeeWatch form. To see this, click anywhere on the CoffeeWatch form. The Component Request Pending dialog box appears, as shown here:

    The dialog box appears because CoffeeWatch is waiting on its call to CoffeeMonitor.ShowForm, which is waiting on the modal TestForm. However, if TestForm were truly modal with respect to Form1, clicking on Form1 would generate a system sound indicating that Form1 was disabled.

  4. Click Switch To, to bring TestForm to the front.

    Note Depending on your system configuration and the order in which programs are loaded, the copy of Visual Basic in which Coffee is running may come to the front along with TestForm, obscuring CoffeeWatch.

  5. Click on CoffeeWatch again, to bring it to the front and display the Component Request Pending dialog box again.

    Note If you can’t see CoffeeWatch (the form, not the project), use the task bar (or press alt+tab) to bring it to the front.

    In the strict sense of the word, TestForm is modal to CoffeeWatch. That is, you can’t do anything with CoffeeWatch until TestForm is dismissed. However, because the two forms are in different processes, CoffeeWatch can appear on top of TestForm.

  6. Click Switch To, to bring TestForm to the front, and then click TestForm’s close box to dismiss the modal form.

  7. Click Show Modeless Form to show TestForm as a modeless form.

    TestForm behaves like a modeless form that’s not owned by CoffeeWatch. That is, it doesn’t stay on top of CoffeeWatch. You can verify this by clicking on each of the two forms, to bring them alternately to the front.

    Important Because TestForm is in a different process from CoffeeWatch, you cannot make CoffeeWatch the owner of TestForm as you would if the forms were in the same process — that is, by passing a reference to CoffeeWatch in the OwnerForm argument of TestForm. For details see "Displaying Forms from Code Components," in "Building Code Components."

  8. DO NOT dismiss the modeless TestForm. Instead, close CoffeeWatch by clicking its close box.

    TestForm doesn’t close. (It may be hidden behind the instance of Visual Basic containing the CoffeeWatch project — use the task bar or alt+tab to bring it to the front.)

    This illustrates two important points: First, a form displayed by an out-of-process component is not dependent on the client application. Nor is its lifetime limited by the client’s lifetime.

    Second, a loaded form can keep an out-of-process component’s executable from unloading. For details, see "Starting and Ending a Component" in "General Principles of Component Design."

    In its Terminate event, CoffeeMonitor should unload any forms it has shown.

  9. Dismiss TestForm by clicking its close box. The Coffee component remains in run mode. To return Coffee to design mode, click the End button, or select End from the Run menu.

    Once you put an ActiveX EXE project in run mode, it remains in run mode. This is convenient for testing, but it’s different from the behavior of the made .exe. The executable for an out-of-process component unloads when the last client releases its last reference to an object provided by the component, as discussed in "Starting and Ending a Component."

    Note The only way to test the shutdown behavior of an out-of-process component is to test with the made executable.

The lesson to take away from all this is that out-of-process code components are generally not the best way to show forms.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Providing an Asynchronous Notification Event
Start from the beginning | Creating an ActiveX EXE Component


Providing an Asynchronous Notification Event

One of the most interesting uses for out-of-process components is to provide asynchronous notifications to the client. That is, the client doesn’t remain blocked while the component executes a method — instead, it goes about its business while the component works on a task or watches for an occurrence of interest. The component’s notification arrives out of the blue, without any specific action on the part of the client.

The procedure in this topic sets up a simple asynchronous notification based on a common data processing problem: How do you know when the coffee is ready?

The demonstration assumes that you have a coffee maker with a serial interface (however, the demonstration will work even if you don’t). The Coffee component tests the serial port periodically to see if the coffee maker’s High bit is set, indicating that the coffee is ready.

Before you begin this procedure, make sure you’ve returned the Coffee project to design mode, as described at the end of "How Modal and Modeless Forms Behave Out of Process."

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To set up an asynchronous notification event in the CoffeeMonitor class

  1. In the Project Explorer window, right-click TestForm to open the context menu, and select View Object to open the form designer. Add a Timer control, and set its properties as follows:
    Object Property Setting
    Timer control (Name)
    Enabled
    Interval     		tmrCoffee
    True
    10000

    There’s no need to put code in the tmrControl_Timer event procedure. As you’ll see, CoffeeMonitor will handle the control’s Timer event, test the serial port, and raise the CoffeeReady event to notify CoffeeWatch.

  2. In the Project Explorer window, double-click CoffeeMonitor (or select it and then click the Code button on the Project Explorer window toolbar), to open its code window. Highlight all of the code except Option Explicit, and comment it out using the Comment Block button on the Edit toolbar, as shown here:

    In order to view all the code at once, make sure you’ve selected Full Module View, as shown by the buttons in the lower left-hand corner of the code window.

    You can show the Edit toolbar using the context menu accessed by right-clicking the menu or standard toolbar, as shown here:

  3. In the Declarations section, add the following variables and event declaration:
    Option Explicit
Private mTestForm As TestForm
Private WithEvents mwtmrCoffee As Timer
Event CoffeeReady()
    • CoffeeMonitor will raise the CoffeeReady event to notify the CoffeeWatch program when the coffee’s ready.

    • The variable mTestForm will hold a reference to an instance of TestForm, whose only purpose is to hold the Timer control.

    • The variable mwtmrCoffee will hold a reference to the Timer control on the TestForm. The variable is declared WithEvents so that the CoffeeMonitor object can handle the timer’s events.
  4. In the left-hand (Object) drop down, select Class to make the event procedure templates for the class module available in the right-hand (Procedure) drop down, and to open the template for the default event (Initialize). Add the following code to create and load an instance of TestForm when the CoffeeMonitor object is created:
    Private Sub Class_Initialize()
   Set mTestForm = New TestForm
   Load mTestForm
   Set mwtmrCoffee = mTestForm.tmrCoffee
End Sub

    After the instance of TestForm is created and loaded, a reference to tmrCoffee is placed in the variable mwtmrCoffee. When the reference is placed in the WithEvents variable, Visual Basic connects the timer’s events to the associated event procedures in CoffeeMonitor.

  5. In the Procedure drop down, select the Terminate event for the class. Add the following code to the event procedure template:
    Private Sub Class_Terminate()
   Set mwtmrCoffee = Nothing
   Unload mTestForm
   Set mTestForm = Nothing
End Sub

    As we saw in "How Modal and Modeless Forms Behave Out of Process," objects that use forms in out-of-process components need to dispose of the forms when they’re done with them. The first step is to set the WithEvents variable to Nothing, so that CoffeeMonitor will stop handling the Timer control’s events. Then TestForm can be unloaded, and the variable containing the reference to it can be set to Nothing.

    Note Strictly speaking, there’s no need to set mTestForm to Nothing here. Visual Basic will set the variable to Nothing when it destroys the CoffeeMonitor object.

  6. In the Object drop down, select mwtmrCoffee. The Timer control’s only event, Timer, appears in the Procedure drop down, and the event procedure template is added to the code window. Add the following code:
    Private Sub mwtmrCoffee_Timer()
   ' (Code to test serial port omitted.)
   RaiseEvent CoffeeReady
End Sub

    Event procedures associated with a WithEvents variable always begin with the variable name, as discussed in "Adding Events to Classes" in "General Principles of Component Design."

    When the CoffeeMonitor object receives the Timer event, it raises its own CoffeeReady event to notify any clients (CoffeeWatch, in this case) that the coffee’s ready.

    (This code simply raises the CoffeeReady event every ten seconds. If you actually have a coffee pot with a serial port, you can add code to test coffee maker’s status, and conditionally raise the event.)

    Note One of the advantages of using events to provide notifications is that only one reference is needed. That is, TestForm doesn’t need a reference to the CoffeeMonitor object in order for the Timer control to send CoffeeMonitor an event. This avoids the circular reference problem described in "Dealing with Circular References" in "General Principles of Component Design."

  7. Press CTRL+f5 to run the project. Remember, when working with out-of-process components, the component project must be in run mode before you can edit or run the client project.

    Component projects should be run with CTRL+F5 (or Start with Full Compile on the Run menu) if Compile On Demand is checked, as discussed in "Showing Forms from the CoffeeMonitor Class."

For More Information Events are introduced in "Adding an Event to a Class" and "Adding Events to Forms," in "Programming with Objects" in the Visual Basic Programmer’s Guide.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Receiving an Asynchronous Notification Event
Start from the beginning | Creating an ActiveX EXE Component


Receiving an Asynchronous Notification Event

When you use events for asynchronous notifications, most of the work is done by the component author. Setting up a client to receive the events is pretty easy.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To receive the CoffeeReady event in CoffeeWatch

  1. Switch to the instance of Visual Basic that has the CoffeeWatch project loaded.

  2. In the Project Explorer window of the CoffeeWatch project, right-click Form1 to open the context menu, and select View Code to open the code window. Comment out the code in the Click events of the two buttons.

  3. Modify the code in the Declarations section as follows:
    Option Explicit
Private WithEvents mwcmnTest As CoffeeMonitor

    Notice the letter ‘w’ added to the variable mwcmnTest, to remind the author that this is a WithEvents variable. This is one author’s private convention. The letters ‘cmn’ have been chosen to indicate a variable of type CoffeeMonitor.

  4. Add the following code to the Load event of Form1, to create a new CoffeeMonitor object and assign it to the WithEvents variable. At run time, Visual Basic connects the object’s events to their event procedures when this assignment takes place.
    Private Sub Form_Load()
   Set mwcmnTest = New CoffeeMonitor
End Sub
  5. In the Object drop down, select mwcmnTest to obtain access to its event procedures. Add the following code to the CoffeeReady event.
    Private Sub mwcmnTest_CoffeeReady()
   MsgBox "COFFEE!"
End Sub
  6. That’s it. Press f5 to run the project.

    Every ten seconds you’ll get a notification from Coffee. CoffeeWatch is not blocked in the meantime — you can move and resize it, and click the buttons.

  7. Close the CoffeeWatch form to return to design mode.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Sharing the CoffeeMonitor
Start from the beginning | Creating an ActiveX EXE Component


Sharing the CoffeeMonitor

Of course, a thorough programmer would want to be sure of getting coffee notifications regardless of which application she was using. You could create separate CoffeeMonitor objects for each program in which you wanted to be notified, but that could cause problems if your computer doesn’t have multiple serial ports.

One way to allow multiple clients to hook up to a single CoffeeMonitor object is to provide a connector object, as shown in the following procedure.

To create the Connector object

  1. Switch to the instance of Visual Basic containing the Coffee project, and click the End button to return to design mode.

  2. On the Project menu, select Add Module to add a standard module to the Coffee project. (If you have the Add Module dialog box enabled, double-click the Module icon when the dialog box appears.) Add the following variable declarations to the Declarations section:
    Option Explicit
Public gCoffeeMonitor As CoffeeMonitor
Public glngUseCount As Long

    The variable gCoffeeMonitor is used to keep a reference to the single shared CoffeeMonitor. The glngUseCount variable keeps track of the number of Connector objects using the CoffeeMonitor.

  3. On the Project menu, select Add Class Module to add a class module to the Coffee project. (If you have the Add Class Module dialog box enabled, double-click the Class Module icon when the dialog box appears.) In the Properties window, set (Name) to Connector.

  4. Add the following code to the Connector class’s code window:
    Public Property Get CoffeeMonitor() As CoffeeMonitor
   Set CoffeeMonitor = gCoffeeMonitor
End Property

    The Connector’s read-only CoffeeMonitor property returns a reference to the single global instance of CoffeeMonitor.

  5. In the Object drop down, select Class to open the template for the default event (Initialize). Add the following code to create the global instance of CoffeeMonitor:
    Private Sub Class_Initialize()
   If gCoffeeMonitor Is Nothing Then
      Set gCoffeeMonitor = New CoffeeMonitor
   End If
   glngUseCount = glngUseCount + 1
End Sub

    The first time a client requests a Connector object, it will create the global CoffeeMonitor. Each Connector object increases the use count of the CoffeeMonitor.

Tip The Initialize event of the first object a component creates is a good place to initialize the component. You’ll be much less likely to encounter object creation time-out problems and deadlocks that may occur if Sub Main is used to initialize the component.

  6. In the Procedure drop down, select the Terminate event for the class. Add the following code to the event procedure template:
    Private Sub Class_Terminate()
   glngUseCount = glngUseCount - 1
   If glngUseCount = 0 Then
      Set gCoffeeMonitor = Nothing
   End If
End Sub

    Just as objects should dispose of any forms they create, so they should release any objects they use. Because the reference to the global CoffeeMonitor is in a global variable, the last Connector object must release it.

    Note A compiled out-of-process component shuts down when all clients release their references to its objects, unless it has a loaded form. When compiled, the Coffee component would be kept running by the TestForm that CoffeeMonitor keeps a reference to. Since the CoffeeMonitor object is kept from terminating by the global variable, the component would never shut down. This is discussed in "Starting and Ending a Component," in "General Principles of Component Design."

  7. In the Project Explorer window, double-click CoffeeMonitor to activate it. In the Properties window, set the Instancing property of the class to PublicNotCreatable.

    Important Make sure you have the CoffeeMonitor class active before you set the Instancing property. If you set the Instancing property of the wrong class, you’ll get an error later when you run CoffeeWatch.

    Any client can create an instance of the Connector class, because its Instancing property is set to MultiUse (the default). Clients can use a Connector object’s CoffeeMonitor property to get a reference to the single shared CoffeeMonitor object. By making CoffeeMonitor a PublicNotCreatable class, you allow clients to use the shared global instance — while preventing them from creating their own CoffeeMonitors.

  8. Press CTRL+f5 to run the project. Remember, when working with out-of-process components, the component project must be in run mode before you can edit or run the client project.

    When prompted to save the new files, use the following names. Visual Basic supplies the extensions.

    File | File name | Extension
    Class module | Coffee_Connector | .cls
    Module | Coffee_Module1 | .bas

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Using the Shared CoffeeMonitor
Start from the beginning | Creating an ActiveX EXE Component


Using the Shared CoffeeMonitor

This procedure shows how to use the Connector object to get a reference to a single shared CoffeeMonitor object.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To receive the CoffeeReady event in CoffeeWatch

  1. Switch to the instance of Visual Basic that has the CoffeeWatch project loaded.

  2. In the Project Explorer window of the CoffeeWatch project, right-click Form1 to open the context menu, and select View Code to open the code window. Modify the code in the Declarations section as follows:
    Option Explicit
Private WithEvents mwcmnTest As CoffeeMonitor
Private mcctTest As Connector

    The new variable, mcctTest, will hold a reference to the Connector object. The letters ‘cct’ have been chosen to indicate a variable of type Connector.

  3. Change the code in the Load event of Form1, so that it first creates a Connector object, and then obtains a reference to the shared CoffeeMonitor object using the Connector’s CoffeeMonitor property.
    Private Sub Form_Load()
   Set mcctTest = New Connector
   Set mwcmnTest = mcctTest.CoffeeMonitor
End Sub
  4. Press f5 to run the project.

    Once again, you’ll get a notification from Coffee every ten seconds.

  5. Close the CoffeeWatch form to return to design mode.

  6. Switch to the Coffee project, and click the Break button (or select Break from the Run menu) to enter break mode. In the Immediate window, type:
    ?TypeName(gCoffeeWatch)

    The result is Nothing, showing that the shared CoffeeMonitor object was released.

  7. Click the End button to return to design mode.

You may find it interesting to run the Coffee project again, then make CoffeeWatch.exe and run multiple instances of it. (You must run the Coffee project first, so that its type library information is available.) You can set break points in Coffee, to observe the workings of the events and properties of its objects while it provides objects to several clients.

A Bug in Connector

There’s a bug in the Connector class. If all clients release their Connector objects (but keep their references to the shared CoffeeMonitor), the last Connector will release the global reference. At that point, any client that creates a Connector will cause a second CoffeeMonitor to be created — because the new Connector will find that the global variable gCoffeeWatch contains Nothing. If CoffeeMonitor really used the serial port, this could cause a conflict.

The sample applications Coffee2.Vbp and CoffWat2.Vbp explore a possible solution to this bug.

Asynchronous Call-Back Methods

An alternate technique for notifying clients is the asynchronous call-back method, discussed in "Asynchronous Notifications Using Call-Back Methods." Call-backs are somewhat more complicated to implement, but they allow the component to receive return values and errors raised by the client, and they have a slight performance advantage.

The sample applications Coffee2.Vbp and CoffWat2.Vbp include an alternate implementation of CoffeeReady using call-backs.

Multithreading

Code components with no user interface can be marked for unattended execution, which is to say they can have no user interaction. For out-of-process components this option also enables multithreading, as explained in "Scalability and Multithreading," in "Building Code Components."

The sample application MTCoffee.vbp demonstrates a simple multithreading scenario.

For More Information The basics of raising and handling events are introduced in "Adding Events to a Class," in "Programming with Objects" in the Visual Basic Programmer’s Guide. Events in components are discussed in "Adding Events to Classes" in "General Principles of Component Design."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | Creating and Testing the Coffee Executable
Start from the beginning | Creating an ActiveX EXE Component


Creating and Testing the Executable

Creating and Testing the Coffee Executable

Once an out-of-process component project is tested and debugged in the development environment, you can compile and test the .exe file.

An ActiveX component is compiled the same way any Visual Basic project is compiled — by choosing Make from the File menu. The .exe file includes a type library that describes your objects and allows them to be browsed. Visual Basic automatically registers the component in your Windows registry when you make the executable file.

Note When you distribute your application as part of an integrated solution, registration is done during setup. If you use the Visual Basic Package and Deployment Wizard, registration of your component will be included in the setup process automatically. This is discussed in "Deploying Components" in "Debugging, Testing, and Deploying Components."

For More Information The Package and Deployment Wizard is described in "Distributing Your Applications" in the Visual Basic Programmer’s Guide.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

To compile and test the Coffee executable

  1. If the Coffee project is still in run mode, click the End button to return to design mode.

  2. On the File menu, click Make Coffee.exe to open the Make Project dialog box. Click OK to build the .exe file.

  3. Switch to the CoffeeWatch project. On the Project menu, click References to open the References dialog box. Remove the check mark from the Coffee Monitor Component entry for the .vbp file, and check the entry for the .exe file.

    You can tell the entries apart by observing the Location in the information panel at the bottom of the dialog box.

  4. On the File menu, click Make CoffeeWatch.exe to open the Make Project dialog box. Click OK to build the .exe file.

  5. (Optional) Press f5 to run CoffeeWatch using the compiled Coffee executable.

  6. Use the Explorer (or the Run box, on the Start menu) to start one or more instances of CoffeeWatch.exe.

    Notice that all the instances of CoffeeWatch appear to get their CoffeeReady events at the same time. In reality, the clients get their events one after the other — a single thread of execution traces through all of the event handlers. You can see this demonstrated in the sample applications for this chapter.

    Important When multiple objects are handling events from one event source — a scenario referred to as multicasting — the order in which they get events is undefined, and implementation dependent. You should never write code that depends on any particular order of event arrival among the multicasting clients.

    Events are anonymous — that is, the object that raises the event doesn’t know how many objects (if any) are handling the event.

To switch back to using Coffee.vbp, you can use the References dialog box to clear the entry for the .exe file and check the entry for the .vbp file instead.

Note You can compile CoffeeWatch.exe using either the compiled Coffee.exe, or Coffee.vbp. The latter is useful for debugging with multiple clients. The CoffeeWatch executable will be compiled using whichever entry is currently checked in the References dialog box.

For More Information You can read more about test projects and debugging techniques in "Debugging, Testing, and Deploying Components."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX EXE.

To | See
Go to the next step | ActiveX EXE Component Creation Summary
Start from the beginning | Creating an ActiveX EXE Component


ActiveX EXE Component Creation Summary

In order to introduce new concepts in the most natural order, the procedures in this chapter have not followed the normal sequence of steps for creating an out-of-process component. When you create a new ActiveX EXE component, the steps you’ll generally follow are these:

  1. Determine the features your component will provide.

  2. Determine what objects are required to divide the functionality of the component in a logical fashion.

  3. Design any forms your component will display.

  4. Design the interface — that is, the properties, methods, and events — for each class provided by your component.

  5. Create a separate test project, usually a Standard Exe project.

  6. Implement the forms required by your component.

  7. Implement the interface of each class.

  8. As you add each interface element or feature, add features to your test project to exercise the new functionality.

  9. Compile your Exe and test it with all potential target applications.

Note This topic is part of a series that walks you through creating a sample ActiveX EXE. It begins with the topic Creating an ActiveX EXE Component.

For More Information General design issues for ActiveX components are discussed in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components." Issues exclusive to code components are discussed in "Building Code Components." The step-by-step procedures in "Creating an ActiveX DLL" demonstrate other component features, including object lifetime, circular references, global objects, showing modal and modeless forms from in-process components, and another use for events.


Creating an ActiveX Control

The series of step-by-step procedures in this chapter builds a simple ActiveX control called ShapeLabel. Although the control itself is not very interesting, building it will quickly demonstrate the major events in the life of an ActiveX control, introduce you to the intricacies of running code at design time, and show the basic steps for creating and hooking up a property page.

All of the subjects introduced in these procedures are covered in greater depth in later chapters. References to in-depth material will be found in each procedure. In addition, "Building ActiveX Controls," shows how you can use the ActiveX Control Interface Wizard to make building controls even easier.

The procedures for creating the ShapeLabel control build on each other, so the sequence in which you perform the procedures is important.

Topics

Creating the ControlDemo Project

Opens a blank ActiveX control project, examines the ActiveX control designer, and sets the appropriate project properties.
Adding the TestCtlDemo Project

Creates a project group, including the ActiveX control and a test project, and links the two projects.
Running the ShapeLabel Control at Design Time

Shows how to put a single ActiveX control into run mode, without putting the test project into run mode, so the control’s design-time behavior can be debugged.
Life and Times of a UserControl Object

Illustrates the major events that occur in the life of an ActiveX control, and shows how an instance of a control is continually destroyed and re-created.
Drawing the ShapeLabel Control

You can draw your control from scratch, with graphics methods, or construct it by combining other controls. ShapeLabel uses the latter technique.
Saving the ShapeLabel Control's Property Values

Adds a Caption property to the ShapeLabel control, and demonstrates how to save the value. Introduces the Extender object.
Giving the ShapeLabel Control a Property Page

Shows how to create a property page, and how to connect it with the ShapeLabel control.
Adding an Event to the ShapeLabel Control

Demonstrates how to expose events.
Compiling the ControlDemo Component

Compiles the project, and shows how to test the .ocx file with the test project, and how to include it in another project.
Control Creation Recap

Summarizes the key tasks required to create an ActiveX control.

These procedures will be easier to follow if you set up your Visual Basic development environment to show the necessary windows.

Before You Begin

  1. On the View menu, click Toolbox to open the Toolbox.

  2. On the View menu, click Project Explorer to open the Project Explorer window. The Project Explorer window will be used extensively to switch between project files.

  3. If the Project Explorer window is in Folder view, as shown below, click the Toggle Folders button on the Project Explorer window toolbar to turn the folders off.

  4. On the View menu, click Properties window to open the Properties window.

  5. On the View menu, click Immediate window to open the Immediate window. You will need this window open at design time, in order to demonstrate the control’s code running at design time.

  6. On the Tools menu, click Options to open the Options dialog box.

    Select the Editor tab, and make sure the Require Variable Declaration check box is selected. This makes it much easier to catch typing errors.

    Select the Environment tab. Make sure Prompt To Save Changes is checked, then click OK. This will make it easy to save the changes to the project as you go along.

Sample application

CtlPlus.vbg

Fills in all the properties, methods, and events required to make ShapeLabel a functional control. Expands on the material in this chapter, showing additional control creation features, at the expense of some of the basic material covered in the step-by-step procedures. The sample applications are listed in the Samples directory.


Creating the ControlDemo Project

ActiveX controls can be added to any project type. When a control is compiled as part of an .exe file, however, it cannot be shared with other applications. The ShapeLabel control will be compiled into an .ocx file in a later procedure in this chapter, so it can be shared. Thus the ControlDemo project will be created as an ActiveX control project.

An ActiveX control project can contain as many controls as you like. When you build the project, the resulting .ocx file contains all the controls you’ve added.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To create the ControlDemo project

  1. On the File menu, click New Project to open the New Project dialog box. (This will close your current project or project group; you will be prompted to save any changes you have made.) Double-click the ActiveX Control icon to create a new project.

    Visual Basic automatically adds a UserControl designer to the project. The default name, UserControl1, appears as the caption of the designer.

  2. On the Project menu, click Project1 Properties to open the Project Properties dialog box. Select the General tab, fill out the information shown below, and then click OK.

  3. Double-click UserControl1 in the Project Explorer window to bring the designer to the front.

  4. In the Properties window, double-click the Name property and change the name of the user control to ShapeLabel. The new name appears in the caption of the designer and in the Project Explorer window.

    The name you specify becomes the class name of your control, just as CommandButton is the class name for a command button. "Building ActiveX Controls" provides guidelines for choosing class names for controls.

    Notice that the Properties window looks much as it would for a Visual Basic form. Some properties you’re used to seeing are missing, however, and there are properties not found on ordinary Visual Basic forms. These properties are discussed in "Building ActiveX Controls."

  5. Within the control designer, resize the control using the drag handle at the lower right corner of the control, dragging up and left to make the control smaller.

    This sets the default size of the control. For convenience in later procedures, the ShapeLabel control should be of modest size.

  6. On the File menu, click Save Project to save the project files. Name them as shown in the following table. Visual Basic will provide the indicated extensions automatically.
    File | Filename | Extension
    User control | ControlDemo_ShapeLabel | .ctl
    Project | ControlDemo | .vbp

    Binary information in a control — such as bitmaps — will be saved in a binary file with the same name and an extension of .ctx.

For More Information See "Setting Up a New Control Project and Test Project" and "Debugging Controls," in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Adding the TestCtlDemo Project
Start from the beginning | Creating an ActiveX Control


Adding the TestCtlDemo Project

In order to test the ShapeLabel control, you need a test project.

To allow debugging of in-process components, Visual Basic allows you to load two or more projects into a project group. In addition to enabling in-process debugging, the project group makes it easier to load your .ocx project and test project.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To add a test project to the project group

  1. On the File menu, click Add Project to open the Add Project dialog box.

    Important Do not click Open Project or New Project, as these will close your control project.

  2. Double-click the Standard EXE icon to add an ordinary .exe project. You can now see both projects in the Project Explorer window, and the caption of the Project Explorer window shows the default project group name.

    The new project immediately becomes the Startup project for the project group. The Project Explorer window identifies the Startup project by displaying its name in bold type. An ActiveX control project, like ControlDemo, cannot be the Startup project.

  3. On the File menu, click Save Project Group to save the test project and the project group. Name the files as shown below. Visual Basic will provide the indicated extensions automatically.
    File | Filename | Extension
    Form | TestCtlDemo_Form1 | .frm
    Project | TestCtlDemo | .vbp
    Project group | ControlDemo | .vbg

For More Information Test projects for ActiveX controls are discussed in more detail in "Debugging Controls," in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Running the ShapeLabel Control at Design Time
Start from the beginning | Creating an ActiveX Control


Running the ShapeLabel Control at Design Time

Unlike other programmable objects, controls have both design-time and run-time behavior. That is, some of the code in your control will execute when a developer places an instance of the control on a form at design time.

For example, the code you place in the UserControl_Resize event procedure will be executed both at design time and at run time.

In order to debug the design-time behavior of your control, you must be able to execute code in the control while the test form on which you place the control remains in design mode.

The following two procedures demonstrate this neat trick. In the first procedure, you’ll add code to the Resize event of the ShapeLabel control. In the second procedure, you’ll put part of ControlDemo into run mode — while the test project remains in design mode — and then add an instance of the ShapeLabel control to a form in the test project.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To add code to the Resize event

  1. In the Project Explorer window, double-click ShapeLabel to make it the active designer.

  2. Double-click the ShapeLabel control to open the code window.

  3. In the Procedure box, click the Resize event to go to its event procedure. Add the following code:
    Private Sub UserControl_Resize()
   Static intCt As Integer
   intCt = intCt + 1
   Debug.Print "Resize " & intCt
End Sub

    Note The name of the event procedure has the prefix "UserControl," just as the Form_Resize event procedure for an ordinary form has the prefix "Form."

In developing an ordinary Visual Basic application, you would now click the Start button on the toolbar, or press F5, to run your application. In order to put a ShapeLabel control on Form1, however, you have to run just the code for the control, leaving everything else in design mode.

To run the ShapeLabel control at design time

  1. In the Project window, double-click ShapeLabel to bring its designer to the front, then press CTRL+F4 to close the window. Closing the designer’s window puts the ShapeLabel control in run mode. As soon as the control is in run mode, its icon (the default toolbox icon for a user control) is enabled in the toolbox.

    Important Don’t click the Start button on the toolbar, or press F5, because this would put the entire project group into run mode, and you would be unable to add the new control to a form.

    When you put a control in run mode, it doesn’t matter how you close the designer’s window. (You always can tell if the designer is open, because the control’s toolbox icon will be grayed.)

  2. In the Project Explorer window, double-click Form1 to bring it to the front.

  3. Double-click the ShapeLabel icon to add a ShapeLabel control to Form1. The control appears as a flat gray rectangle with grab handles:

    In the Properties window you can see the default properties for a new control. The ShapeLabel control you just added to the form has been given a default name, ShapeLabel1.

    Note Naming your control when you begin designing it avoids confusion. Suppose you place a control with a default name, such as UserControl1, on a form. Automatic numbering of new controls would append a number to the control name, resulting in a confusing name like UserControl11.

  4. The ShapeLabel control’s Resize event occurred when it was placed on the form, as you can see by looking at the Immediate window. Use the grab handles to resize the control several times. Each time you resize it, the Resize event occurs again.

    If you simply move the control around the form, the Resize event does not occur.

  5. On Form1, double-click the ShapeLabel control to open the code window for Form1. The cursor will be on the default event procedure, ShapeLabel1_GotFocus. You can use the Procedure box to view the other three events Visual Basic automatically provides for your control. Close the code window when you are done.

  6. In the Project Explorer window, double-click ShapeLabel to open the ShapeLabel designer. Notice that the ShapeLabel control you placed on Form1 is shaded with hatch marks to indicate that it is inactive.

    Opening a control’s designer makes all instances of the control inactive. Changing the code in the control’s code window may also make control instances inactive.

  7. Code in ShapeLabel’s code module cannot be executed while the designer is open. Use the grab handles to resize the shaded ShapeLabel control on Form1. The Resize event doesn’t fire, so no new messages appear in the Immediate window.

  8. Be sure the ShapeLabel designer is in front, then press CTRL+F4 to close the window, reactivating the control instance. The shading disappears from the control on Form1, indicating that the instance is active again.

    If the control has become inactive because of changes to its code, you can right-click the test form to bring up its context menu, and click Update UserControls to reactivate control instances.

Note Due to the number of windows required by these procedures, you may frequently find that ShapeLabel’s designer has disappeared behind another form. You can double-click ShapeLabel in the Project Explorer window to bring the designer to the front.

For More Information More information about running code at design time can be found in "Debugging Controls," in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Life and Times of a UserControl Object
Start from the beginning | Creating an ActiveX Control


Life and Times of a UserControl Object

The life of an ordinary Visual Basic form is marked by certain key events, such as Initialize, Load, QueryUnload, and Unload. In order to create well-behaved applications, it’s important to know when these events occur in the life cycle of a form.

The same is true for controls. The key events in the life cycle of a UserControl are Initialize, InitProperties, ReadProperties, WriteProperties, and Terminate. The following procedure explores these events.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To observe key events for ShapeLabel

  1. In the Project Explorer window, double-click ShapeLabel to open its designer.

  2. Double-click the designer to open a code window for ShapeLabel, and enter code in the following event procedures:
    Private Sub UserControl_Initialize()
   Debug.Print "Initialize"
End Sub

Private Sub UserControl_InitProperties()
   Debug.Print "InitProperties"
End Sub

Private Sub UserControl_ReadProperties(PropBag As _
      PropertyBag)
   Debug.Print "ReadProperties"
End Sub

Private Sub UserControl_WriteProperties(PropBag _
      As PropertyBag)
   Debug.Print "WriteProperties"
End Sub

Private Sub UserControl_Terminate()
   Debug.Print "Terminate"
End Sub

    Note For UserControl objects, Load and Unload are superseded by the ReadProperties and WriteProperties events. This is discussed in more detail in "Understanding Control Lifetime and Key Events," in "Building ActiveX Controls."

  3. Be sure the ShapeLabel designer is in front, then press CTRL+F4 to close the window, putting the control in run mode. Debug messages will appear in the Immediate window:

    What’s going on here? You haven’t put another instance of the ShapeLabel control on Form1. Where did all these events come from?

    This illustrates an important point about controls. A user puts a control on a form, and thereafter thinks of the control as a permanent fixture of the form. From the control author’s perspective, however, controls are getting destroyed and re-created all the time.

    When you put ShapeLabel in run mode by closing its designer, the instance of ShapeLabel on Form1 was destroyed and re-created, at which point it received an Initialize event. Why didn’t you see a Terminate event first? Because the original instance of ShapeLabel you placed on Form1 was created before you added the code in the UserControl_Terminate event procedure! Welcome to the wild and woolly world of control creation.

    Note Control instances are also destroyed and recreated when you click Update UserControls on the form’s context menu.

  4. Press CTRL+F5, or click the Start button on the toolbar, to run TestCtlDemo. When the project is running, the grid on Form1 is gone, so you can’t see the ShapeLabel, but you can see its life flash before your eyes in the Immediate window:

    After a control instance is created, the ReadProperties event gives you a chance to obtain the control’s saved property values from the .frm file belonging to the form that contains the control instance.

    When the design-time instance of the control is destroyed, the WriteProperties event gives you a chance to save the property values set at design time by the developer who's using your control. Property values are saved in the containing form’s .frm file, as you’ll see in "Saving the ShapeLabel Control's Property Values," later in this chapter.

    The Terminate event occurs when the control is being destroyed.

    Note By default, the Compile On Demand option is checked on the General tab of the Options dialog box (accessed from the Tools menu). Using CTRL+F5 (or Start with Full Compile, on the Run menu) overrides Compile On Demand, and fully compiles all the projects in the group before entering run mode. This is useful because compile errors usually require resetting the project, which means returning to design mode. When you’re debugging controls, you may prefer to turn Compile On Demand off rather than remembering to use CTRL+f5.

  5. Close Form1, to return the project to design mode. In the Immediate window, you’ll see a Terminate event (but not WriteProperties — why not?) as the run-time instance of ShapeLabel is torn down. Then you’ll see the Initialize, ReadProperties, and Resize events, as the design-time instance of the control is created.

    The run-time instance of a control never gets a WriteProperties event, because it doesn’t need to save its property values. To see why not, consider ShapeLabel’s future. When it’s compiled into an .ocx file, you’ll add it to another project, put an instance on a form, compile the project into an .exe, and run it. When you close that .exe, the only place the ShapeLabel instance could save its property values would be in the .exe file. This sort of behavior is not tolerated by well-behaved operating systems.

  6. Scroll to the top of the Immediate window, click in the top left corner, and drag to select all the text in the window. Press the DELETE key to clear the window.

  7. In the Project Explorer window, double-click Form1 to bring Form1 to the front.

  8. On the Toolbox, double-click the ShapeLabel icon to add another instance of the control to Form1. You’ll see a new event this time.

    When a new instance of your control is placed on a container, it gets an InitProperties event. In the UserControl_InitProperties event procedure you can place code to:

    • Set the default values for each of the control’s properties values

    • Perform tasks whenever a user creates an instance of your control.
  9. Close the Form1 designer by clicking its Close button or pressing CTRL+F4 while the designer is in front. In the Immediate window, you will see two sets of WriteProperties and Terminate events, one for each instance of ShapeLabel.

  10. In the Project Explorer window, double-click Form1 to open its designer again. When the designer opens, all the controls on Form1 are created, and their Initialize events are fired. All controls then receive ReadProperties events, which allow them to retrieve their saved property values. The InitProperties event does not occur, because both instances of the ShapeLabel control already exist.

For More Information Control lifetime, and key events therein, are discussed in "Understanding Control Lifetime and Key Events," in Chapter 9, "Building ActiveX Controls." "Exposing Properties of Constituent Controls," in the same chapter, explains how the ActiveX Control Interface Wizard simplifies the creation of code to save and retrieve property values.Control lifetime, and key events therein, are discussed in "Understanding Control Lifetime and Key Events," in "Building ActiveX Controls." "Exposing Properties of Constituent Controls," in the same chapter, explains how the ActiveX Control Interface Wizard simplifies the creation of code to save and retrieve property values.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Drawing the ShapeLabel Control
Start from the beginning | Creating an ActiveX Control


Drawing the ShapeLabel Control

You can use graphics methods, such as Circle and Line, to draw your control, or you can create your control’s appearance using existing ActiveX controls and Visual Basic intrinsic controls. Controls you add to the UserControl to create its appearance are called constituent controls.

As its name suggests, ShapeLabel’s appearance is created using a Shape control and a Label control.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To add constituent controls to the ShapeLabel control

  1. In the Project Explorer window, double-click ShapeLabel to open its designer.

  2. In the Toolbox, double-click the Visual Basic Shape control to place a Shape control on the ShapeLabel designer. If you haven’t used the Shape control before, hold the mouse over the Toolbox buttons until you find the one whose ToolTip is "Shape."

  3. In the Properties window, set the following property values for the Shape control:
    Property | Value
    BorderStyle | 0 - Transparent
    FillColor | &H000000FF (Red)
    FillStyle | 0 - Solid
    Name | shpBack
    Shape | 2 - Oval

    Note To set color properties such as FillColor and ForeColor to specific colors, select the Palette tab of the color selection dialog.

  4. In the Toolbox, double-click the Label control to add a label on top of the Shape control. In the Properties window, set the following property values for the Label control:
    Property | Value
    Alignment | 2 - Center
    BackStyle | 0 - Transparent
    ForeColor | &H00FFFFFF (White)
    Name | lblCaption

  5. Use the bottom grab handle to change the height of the label so that it is slightly taller than the text it contains. ShapeLabel should look something like this:

  6. Double-click the ShapeLabel designer to bring the code window to the front, and replace the code in the UserControl_Resize event procedure with the following:
    Private Sub UserControl_Resize()
   ' Size the Shape control to fill ShapeLabel's
   '   visible surface area.
   shpBack.Move 0, 0, ScaleWidth, ScaleHeight
   ' Center the Label control vertically, and
   '   make it the same width as ShapeLabel.
   lblCaption.Move 0, (ScaleHeight _
      - lblCaption.Height) / 2, ScaleWidth
End Sub
  7. When you’re designing a user control, remember that the area you have to work with is bounded by the ScaleWidth and ScaleHeight of the control. Nothing outside this is visible to the user of your control. Furthermore, the size of the client area will change at the whim of the user. The Resize event is thus one of the most important events in control design.

  8. Be sure the ShapeLabel designer is in front, then press CTRL+F4 to close the window, putting the control in run mode. In the Project window, double-click Form1 to bring it to the front.

  9. The two ShapeLabel controls should now appear as red ovals, with centered white captions that read, "Label1." Resize the ShapeLabels to test the Resize event code.

For More Information See "Drawing Your Control" in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Saving the ShapeLabel Control's Property Values
Start from the beginning | Creating an ActiveX Control


Saving the ShapeLabel Control's Property Values

You can add properties and methods to an ActiveX control in the same way you add them to class modules: by creating Public procedures. Since ShapeLabel is going to be an enhanced label control, it makes sense for it to have a Caption property.

The following procedure adds a Caption property, and the support code to save and retrieve the property value. A control’s property values are saved along with the other data that describes the container — in this case, Form1.

Note This topic is part of a series that walks you through creating a sample ActiveX control. IIt begins with the topic Creating an ActiveX Control.

To add a Caption property to the ShapeLabel control

  1. In the Project Explorer window, double-click ShapeLabel to open its designer, then double-click on ShapeLabel to bring its code window to the front.

  2. On the Tools menu, click Add Procedure to open the Add Procedure dialog box. In the Name box, enter the name Caption. Click Property and Public, then click OK.

  3. In the Code window, change the newly created property procedures to appear as follows:
    Public Property Get Caption() As String
   Caption = lblCaption.Caption
End Property

Public Property Let Caption( _
         ByVal NewCaption As String)
   lblCaption.Caption = NewCaption
   PropertyChanged "Caption"
End Property

    Note Be careful to change both property declaration lines by adding As String, as shown above. Property Get and Property Let declarations must match. Using specific type names speeds up execution, and provides type checking for the developer who uses your control.

    The Property Let procedure is executed whenever a new value is assigned to the Caption property. It stores the new value directly in the Caption property of the lblCaption label on ShapeLabel.

    The Property Get procedure is executed whenever the property value is retrieved. It reads the value stored in the Caption property of the lblCaption label.

    Property Let procedures in controls must call PropertyChanged, as explained in "Adding Properties to Controls" in "Building ActiveX Controls." This tells the Properties window to refresh its display, and informs Visual Basic that the form has changed.

    Property procedures are discussed in "Adding Properties to a Class," in "Programming with Objects" in the Visual Basic Programmer’s Guide.

  4. To initialize the Caption property, add the following code to the UserControl_InitProperties event procedure:
    Private Sub UserControl_InitProperties()
   ' Let the starting value for the Caption
   '   property be the Name given to this
   '   instance of ShapeLabel.
   Caption = Extender.Name
   Debug.Print "InitProperties"
End Sub

    What is this Extender object? To the user of a control, extender properties — such as Name, Top, and Left — appear to be part of your control. However, extender properties are really provided by the container your control is placed on. The Extender object of the UserControl gives you, the control designer, access to these properties from within your control.

    The read-only Name property of the Extender object returns the name the container (or the user) gives to a specific instance of your control. Using this name (for example, ShapeLabel1) as the initial value of the Caption property mimics the behavior of the Label control.

Tip If your control imitates the behavior of controls that provide similar functionality, using it will be more intuitive.

What would happen if you created a Name property for your control? You would be able to access it from within your control, but the only Name property your user would see would be the Name property of the Extender object.

This introduces a recurrent theme for controls: The container determines a large portion of your control’s behavior and appearance. It’s the container that determines your control’s Name, and your Top and Left properties are maintained relative to the container’s coordinates. This theme will be taken up again in "Building ActiveX Controls."

One last item of business: Why put this code in the InitProperties event? Why not use the Initialize event? As you have seen, Initialize is called every time the control instance is created, which happens often. InitProperties happens only when the user places the control on the container. This makes it the appropriate place to set initial values for a control instance.

In addition, the UserControl object’s Extender and AmbientProperties objects are not yet available when the Initialize event occurs. "Understanding Control Lifetime and Key Events," in "Building ActiveX Controls," discusses appropriate uses of the Initialize event.

  5. To save the value of your Caption property, add the following code to the UserControl_WriteProperties event procedure:
    Private Sub UserControl_WriteProperties(PropBag As _
      PropertyBag)
   Debug.Print "WriteProperties"
   PropBag.WriteProperty "Caption", Caption, _
      Extender.Name
End Sub

    The PropertyBag is just what its name implies, a "bag" in which property values are saved. The bag is provided by the container. You can’t see into it, and you have no idea where or how the data is saved. All you can do is put values in and take them out.

    The first argument of the WriteProperty method is the name of the property, which will be used as the retrieval key. You should use the name of the property for this argument, because it will appear in the .frm text file (in Visual Basic—other containers may use other file names to save project data), and may be seen by the user of the control.

    The second argument is the value. A property value is saved as a Variant.

    The third argument, oddly enough, is a default value. Why provide a default when saving the property value? Before saving the value, the WriteProperty method compares the property value with this default. If they are the same, the property value doesn’t have to be saved, because default values will be set automatically when the control is reloaded. This keeps the .frm file from being cluttered with hundreds of default entries, a great favor to your users!

  6. Place the following code in the ReadProperties event, to retrieve the persisted property value for the Caption property:
    Private Sub UserControl_ReadProperties(PropBag As _
      PropertyBag)
   Debug.Print "ReadProperties"
   Caption = PropBag.ReadProperty("Caption", _
      Extender.Name)
End Sub

    The second argument of the ReadProperty method is a default value to be used if no value has been saved, if the user has deleted the property from the text file, or if the value has never been changed from the default, and therefore never saved by WriteProperty.

  7. Be sure the ShapeLabel designer is in front, then click the Close button or press CTRL+F4 to close the window, putting the control into run mode. Like magic, the captions of the ShapeLabel controls change to match the default names of the two instances, ShapeLabel1 and ShapeLabel2.

    Use the Properties window to change the Caption properties of the two ShapeLabel controls on Form1, then click the Close button on the Form1 designer. In the Project Explorer window, double-click Form1 to re-open the Form1 designer.

    From the messages in the Immediate window, you can see that the controls have been destroyed and re-created, but the values of the Caption properties have been saved and retrieved.

  8. Press CTRL+F5 to run TestCtlDemo, the Startup project of the project group, and observe the run-time behavior of the ShapeLabel control.

  9. Click the Close button on Form1 to return to design mode.

For More Information Details of saving and retrieving property values can be found in "Adding Properties to Controls," in "Building ActiveX Controls." "Exposing Properties of Constituent Controls," in the same chapter, explains how the ActiveX Control Interface Wizard simplifies the creation of code to save and retrieve property values.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Giving the ShapeLabel Control a Property Page
Start from the beginning | Creating an ActiveX Control


Giving the ShapeLabel Control a Property Page

Simple properties that you create using property procedures will be shown automatically in the Visual Basic Properties window. You can also connect your control to property pages, which display your control’s properties in an alternate format.

Each property page you connect to your control becomes one tab on the tabbed Properties dialog box. Visual Basic handles all the details of presenting the pages as a tabbed dialog, and manages the OK, Cancel, and Apply buttons. All you have to do is lay out the controls that will be used to set the property values.

Property pages are useful when a group of properties interact in a complex fashion, as with the Toolbar control included with Visual Basic. They’re also useful for controls that will be distributed internationally, because the captions can be localized for different languages. Property pages also allow your controls to be used with development tools that don’t have a Properties window.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To add a property page to the project

  1. In the Project Explorer window, click ControlDemo to select the control project. On the Project menu, click Add Property Page to open the Add PropertyPage dialog box. Double-click the Property Page icon to add a property page to the project.

  2. In the Properties window, double-click the Name property, and change the name of the property page to SLGeneral. Double-click the Caption property, and change the caption to General.

    The caption is what will appear on the property page’s tab when it’s in use.

    Why name the page SLGeneral instead of General? You may have several controls in a project, and each one may have a General page. This is the ShapeLabel control’s General page.

  3. On the File menu, click Save Project Group to save the project group. Name the property page as shown in the following table. Visual Basic will provide the indicated extension automatically.
    File | Filename | Extension
    Property page | ControlDemo_SLGeneral | .pag

    Binary information in a property page — such as bitmaps — will be saved in a binary file with the same name and an extension of .pgx.

The designer for a property page looks much like the designer for a control, except that the caption bar of the designer shows the Caption property of the property page, instead of the Name property.

To design the General property page for the ShapeLabel control

  1. Place a Label control on the property page, and set the Caption property of the label to the word Caption.

  2. Underneath the label, place a TextBox control, and assign it the following property values:
    Property | Value
    Name | txtCaption
    Text | <empty>

    The property page should appear as shown below.

    Placing the property description label above the text box in this fashion makes it easier to localize your control component for other languages, in which the word for "Caption" may be longer or shorter. Localization of controls is discussed in detail in "Building ActiveX Controls."

  3. Double-click the property page, to open a code window. In the Events drop down, select the SelectionChanged event, and add the following code:
    Private Sub PropertyPage_SelectionChanged()
   ' Display the caption of the first control in
   '   the list of currently selected controls.
   txtCaption.Text = SelectedControls(0).Caption
End Sub

    The purpose of this event is to get the existing property values from the ShapeLabel control or controls that are currently selected. That’s right, there may be more than one ShapeLabel control selected. Multiple selection is a wonderful thing for the user of your control, but it means more work for you.

    A property page receives a SelectionChanged event whenever it is opened. It also receives this event when the list of selected controls changes. This is necessary because the Property Pages dialog box is modeless, so a user may select additional controls while the dialog box is open.

    You have to decide how to handle multiple selection on a property-by-property basis. For example, if your property page displays the Width property of the first control in the SelectedControls collection — that is, SelectedControls(0), as shown in the code above — it will be easy for the user to change the widths of all the selected controls to that value.

    On the other hand, there is very little use in setting the captions of all the ShapeLabel controls on a form to the same value, so the logical thing to do with the Caption property is to disable txtCaption if the Count property of the SelectedControls collection is greater than one.

    However, this procedure doesn’t do the logical thing. For illustration purposes, the property page will be allowed to set multiple captions. Later, if you want to enable the behavior described above, you can add the following lines of code to the PropertyPage_SelectionChanged event procedure:

       ' Please don't do this yet!
   If SelectedControls.Count > 1 Then
      txtCaption.Enabled = False
   Else
      txtCaption.Enabled = True
   End If
  4. To set the property values for all currently selected controls, add the following code to the ApplyChanges event:
    Private Sub PropertyPage_ApplyChanges()
   ' Use a generic Object variable, in case more
   '   than one kind of control is selected.
   Dim objControl As Variant
   For Each objControl In SelectedControls
      objControl.Caption = txtCaption.Text
   Next
End Sub

    Your property page receives the ApplyChanges event when the user clicks the Apply or OK buttons of the Property Pages dialog box, or switches to another tab.

    How do you know that every control in SelectedControls has a Caption property? As the designer of the control component, you determine which property pages are connected to any given control. A property page will only appear if all the currently selected controls have that page in their Property Pages list. The easiest thing to do is to make sure that the pages assigned to each control don’t show properties the control doesn’t have.

    If you wish to use a general-purpose property page for a number of controls, and some of those controls don’t have all the properties displayed on the page, you can add code to the ApplyChanges event to test the type of the control, and apply the property value as appropriate. Alternatively, you can use an On Error statement to trap and ignore errors from controls that don’t have the property.

    You only need to be concerned with the controls in your component, because controls that are not part of your component will never use your component’s property pages.

    "Creating Property Pages for ActiveX Controls" discusses property page layout and assignment in greater detail.

  5. To enable the Apply button of the Property Page dialog box when the Caption property is changed, add the following code to the Change event of the txtCaption text box:
    Private Sub txtCaption_Change()
   ' The Changed property of the property page
   '   controls the Apply button of the Property
   '   Pages dialog box.
   Changed = True
End Sub
  6. In the Project window, double-click SLGeneral to bring the property page designer to the front. Click the designer’s Close button or press CTRL+F4 to close the designer and put the page in run mode. Like UserControl objects, PropertyPage objects must run while the rest of the project group is in design mode.

To connect the property page to the ShapeLabel control

  1. In the Project Explorer window, double-click ShapeLabel to open the designer.

  2. In the Properties window, double-click the PropertyPages property to display the Connect Property Pages dialog box.

    The Connect Property Pages dialog box can be used to connect multiple pages to a user control, and to control the display order of the tabs in the Property Pages dialog box for your control.

    Property pages can also be connected at run time. This is discussed in "Creating Property Pages for ActiveX Controls."

  3. Check SLGeneral, and then click OK.

  4. Bring the ShapeLabel designer to the front, then click its Close button or press CTRL+F4 to put the ShapeLabel control in run mode.

  5. In the Project Explorer window, double-click Form1 to open its designer.

  6. Right-click on one of the ShapeLabel controls on Form1, to show the context menu, and click Properties to show the Property Pages dialog box.

  7. In the Caption box on the General tab, replace the current caption with a new value. When you do this, the Apply button is enabled. Click the Apply button to change the caption of the control.

    Note You could also change the caption by pressing OK, but this would close the Property Pages dialog box. The dialog box should stay open for the next step.

  8. Hold down the CTRL key and click the second ShapeLabel control on Form1, so that both ShapeLabels are selected. Change the caption and click the Apply button to set both captions to the same value.

    You may want to try adding other controls, such as command buttons, to Form1, and observing the effects of different multiple selections on the Property Pages dialog box.

  9. When you’re done experimenting, click OK to close the Property Pages dialog box.

For More Information Property pages are discussed in detail in "Creating Property Pages for ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Adding an Event to the ShapeLabel Control
Start from the beginning | Creating an ActiveX Control


Adding an Event to the ShapeLabel Control

It’s important to distinguish between the events received by your UserControl object (or by the controls it contains) and the events your control raises. Events your control receives are opportunities for you to do something interesting; events your control raises provide opportunities for the developer who uses your control to do something interesting.

Figure 4.1 shows what happens when a control author simply uses the events received by the UserControl object, and doesn’t raise any events for the developer who buys the control.

Figure 4.1 An ActiveX control that simply uses events

Figure 4.2 shows what happens when the author of ControlDemo.ocx — no doubt tired of developer complaints — improves the ShapeLabel control by raising a Click event for the developer to respond to.

Figure 4.2 A control that raises events for the developer to use

There are many events that might be of interest to the user of the ShapeLabel control. The Visual Basic Label control raises a Click event, and ShapeLabel is just a fancy label, so the following procedure will add a Click event. To make the event more interesting, it will be raised only if the user clicks on the oval background.

Being compatible with other controls of the same type is an important reason to add a particular event to your control. Other criteria for choosing what events to raise can be found in "Raising Events from Controls," in "Building ActiveX Controls."

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

To add a Click event to the ShapeLabel control

  1. In the Project Explorer window, click ShapeLabel to select it, then press F7 or click the Code button on the Project Explorer window toolbar, to open the Code window.

  2. In the Object box, select (General). In the Procedure box, select (Declarations) to position yourself at the top of the code module. Add the following code:
    Option Explicit
' Declare a public Click event with no arguments.
Public Event Click()
  3. In the Object box, select lblCaption. In the Procedure box, select the Click event for the label control. Add the following code to the lblCaption_Click event procedure:
    Private Sub lblCaption_Click()
   ' Raise a Click event whenever the user clicks
   '   on the label.
   RaiseEvent Click
End Sub

    The code above raises a Click event only if the user clicks on the constituent control lblCaption. It will seem more natural to users to be able to click anywhere on ShapeLabel’s oval background, so the next step shows how to raise the click event if the user clicks on the colored oval.

  4. In the Object box, select UserControl. In the Procedure box, select the UserControl’s MouseUp event. Add the following code to the UserControl_MouseUp event procedure:
    Private Sub UserControl_MouseUp(Button As Integer, _
   Shift As Integer, X As Single, Y As Single)
   ' Raise a Click event only if the color of the
   '   point that was clicked on matches the color
   '   of the Shape control.  Ignore clicks that are
   '   outside the oval.
   If Point(X, Y) = shpBack.FillColor Then
      RaiseEvent Click
   End If
End Sub

    Determining whether an event occurred in a particular location is called hit testing.

    You might expect to put the hit test code in the shpBack_Click event procedure, because shpBack is always resized to cover the entire surface of the ShapeLabel control. However, Shape controls don’t receive Click events. Instead, the Click event is received by the object that contains the Shape — in this case, the UserControl object.

    "Drawing Your Control," in "Building ActiveX Controls," discusses the use of transparent backgrounds to create irregularly shaped controls.

  5. In the Project Explorer window, click Form1 to select it, then press F7 or click the Code button on the Project Explorer window toolbar, to open the Code window.

  6. In the Object box, select one of the ShapeLabel controls you added to Form1. In the Procedure box, select the Click event.

    Note If the Click event does not appear, make sure the ShapeLabel designer is closed.

    Add the following code to the ShapeLabel1_Click event procedure:

    Private Sub ShapeLabel1_Click()
   MsgBox "Thanks for clicking! My caption is: " _
      & ShapeLabel1.Caption
End Sub

    Note If the ShapeLabel you selected is not named ShapeLabel1, use the appropriate name when entering the code above.

    You can click the arrow on the Procedure box to view all of the events for the ShapeLabel control. In addition to your Click event, there are four events — DragDrop, DragOver, GotFocus, and LostFocus — that are automatically provided for you by the container, Form1.

  7. On the toolbar, click the Start button, or press CTRL+F5 to run TestCtlDemo. Try clicking various places on the form and on the ShapeLabel control, to verify that the Click event is being raised only when you click inside the oval background.

  8. There’s a subtle bug in the hit testing for ShapeLabel’s click event. To see this, press the mouse button while the mouse pointer is in the lower half of the red oval. Holding the mouse button down, carefully move the mouse pointer until the tip of the arrow is on the white text of ShapeLabel’s caption, then release the mouse button. The message box doesn’t appear!

    The lblCaption_Click event procedure doesn’t get executed, because the MouseDown event occurred over the UserControl. Therefore, when the MouseUp event occurs, it is received by the UserControl — even if the mouse has been moved completely off Form1.

    The hit test code in the MouseUp event works if the mouse button is released over the red background that shows through lblCaption’s transparent background, but not if the button is released over the white foreground color of the text. (If the button is released outside ShapeLabel, the Point function returns -1, so releasing the mouse button over some random red spot will not raise the Click event.)

    Fixing this bug is left as an exercise for the reader. (Hint: Moving the hit test to the Click event of the UserControl won’t help, because the Click event doesn’t occur when the MouseUp event is over a different object from the MouseDown.)

For More Information See "Adding Events to Controls" in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Compiling the ControlDemo Component
Start from the beginning | Creating an ActiveX Control


Compiling the ControlDemo Component

Once you have created an ActiveX control project containing one or more UserControl objects, you can compile it into an .ocx file and use the controls in other applications. The following procedures demonstrate this.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

Compiling the ControlDemo project

  1. If the TestCtlDemo project is still in run mode, click the Close button on Form1 to return to design mode.

  2. In the Project Explorer window, click ControlDemo to select the project.

  3. On the File menu, click Make ControlDemo.ocx to open the Make Project dialog box. Click OK to build the .ocx file.

  4. On the File menu, click Remove Project to remove ControlDemo from the project group, so that Visual Basic will use the compiled binary component (.ocx file) instead of the project.

    Visual Basic displays a warning message, because the TestCtlDemo project contains a reference to ControlDemo. Click Yes to remove ControlDemo anyway.

    When you remove ControlDemo from the project group, Visual Basic looks for ControlDemo.ocx in the Windows Registry. If the .ocx file exists, Visual Basic automatically updates the reference you set in the procedure "Adding the TestCtlDemo Project."

    To switch back to using the project instead of the binary component, you can click Add Project, on the File menu, and add the ControlDemo project back to the project group.

  5. Press F5 to run TestCtlDemo using the .ocx file.

When ControlDemo is running from source code, you cannot access the ShapeLabel control from other applications, or from another copy of Visual Basic. This is because ActiveX control components must run in process. Once you have compiled a .ocx component, you can test it from other applications.

To use ControlDemo.ocx in another copy of Visual Basic

  1. Open a new instance of Visual Basic. In the New Project dialog box, double-click the Standard EXE icon to open an .exe project.

  2. On the Project menu, click Components to open the Components dialog box. On the Controls tab, check ActiveX Control Creation Demo, and then click OK.

    The icon for ShapeLabel appears on the Toolbox. You can now add ShapeLabel controls to the default form, and use the Properties window to set their properties. You can also right-click on an instance of ShapeLabel, and choose Properties from the Context menu to edit the control’s properties with the property page.

  3. Press F5 to run the project.

    You can also compile the project and run the .exe.

For More Information An .ocx file can contain multiple controls and property pages. "Distributing Controls," in "Building ActiveX Controls," discusses control packaging and distribution.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Control Creation Recap
Start from the beginning | Creating an ActiveX Control


Control Creation Recap

In order to introduce new concepts in the most natural order, the procedures in this chapter have not followed the normal sequence of steps for creating a new control.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Control.

When you create a new control, the steps you’ll generally follow are these:

  1. Determine the features your control will provide.

  2. Design the appearance of your control.

  3. Design the interface for your control — that is, the properties, methods, and events your control will expose.

  4. Create a project group consisting of your control project and a test project.

  5. Implement the appearance of your control by adding controls and/or code to the UserControl object.

  6. Implement the interface and features of your control.

  7. As you add each interface element or feature, add features to your test project to exercise the new functionality.

  8. Design and implement property pages for your control.

  9. Compile your control component (.ocx file) and test it with all potential target applications.

If your control component will provide more than one control, you should begin by deciding what controls the package will include. Your test project should have separate test forms for the individual controls, and at least one form that tests the controls together.

For More Information General design issues for ActiveX components are discussed in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components." Issues exclusive to ActiveX control creation, testing, packaging, and deployment are discussed in "Building ActiveX Controls" and "Creating Property Pages for ActiveX Controls."


Creating an ActiveX Document

The series of step-by-step procedures in this chapter builds a simple combination of two ActiveX documents and two forms. The project is not intended to be a functioning application, but building it will quickly demonstrate object lifetime, global objects, and a set of components working together in Internet Explorer. You will also see how to debug an ActiveX document using Visual Basic's debugging tools.

All of the subjects introduced in these procedures are covered in greater depth in later chapters. References to in-depth material will be found in each procedure. In addition, "Building ActiveX Documents" shows how you can use the ActiveX Document Wizard to make building an ActiveX document even easier.

Note To complete the step-by-step procedures in this chapter, you must have Microsoft Internet Explorer and access to the Internet or an intranet. You can install Internet Explorer from the Tools directory of your Visual Basic CD-ROM.

The procedures for creating the ActXDoc project build on each other, so the sequence in which you perform the procedures is important.

Topics

Creating the ActXDoc Project

Shows how to create a single ActiveX document in the ActXDoc project.
Running and Debugging the ActXDoc Project

Explains how to run the ActXDoc project and view the ActiveX document in Internet Explorer.
Adding a Second ActiveX Document to the ActXDoc Project

Shows how to add a second ActiveX document named SecndDoc to the project.
Adding a Form to the ActXDoc Project

Illustrates how to add a standard form to the project.
Adding a Property to the UserDocument

Explains how to add a public property and two methods to the ActiveX document. These are exposed and can be used by the SecndDoc ActiveX document or by a form.
Saving Properties to the PropertyBag

Illustrates how to save property values of the UserDocument object to the PropertyBag through the WriteProperties event and retrieve property values through the ReadProperties event.
Adding a Menu to the ActXDoc Project

Shows how to add a menu that will be visible in the host application to the FirstDoc ActiveX document.
Life Cycle of a UserDocument

Explains the sequence of events for an ActiveX document
ActiveX Document Creation Recap

Outlines the typical process of designing and creating an ActiveX document.

Sample application

ActXDoc.vbp

This application fills in all the properties, methods, and events required to create an ActiveX document. It shows additional ActiveX document creation features, at the expense of some of the basic material covered in the step-by-step procedures. The sample applications are listed in the Samples directory.


Creating the ActXDoc Project

The first step in authoring the ActXDoc project is to add a single UserDocument object to an ActiveX EXE project. A CommandButton and a TextBox control are then added to the form. Finally, using the Hyperlink object, you add code that navigates to another URL.

The step-by-step procedures in this chapter build a document object and test it in Internet Explorer, which is included with Visual Basic.

Note This topic is part of a series that walks you through creating a sample ActiveX document. It begins with the topic Creating an ActiveX Document.

To create an ActiveX document

  1. On the File menu, click New Project to open the New Project dialog box. (This will close your current project or project group; you will be prompted to save any changes you have made.) Double-click the ActiveX Document EXE icon.

    Visual Basic automatically adds a UserDocument designer to the project. If the designer is not immediately visible, right-click over its default name, UserDocument1, in the Project Explorer window, then click View Object. When the designer appears, its default name is visible in the title bar.

  2. In the Properties window, double-click the Name property and change the name of UserDocument1 to FirstDoc. The new name appears in the title bar of the designer, and in the Project Explorer window.

  3. In the Project Explorer window, click the Project1 icon. In the Properties window, double-click Name, and change the name of the project to ActXDoc. The title bar of the designer now reads "ActXDoc - FirstDoc (UserDocument)."

  4. In the Project Explorer window, double-click the FirstDoc icon to bring it to the front.

  5. On the Toolbox, double-click the CommandButton icon to put a CommandButton control on the designer.

  6. On the Toolbox, double-click the TextBox icon to put a Textbox control on the designer.

  7. Place the two controls as shown in the following illustration:

  8. Change the appropriate properties of the two controls according to the following table:
    Command1 property | Value
    Name | cmdNavigateTo
    Caption | Navigate To

    Text1 property | Value
    Name | txtURL
    Text | http://www.microsoft.com

  9. On the FirstDoc designer, double-click the Navigate To CommandButton control to add the following code to its Click event:
    Private Sub cmdNavigateTo_Click()
   ' Use the Hyperlink object method NavigateTo
   ' to go to the URL in txtURL.
   Hyperlink.NavigateTo txtURL.Text
End Sub

    Important When using the NavigateTo method, you must include the protocol portion of the URL. For example, by default, the above code will navigate to the value of the Text property of the txtURL TextBox control — "http://www.microsoft.com".

  10. On the File menu, click Save Project to save the project files. Name them as shown in the following table. Visual Basic will provide the indicated extensions automatically.
    File | File name | Extension
    User document | FirstDoc | .dob
    Project | ActXDoc | .vbp

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Running and Debugging the ActXDoc Project
Start from the beginning | Creating an ActiveX Document


Compiling and Debugging the ActXDoc Project

Running and Debugging the ActXDoc Project

In order to view the FirstDoc document, you must first run the ActXDoc project, then run another container application in which to view the FirstDoc document. In this procedure, you will run Internet Explorer (included with Visual Basic), and navigate to a .vbd file to open the ActiveX document (a .vbd file is automatically generated for every ActiveX document in a project whenever the project is run).

Note This topic is part of a series that walks you through creating a sample ActiveX document. It begins with the topic Creating an ActiveX Document.

To view the FirstDoc ActiveX document

  1. Run the project by pressing F5.

    The project is now running. If you use Windows Explorer to view the directory where Visual Basic resides, you will find a FirstDoc.vbd file.

  2. If this is the first time you've run the project, the Project Properties dialog box will appear. Click Start Browser with URL, and type the path of the FirstDoc.vbd file, which should be in the directory where you installed Visual Basic. Internet Explorer will open and display your ActiveX document.

  3. Note in the text box that the default URL is http://www.microsoft.com. You can replace this URL with another of your choice.

  4. Click the Navigate To button. The Web page at the URL in the text box will appear in the browser.

Debugging the FirstDoc ActiveX Document

Debugging an ActiveX document is similar to debugging other ActiveX components. You can use all the tools available in Visual Basic — setting breakpoints, watching variables, using debug statements, and so on.

It's also important to remember that the container hosting the ActiveX document is its client — using objects the ActiveX document provides. While the host container is accessing the ActiveX document, stopping the project will cause an error in the host container. To avoid this, at the end of each of these procedures you will quit Internet Explorer to release the reference.

For More Information For alternatives that cause Internet Explorer to release your ActiveX document, see "Debugging ActiveX Documents" in "Building ActiveX Documents."

Putting Your Project Into Break Mode

If you are running a project and are viewing it in Internet Explorer, you can put it into break mode (by pressing CTRL+BREAK) without causing any errors in the host application. However, be aware that modifying any code that causes Visual Basic to reset the project should be avoided.

To add a Stop statement to an ActiveX document

  1. In Internet Explorer, click the Back button to return to the FirstDoc ActiveX document.

  2. Press alt+tab to switch back to Visual Basic.

  3. Press CTRL+BREAK to pause the program.

  4. In the Code window, find the Click event for the cmdNavigateTo button, and place a Stop statement before the existing line of code, as shown:
    Private Sub cmdNavigateTo_Click()
   Stop ' <--- Add this to the procedure.
   Hyperlink.NavigateTo txtURL.Text
End Sub
  5. Press f5 to continue.

  6. Press alt+tab to return to Internet Explorer.

  7. Click Navigate To. Visual Basic will return to the front of your desktop with the Code window visible, and the Stop statement highlighted.

  8. Press f5 to continue. Note that the Web page has been revisited.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Adding a Second ActiveX Document to the ActXDoc Project
Start from the beginning | Creating an ActiveX Document


Adding a Second ActiveX Document to the FirstDoc Project

Adding a Second ActiveX Document to the ActXDoc Project

Having one ActiveX document in a project is equivalent to having a single form in a project — somewhat limiting. To increase your capabilities, add a second ActiveX document to the ActXDoc project.

Unlike standard Visual Basic forms, you cannot use the Show method to show ActiveX documents. This is because the container application (in this case, Internet Explorer) determines when to show or hide the ActiveX document. Instead, you must navigate from one ActiveX document to another. To navigate between the two ActiveX documents, use the HyperLink object and its NavigateTo method.

Note This topic is part of a series that walks you through creating a sample ActiveX document. It begins with the topic Creating an ActiveX Document.

To add a second ActiveX document to the ActXDoc project

  1. Before adding a second ActiveX document to the project, add a CommandButton control to the FirstDoc designer. On the Toolbox, double-click the CommandButton icon to add a CommandButton control to the FirstDoc designer.

  2. Set the appropriate properties for the control using the following table:
    Command1 property | Value
    Name | cmdGoNext
    Caption | Go Next

  3. Position the control on the FirstDoc designer as shown.

  4. Double-click the Go Next button and add the following code to the button's Click event:
    Private Sub cmdGoNext_Click()
   ' Note: the following path may not correspond to
   ' the actual path to the SecndDoc.vbd file on
   ' your machine.
   HyperLink.NavigateTo _
   App.Path & "\SecndDoc.vbd"
End Sub

    Note The App.Path code only works if you keep ActXDoc project in the same directory where you installed the Visual Basic application. If you save the project to another directory, replace App.Path & "\SecndDoc.vbd" with the hard-coded path of the SecndDoc.vbd file.

    When you are running the project, Visual Basic always creates a temporary .vbd file for each ActiveX document in your project, and the .vbd files will always be found in the same directory where Visual Basic has been installed. However, if you are compiling an .exe or .dll, Visual Basic will create the .vbd file in the same directory as the .exe or .dll file.

  5. This next step will insert a second ActiveX document into the project. On the Project menu, click Add UserDocument to open the Add User Document dialog box.

  6. Double-click the User Document icon to add an ActiveX document to the project.

  7. On the Properties window, double-click Name and change the name of the new UserDocument object to SecndDoc. The caption should now read "ActXDoc - SecndDoc (UserDocument)."

  8. Add Label and CommandButton controls to the SecndDoc User document, and use the following tables to set their properties.
    Label1 property | Value
    Name | lblCaption
    Caption | SecndDoc

    Command1 property | Value
    Name | cmdGoBack
    Caption | Go Back

  9. Position the controls on the SecndDoc User document as shown:

  10. Add the following code to the Click event of the cmdGoBack button:
    Private Sub cmdGoBack_Click()
   UserDocument.HyperLink.GoBack
End Sub

    The HyperLink object features the GoBack method, which will navigate back to the previous document in the browser.

  11. On the File menu, click Save Project to save the project files. Name them as shown in the following table. Visual Basic will provide the indicated extensions automatically.
    File | File name | Extension
    User document | SecndDoc | .dob

Running the Project

Now that you have added a second ActiveX document, you can run the project and navigate between the two.

  1. Run the project by pressing f5. Internet Explorer will be launched.

  2. In Internet Explorer, type the path of the FirstDoc.vbd file in the Address box.

Tip If you previously opened the FirstDoc ActiveX document in Internet Explorer, click the arrow next to the Address box to see a list of recent URLs. The FirstDoc document should be available from the drop-down list.

  3. Click Go Next. You will now be on the SecndDoc document.

  4. On the SecndDoc document, click Go Back to return to the FirstDoc ActiveX document.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Adding a Form to the ActXDoc Project
Start from the beginning | Creating an ActiveX Document


Adding a Form to the ActXDoc Project

As with other Visual Basic projects, it's a simple matter to add a form to the FirstDoc ActiveX project. Adding controls to the form is exactly the same as in a standard Visual Basic project. The primary difference, of course, lies in the fact that forms must be called by the ActiveX document.

In this step-by-step procedure, you add a single form to the ActXDoc project.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Document.

To add a form to the ActXDoc project

  1. On the Project menu, click Add Form, then double-click the Form icon in the Add Form dialog box. Set the form's properties using the following table:
    Form1 property | Value
    Name | frmAux
    Caption | Auxiliary Form

  2. On the Toolbox, double-click the TextBox icon to add a TextBox control to the form, and set its properties using the following table.
    Text1 property | Value
    Name | txtAux
    MultiLine | True
    Text | (nothing)

  3. Resize the form, and place the control on the form so it resembles the following figure:

  4. In the Project Explorer window, double-click the FirstDoc icon to bring its designer forward.

  5. On the Toolbox, click the CommandButton icon and draw a new button on the FirstDoc designer, and set its properties according to the following table:
    Command1 property | Value
    Name | cmdShowForm
    Caption | Show Form

  6. Place the control just below the Go Next button as shown:

  7. Double-click the Show Form CommandButton control, and add the following code to its Click event:
    Private Sub cmdShowForm_Click()
   ' Show the auxiliary form, and set the Text
   ' property of txtAux to the URL of FirstDoc.
   frmAux.txtAux.Text = txtURL.Text
   frmAux.Show vbModal
End Sub

    Important Some containers, such as Internet Explorer, don't support the showing of modeless forms called from within a DLL. If you want to show modeless forms, compile your ActiveX document as an .exe file. This will allow your applications to show both modeless and modal forms.

  8. On the File menu, click Save Project to save the project files. Visual Basic will prompt you to name the form as its default (frmAux). Click OK to save the form with the default name.

Running the Project

You can now run the project and observe the interaction of the form with the ActiveX document.

To view the new form interacting with the ActiveX document

  1. Press f5 to run the project.

  2. Click the Address box and type the path of the FirstDoc.vbd.

  3. Click the Show Form button to see the form.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Adding a Property to the UserDocument
Start from the beginning | Creating an ActiveX Document


Adding a Property to the UserDocument

As with the UserControl object, you can create public properties for UserDocument objects. When you create a public property, you expose the property so other applications can set or get its value. In the ActXDoc project, you'll create a single property that can be accessed by either another ActiveX document or by a form. In this procedure, you will also add a code module to the project to contain a single global module.

Note This topic is part of a series that walks you through creating a sample ActiveX document. It begins with the topic Creating an ActiveX Document.

To add a property to FirstDoc

  1. First add a code module to the project. On the Project menu, click Add Module to open the Add Module dialog box. Then double-click the Module icon to add a new code module to the ActXDoc project.

  2. In the Properties window, double-click Name and change the name of the module to mGlobal.

  3. In the Project Explorer window, double-click the module icon and add the following code to the Declarations section:
    ' Object variable for FirstDoc
Public gFirstDoc As FirstDoc

    This global variable will contain the object reference that links the FirstDoc and SecndDoc ActiveX documents.

  4. In the Project Explorer window, double-click the FirstDoc icon.

  5. On the Toolbox, double-click the TextBox icon to add a new TextBox control to FirstDoc. Set its properties according to the following table.
    Text1 property | Value
    Name | txtFirstDoc
    Text | (nothing)

  6. Position the TextBox control below the first TextBox already present on the UserDocument, as shown:

  7. Double-click the FirstDoc designer to open its Code window.

  8. On the Tools menu, click Add Procedure.

  9. In the Add Procedure dialog box, type "strDocProp" into the Name box, and click the Property option to create a public property; then click OK.

  10. Visual Basic will add code for Property Get and Property Let procedures to the module. However, modify the code as shown:
    Public Property Get strDocProp() As String
   ' Note: in the line above, change the return type
   ' of the property from "As Variant" to
   ' "As String."
   strDocProp = txtFirstDoc.Text
End Property

Public Property Let strDocProp(ByVal _
NewStrDocProp As String)
   ' Note: in the line above, change the argument
   ' type from Variant to String.
   txtFirstDoc.Text = NewStrDocProp
End Property

    The code above exposes the strDocProp property as a public property of the FirstDoc ActiveX document. In other words, it delegates to the Text property of the TextBox control the work of displaying and storing the string. Now that the property is public, you can pass its value to the SecndDoc object. You will do this by modifying the code for the Go Next command button.

  11. In the Code window, modify the Click event of the cmdGoNext button to include a Set statement with the global object gFirstDoc. When you set the object variable to Me, you create a reference to the FirstDoc ActiveX document. This reference can then be used to access the public properties and methods of the document.
    Private Sub cmdGoNext_Click()
   ' Note: the following path may not correspond to
   ' the actual path to the SecndDoc.vbd file on
   ' your machine.
   Set gFirstDoc = Me ' <-- Add this line.

   HyperLink.NavigateTo _
   App.Path & "\SecndDoc.vbd"
End Sub
  12. On the Project Explorer window, double-click the SecndDoc icon to bring its designer forward.

  13. Double-click the SecndDoc designer to open its Code window and add the following code to the UserDocument object's Show event.
    Private Sub UserDocument_Show()
   If Not gFirstDoc Is Nothing Then
      lblCaption.Caption = gFirstDoc.strDocProp
      Set gFirstDoc = Nothing
   End If
End Sub

    In the code in step 11, you set the global object variable to the FirstDoc document. The code in step 13 tests to see if the global variable is set to an object. If it is, then the public properties of the ActiveX document are available, and the caption of the label is set to the strDocProp property from the FirstDoc document. Immediately after setting the Caption property, the global variable is destroyed (set to Nothing).

    Note "It is bad practice to allow the global variable to retain a reference to the FirstDoc UserDocument; the reasons for this are covered in the "Building ActiveX Documents."

  14. On the File menu, click Save Project, and save the code module as mGlobal.bas.

Running the Project

  1. Press f5 to run the project.

  2. Type the path of the FirstDoc.vbd in the Address box of Internet Explorer.

  3. Type something distinctive into the new TextBox control such as "HTML was never this easy."

  4. Click Go Next. The text that was in the TextBox control will appear in the label of the SecndDoc document.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Saving Properties to the PropertyBag
Start from the beginning | Creating an ActiveX Document


Saving Properties to the PropertyBag

When the end user of an ActiveX document views a document, there may be several text boxes that she or he will fill out as part of your application. If it's possible that the user will return repeatedly to the document, you may wish to save some time by saving the data to the PropertyBag.

In the following example, the PropertyChanged statement is inserted into the Change event of the TextBox (txtFirstDoc) control. This causes Internet Explorer to prompt the user to save changes. If the user responds positively, the code writes the property value to the PropertyBag.

For More Information To learn more about the PropertyBag, see "Saving the Properties of Your Control" in "Building ActiveX Controls."

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Document.

To write and read a property to the PropertyBag

  1. In the Project Explorer window, double-click the FirstDoc icon to bring its designer forward.

  2. Double-click the TextBox (txtFirstDoc) control to view its code.

  3. In the Code window, from the drop-down list of events for txtFirstDoc, click Change. Add the PropertyChanged statement, as shown:
    Private Sub txtFirstDoc_Change()
   PropertyChanged
End Sub
  4. Add the following code to the UserDocument object's WriteProperties event:
    Private Sub UserDocument_WriteProperties _
(PropBag As VB.PropertyBag)
   PropBag.WriteProperty "StrDocProp", _
   txtFirstDoc.Text, "Hello"
   Debug.Print "WriteProperties"
End Sub
  5. The code in step 4 saves the value of the Text property of the TextBox control to the PropertyBag. To retrieve the value, add the following code to the ReadProperties event:
    Private Sub UserDocument_ReadProperties _
(PropBag As VB.PropertyBag)
   txtFirstDoc.Text = _
   PropBag.ReadProperty("StrDocProp", _
   "Hello")
   Debug.Print "ReadProperties"
End Sub

Running the Project

Now that the property can be written to and read from the PropertyBag, run the project to test it.

  1. Press f5 to run the project.

  2. Click the Address box and type the path of the FirstDoc.vbd.

  3. Type some distinctive text, such as your name, into the blank TextBox control.

  4. On the File menu, click Close. Internet Explorer will present a dialog box asking if you would like to save the your changes. Click Yes.

  5. Run Internet Explorer again.

  6. Click the Address box and type the path of the FirstDoc.vbd. The text you typed will be present in the TextBox control.

Using the PropBag in conjunction with the WriteProperties and ReadProperties events, you can easily persist data for your object.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Adding a Menu to the ActXDoc Project
Start from the beginning | Creating an ActiveX Document


Add a Menu to the ActiveX Document

Adding a Menu to the ActXDoc Project

You can add menus to your ActiveX document using the Menu Editor. Because an ActiveX document alone cannot have a menu, the menu you create will be merged with the application you used to view the ActiveX document. Thus, you must consider menu negotiation when adding a menu to an ActiveX document.

When users navigate to your ActiveX document, they may not have any indication of its origin (or that it is an ActiveX document). To remedy this, you should always include an "About" form with your ActiveX document.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Document.

To add a menu and About box to the FirstDoc ActiveX document

  1. On the Project menu, click Add Form.

  2. In the Add Form dialog box, double-click the About Dialog icon to add an About Box form to the project.

  3. Set Caption property of the form and controls according to the following table.
    Object | Caption value
    frmAbout | About FirstDoc
    lblTitle | FirstDoc ActiveX Document
    lblVersion | Version 1.0
    lblDescription | ActiveX document

  4. Delete the Label control named lblDisclaimer.

  5. In the Project Explorer window, double-click FirstDoc to bring its designer forward.

  6. Click the FirstDoc designer to select it, and on the Tools menu, click Menu Editor to display the menu editor dialog box.

  7. Click the Caption box and type &Help.

  8. Click the Name box and type mnuHelp.

  9. Click the NegotiatePosition box, and click Right.

  10. Click the Next button to create a new menu item.

  11. In the Caption box type About FirstDoc.

  12. In the Name box type mnuAbout.

  13. Click the right-facing arrow button to indent the menu item.

  14. Click OK.

  15. Double-click the FirstDoc designer to bring its Code window to the front.

  16. Add the following code to the mnuAbout Click event:
    Private Sub mnuAbout_Click()
   frmAbout.Show vbModal
End Sub

Running the Project

Once you have added a menu to the UserDocument, you can run the project, view the ActiveX document in Internet Explorer, and see the how the menu has been negotiated.

  1. Run the project by pressing f5.

  2. In Internet Explorer, type the path of the FirstDoc.vbd file in the Address box or select it from the drop-down list of previously typed addresses.

  3. On Internet Explorer's command bar, click the Help menu, and among the menu items will be the FirstDoc Help menu item.

  4. Click About FirstDoc to see the new About box.

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX document.

To | See
Go to the next step | Life Cycle of a UserDocument
Start from the beginning | Creating an ActiveX Document


Life Cycle of a UserDocument

The life of an ordinary Visual Basic form is marked by certain key events, such as Initialize, Load, QueryUnload, and Unload. In order to create well-behaved applications, it’s important to know when these events occur in the life cycle of a form.

Although ActiveX documents look like forms, they behave differently, primarily because they are contained by another application. When programming an ActiveX document, some attention must be given to the ephemeral nature of an ActiveX document. This is especially true when the container application is a Web browser, such as Internet Explorer.

Key events in the life cycle of a UserDocument object include Initialize, InitProperties, Show, Hide, and Terminate. The following procedure demonstrates these events.

Note This topic is part of a series that walks you through creating a sample ActiveX control. It begins with the topic Creating an ActiveX Document.

To observe the key events of the FirstDoc UserDocument

  1. In the Project Explorer window, double-click FirstDoc to bring its designer forward.

  2. Double-click the designer to open a Code window for the UserDocument object, and enter code in the following event procedures:
    Private Sub UserDocument_Initialize()
   Debug.Print "Initialize"
End Sub

Private Sub UserDocument_InitProperties()
   Debug.Print "InitProperties"
End Sub

Private Sub UserDocument_Show()
   Static intCount As Integer
   intCount = intCount + 1
   Debug.Print "Show " & intCount
End Sub

Private Sub UserDocument_Hide()
   Static intCount As Integer
   intCount = intCount + 1
   Debug.Print "Hide " & intCount
End Sub

Private Sub UserDocument_Terminate()
   Debug.Print "Terminate"
End Sub
  3. Run the project by pressing f5.

  4. In Internet Explorer, view the ActiveX document by navigating to the URL of the FirstDoc.vbd file, or by clicking the arrow next to the Address box and selecting FirstDoc.vbd from the list.

  5. Press ALT+TAB to bring Visual Basic back to the front of your window. Notice in the Immediate window that three events — Initialize, InitProperties, and Show — have been printed to the window. As with the UserControl object, the Intialize event precedes InitProperties.

    Note You will notice that Show is followed by a 1, indicating that the ActiveX document has been shown once by Internet Explorer. As long as a reference to the ActiveX document exists in the Internet Explorer cache, this number will increment once each time you navigate to the ActiveX document from another URL.

  6. Press ALT+TAB to return to Internet Explorer. Navigate another URL, either by typing a valid URL into the Address box of Internet Explorer, or using your list of Favorites. It does not matter which HTML page you navigate to.

  7. After navigating to the new URL, press ALT+TAB to switch back to Visual Basic. In the Immediate window you will notice that the Hide and Terminate events have occurred.

  8. Press ALT+TAB to return to Internet Explorer. Click Back until the FirstDoc ActiveX document is once again in view.

  9. Press ALT+TAB again to switch back to Visual Basic. Notice in the Immediate window that the Initialize, InitProperties, and Show events have been fired.

The Initialize and InitProperties Events

The Initialize and InitProperties events have some similarities, but you should be aware of how they differ. In brief, the Initialize event always occurs when the ActiveX document is loaded, while InitProperties will only occur every time until the document is saved. After that event has fired, the ReadProperties and WriteProperties events will fire. To see an example of this:

  1. If Internet Explorer is still running, close it.

  2. If the ActXDoc project is still running, stop it. This is necessary in order to see the InitProperties event occur.

  3. Restart the ActXDoc project.

  4. In Internet Explorer type the path of the FirstDoc.vbd file in the Address box , or select it from the drop-down URL history list.

  5. Type something distinctive into the TextBox (txtFirstDoc) control.

  6. Close Internet Explorer. You will be prompted to save your changes. Click Yes.

  7. Restart Internet Explorer. (Do not stop the ActXDoc project.)

  8. Press ALT+TAB to return to Visual Basic.

  9. Note in the Immediate window that InitProperties occurred only once: the first time that the FirstDoc document was viewed in Internet Explorer. The second time you viewed the FirstDoc document the Initialize event occurred, but not the InitProperties; instead, the ReadProperties event occurred.

    You should note this behavior, as it impacts where you place your code. Needless to say, if you always want a procedure to run on startup, you should put it into the Initialize event. If you want a procedure to run only the first time a user views your ActiveX document, place it in the InitProperties event.

    You should be cautious, however, about the limitations of using the Initialize event. In brief, any procedures that require knowledge of the container (such as the Parent property) are unavailable when the Initialize event occurs. An alternate event that occurs after the ActiveX document has been sited in the container is the Show event.


ActiveX Document Creation Recap

When you create a new ActiveX document, the steps you’ll generally follow are:

  1. Determine the features your document will provide.

  2. Design the appearance of your document.

  3. Design the interface for your document; that is, the properties, methods, and events your document will expose.

  4. Create a project consisting of your user document and any auxiliary forms.

  5. Add controls and/or code to the UserDocument object.

  6. Implement the interface and features of your document.

  7. Compile your document to create a .vbd file and test it with all potential target applications.


General Principles of Component Design

This chapter and "Debugging, Testing, and Deploying Components" contain those topics that apply to all types of ActiveX components. These chapters provide necessary background for the in-depth treatment of component types in subsequent chapters.

This chapter begins with "Component Basics," a group of topics that explain key terminology and concepts of component design.

The rest of the topics in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components" are organized according to the general sequence of development tasks for components:

  1. Determine the features your component will provide.

  2. Determine what objects are required to divide the functionality of the component in a logical fashion.

    See "Adding Classes to Components."

  3. Design any forms your component will display.

  4. Design the interface — that is, the properties, methods, and events — for each class provided by your component.

    See "Adding Properties and Methods to Classes," "Adding Events to Classes," "Providing Named Constants for Your Component," "Providing Polymorphism by Implementing Interfaces," and "Organizing Objects: The Object Model."

    The remainder of the task-oriented topics are contained in "Debugging, Testing, and Deploying Components." In addition to the following development tasks, they cover distribution, version compatibility, and creating international versions of your component.

  5. Create the component project and test project.

  6. Implement the forms required by your component.

  7. Implement the interface of each class, provide browser strings for interface members, and add links to Help topics.

  8. As you add each interface element or feature, add code to your test project to exercise the new functionality.

  9. Compile your component and test it with all potential target applications.

Topics

Component Basics

Component creation terminology and concepts; the limitations of component project types; startup and shutdown rules.
Adding Classes to Components

Class module basics for components, including instancing models, default properties, and robustness.
Adding Properties and Methods to Classes

Interface How To’s, including deciding whether a member is a property or method, private communication using Friend methods, and allowable data types.
Adding Events to Classes

Declaring, raising, and handling events in classes provided by your component.
Providing Named Constants for Your Component

How to add named constants to your component’s type library.
Private Communications Between Your Objects

The objects provided by your component may need to exchange information or perform actions you don’t want to expose to users of your component.
Providing Polymorphism by Implementing Interfaces

When several objects implement the same interface, client applications can gain improved performance through polymorphism.
Organizing Objects: The Object Model

Make it easier for people to use your component by providing relationships between the objects your component provides. Further implications of Instancing.


Component Basics

A software component created with Microsoft Visual Basic is a file containing executable code — an .exe, .dll, or .ocx file — that provides objects other applications and components can use.

An application or component that uses objects provided by other software components is referred to as a client. A client uses the services of a software component by creating instances of classes the component provides, and calling their properties and methods.

In earlier versions of Visual Basic, you could create components called OLE servers. The features of components created with Visual Basic are greatly expanded, including the ability to raise events, improved support for asynchronous callbacks, and the ability to provide ActiveX controls and documents.

The following topics provide key definitions and concepts you should be aware of when developing any software component using Visual Basic.

  • In-Process and Out-of-Process Components Definitions and key differences.

  • What’s in a Name? The names you choose for objects, events, properties, and methods affect the usability of your component.

  • Choosing a Project Type and Setting Project Properties Decide which project type to open when starting a new component, based on what features you intend the component to provide.

  • Polymorphism, Interfaces, Type Libraries, and GUIDs An interface of a class provided by your component is a contract between you and the user, documented in your type library. Classes can have multiple interfaces, identified by GUIDs.

  • Starting and Ending a Component Startup and shutdown rules all component designers need to know.

For More Information The core information on creating OLE servers, referred to as code components, includes "Creating an ActiveX DLL," "Creating an ActiveX EXE Component," "General Principles of Component Design," "Debugging, Testing, and Deploying Components," "Building Code Components," and "ActiveX Component Standards and Guidelines."


In-Process and Out-of-Process Components

An application or component that uses objects provided by another component is called a client.

Components are characterized by their location relative to clients. An out-of-process component is an .exe file that runs in its own process, with its own thread of execution. Communication between a client and an out-of-process component is therefore called cross-process or out-of-process communication.

An in-process component, such as a .dll or .ocx file, runs in the same process as the client. It provides the fastest way of accessing objects, because property and method calls don’t have to be marshaled across process boundaries. However, an in-process component must use the client’s thread of execution.

For More Information "Building Code Components" discusses in-process and out-of-process components in depth, including thread-safe DLLs and multithreaded EXE components. Control components (.ocx files) are always in-process, as discussed in depth in "Building ActiveX Controls." Components that provide active documents can run in process or out of process, as discussed in "Building ActiveX Documents."


What's in a Name?

What’s in a Name?

The names you select for your class modules and for their properties, methods, and events make up the interface(s) by which your component will be accessed. When naming these elements, and their named parameters, you can help the user of your component by following a few simple rules.

  • Use complete words whenever possible, as for example "SpellCheck." Abbreviations can take many forms, and hence can be confusing. If whole words are too long, use complete first syllables.

  • Use mixed case for your identifiers, capitalizing each word or syllable, as for example ShortcutMenus, or AsyncReadComplete.

  • Use the same word your users would use to describe a concept. For example, use Name rather than Lbl.

  • Use the correct plural for collection class names, as for example Worksheets, Forms, or Widgets. If the collection holds objects with a name that ends in "s," append the word Collection, as for example SeriesCollection.

  • Use a prefix for the named constants in Enums, as discussed in "Providing Named Constants for Your Component," later in this chapter.

  • Use either the verb/object or object/verb order consistently for your method names. That is, use InsertWidget, InsertSprocket, and so on, or always place the object first, as in WidgetInsert and SprocketInsert.

Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.

For More Information An expanded version of this list can be found in "Object Naming Guidelines," in "Standards and Guidelines."


Choosing a Project Type and Setting Project Properties

When you open a new project, you have three choices for project type: ActiveX EXE, ActiveX DLL, and ActiveX Control. The type you choose determines what kinds of objects your component can provide.

The following list may assist you in selecting the correct project type for your component.

  1. If your component is going to provide ActiveX controls, open a new ActiveX control project. Controls can only be provided by control components (.ocx files), which must be compiled from ActiveX control projects. Control components always run in process.

    Note Control components are limited in their ability to provide other kinds of objects because class modules in ActiveX control projects can only have two Instancing settings, PublicNotCreatable or Private. Objects in control components are best used to enhance the features of controls; put objects with other uses in a separate ActiveX DLL project.

  2. If your component is going to provide ActiveX documents, you can open either an ActiveX EXE project or an ActiveX DLL project. (The New Project dialog box provides templates for new projects that will provide ActiveX documents.)

    See "Building ActiveX Documents" for a discussion of the reasons to choose an in-process or out-of-process component to provide active documents.

  3. If you’re creating an out-of-process component, open a new ActiveX EXE project. Reasons to create an out-of-process component include:
    • The component can run as a standalone desktop application, like Microsoft Excel or Microsoft Word, in addition to providing objects.

    • The component can process requests on an independent thread of execution, notifying the client of task completion using events or asynchronous call-backs. This frees the client to respond to the user.

    • If you’re using COM or Remote Automation to run components on remote computers (Enterprise Edition only), some components may need to be .exe files.
  4. If you’re creating an in-process component, open a new ActiveX DLL project. Reasons to create an in-process component include:
    • An in-process component shares its client’s address space, so property and method calls don’t have to be marshaled. This results in much faster performance.

    • An in-process component designed for unattended execution (that is, no user interaction) can be marked thread-safe. When used by a multithreaded client, it can provide objects on any of the client’s threads of execution, while retaining the performance benefits of in-process method and property calls.

Once you’ve opened a project for your new component, there are some project properties you should always set.

To set properties for a new component project

  1. On the Project menu, click Project1 Properties to open the Project Properties dialog box.

  2. On the General tab, set the Project Name.

    This is the most important property of any new component. It identifies your component in the Windows registry and the Object Browser; its uniqueness is therefore important.

    It’s also the default name of the compiled component, and the name of the type library that contains descriptions of the objects and interfaces provided by your component. See "Polymorphism, Interfaces, Type Libraries, and GUIDs," later in this chapter.

  3. On the General tab, set the Project Description.

    Project description is the text string a developer or user will see when setting a reference to your component, or when selecting your control component in the Components dialog box.

  4. On the General tab, set the Startup Object.

    Click None if there is no code you need to execute to initialize your component. If your component requires initialization, click Sub Main, add a module to your project, and in that module declare a Public Sub named Main. Place your initialization code in this Sub procedure.

    Important See "Starting and Ending a Component" later in this chapter for an explanation of the need to keep the processing in Sub Main to a minimum.

    Note Do not place Sub Main in a class module. Placing Sub Main in a class module turns it into a method named Main, rather than a startup procedure.

Setting Other Properties

Depending on the type of component you’re creating, other project properties may be of interest to you.

Unattended Execution

Located on the General tab, this property can be set for code components that have no user interaction. In an ActiveX DLL project, this makes the DLL thread-safe (via Apartment-model threading).

In an ActiveX EXE project, it allows the component to be multithreaded. The implications and limitations of this property are discussed in "Building Code Components."

Note Components that contain UserDocuments, UserControls, and forms cannot be marked for Unattended Execution.

Help File Name and Project Help Context ID

Providing a Help file for your component is highly recommended. See "Providing User Assistance for ActiveX Components" in "Debugging, Testing, and Deploying Components" for information on linking help topics to the properties and methods of the classes your component provides.

Start Mode

For out-of-process components that will double as standalone desktop applications, the Start Mode property on the Components tab allows you to debug both startup modes, as discussed in "Debugging Out-of-Process Components" in "Debugging, Testing, and Deploying Components."

Make Tab Properties

Properties on the Make tab allow you to control file version numbers and version information about your component. Use of this tab to provide such information is highly recommended.

Important Incrementing file version numbers is extremely important for components, as it helps ensure that Setup for your component will never overwrite a newer version with an older one.

Version Compatibility

On the Component tab, you can select a Version Compatibility mode. For new projects, this option is automatically set to Project Compatibility the first time you compile your component, as discussed in "How to Test ActiveX Components" in "Debugging, Testing, and Deploying Components."

For successive versions of your component, you can select Binary Compatibility to ensure that programs compiled with old versions continue to work with the new version. See "Version Compatibility" in "Debugging, Testing, and Deploying Components."

Compile Tab Properties

Properties on the compile tab allow you to select p-code or native code for your compiled component, as discussed in "More About Programming" in the Visual Basic Programmer’s Guide.

Remote Server

In the Enterprise Edition of Visual Basic, setting this Component tab property generates the support files required to run your component on a remote computer.


Polymorphism, Interfaces, Type Libraries, and GUIDs

Components deliver services by providing classes from which clients can create objects. Clients use services by creating objects and calling their properties and methods.

Information about the classes provided by your component is contained in a type library. In Visual Basic, the type library is included as a resource in the compiled component. Clients access the type library by setting references to it.

Setting the Type Library Name

Project Name, on the General tab of the Project Properties dialog box, sets the name of your component’s type library, and is used to qualify the names of classes. For example, the following code fragment declares a variable that will hold a reference to an object of the Widget class, provided by a component whose project name is SmallMechanicals:

Public gwdgDriveLink As SmallMechanicals.Widget

Some applications can manipulate objects, but cannot declare variables of a specific object type. Such applications declare generic object variables As Object, and use the project name in the projectname argument of the CreateObject function to get a new object reference, as shown in the following syntax:

Set objectvariable = CreateObject("projectname.class")

The combination of project name and class name is sometimes referred to as a fully qualified class name, or as a programmatic ID. The fully qualified class name may be required to correctly identify an object as belonging to your component. For example, you might implement a Window class in your component. Microsoft Excel also provides a Window object, which could lead to the following confusion for client applications:

' A variable of the Microsoft Excel Window class.
Dim xlWindow As Excel.Window
' A variable of the ProgramX component's Window class.
Dim pxWindow As ProgramX.Window
' A variable of the Window class that belongs to the
'   component - Microsoft Excel or ProgramX - that
'   appears first in the client application's
'   References dialog box!
Dim xWindow As Window

Default Interfaces

An interface is a set of properties and methods, or events. Every class provided by your component has at least one interface, called the default interface, which is composed of all the properties and methods you declare in the class module.

The default interface is usually referred to by the same name as the class, though its actual name is the class name preceded by an underscore. The underscore prefix is a convention, signifying that the name is hidden in the type library.

If the class raises events, it also has an IConnectionPointContainer interface that enumerates those events. Events are outgoing interfaces, as opposed to the incoming interfaces composed of properties and methods. In other words, clients make requests by calling into your class’s properties and methods, while the events raised by your class call out to event handlers in clients.

Incoming and outgoing interfaces are symbolized differently in interface diagrams, as shown in Figure 6.1.

Figure 6.1 Incoming and outgoing interfaces

Important You should consider interfaces as contracts between you and the user of your component, because changing an interface may cause applications compiled against your component to fail.

Visual Basic provides two mechanisms for enhancing components without affecting compiled applications: version compatibility for interfaces, and multiple interfaces. In order to discuss these mechanisms, however, you have to learn to dig GUIDs.

Type Libraries, Interfaces, and GUIDs

GUID (pronounced goo-id) stands for Globally Unique IDentifier, a 128-bit (16-byte) number generated by an algorithm designed to ensure its uniqueness. This algorithm is part of the Open Software Foundation (OSF) Distributed Computing Environment (DCE), a set of standards for distributed computing.

GUIDs are used to uniquely identify entries in the Windows registry. For example, Visual Basic automatically generates a GUID that identifies your type library in the Windows registry.

Visual Basic also automatically generates a GUID for each public class and interface in your component. These are usually referred to as class IDs (CLSID) and interface IDs (IID). Class IDs and interface IDs are the keys to version compatibility for components authored using Visual Basic.

Note You may also see GUIDs referred to as UUIDs, or Universally Unique IDentifiers.

What If Visual Basic Runs Out of GUIDs?

This is not a problem we need to worry about in our lifetimes. The algorithm that generates GUIDs would allow you to compile several new versions of your component every second for centuries — without repeating or colliding with GUIDs generated by other developers.

Version Compatibility for Interfaces

When a developer compiles a program that uses your component, the class IDs and interface IDs of any objects the program creates are included in the executable.

The program uses the class ID to request that your component create an object, and then queries the object for the interface ID. An error occurs if the interface ID no longer exists.

During development of a new component Visual Basic generates new CLSIDs and IIDs every time you compile, as long as either Project Compatibility or No Compatibility is selected on the Component tab of the Project Properties dialog box. Once you’ve released a component, however, and begin working on an enhanced version of it, you can use the Binary Version Compatibility feature of Visual Basic to change this behavior.

As described in detail in "Version Compatibility" in "Debugging, Testing, and Deploying Components," binary version compatibility preserves the class IDs and interface IDs from previous versions of your component. This allows applications compiled using previous versions to work with the new version.

To ensure compatibility, Visual Basic places certain restrictions on changes you make to default interfaces. Visual Basic allows you to add new classes, and to enhance the default interface of any existing class by adding properties and methods. Removing classes, properties, or methods, or changing the arguments of existing properties or methods, will cause Visual Basic to issue incompatibility warnings.

If you decide to ship an incompatible interface, Visual Basic changes the major version number of the type library and suggests that you change the executable name and Project Name, so that the new version of your component will not over-write the old on your users’ hard disks.

Multiple Interfaces: Polymorphism and Compatibility

The Implements statement, discussed in "Polymorphism," in "Programming with Objects" in the Visual Basic Programmer’s Guide, allows your classes to implement additional interfaces.

When two classes implement the same secondary interface, they are said to be polymorphic with respect to that interface. That is, a client can make early-bound calls to the properties and methods of the interface without having to know the class of the object it’s using.

By creating standard interfaces and implementing them in multiple classes, provided by one or more components, you can take advantage of polymorphism in your applications, or across your entire organization.

For More Information "Creating Standard Interfaces with Visual Basic," later in this chapter, explains how to create interfaces by defining class modules that have no implementation code.

Interfaces and Compatibility

Multiple interfaces provide an alternate means of enhancing and evolving your components while maintaining compatibility with older applications compiled against earlier versions.

The ActiveX rule you must follow to ensure compatibility with multiple interfaces is simple: once an interface is in use, it can never be changed. The interface ID of a standard interface is fixed by the type library that defines the interface.

The way to enhance standard interfaces is to create a new standard interface, embodying the enhancements. Future components, or future versions of existing components, can implement the old interface, the new interface, or both, as shown in Figure 6.2.

Figure 6.2 Providing compatibility with multiple interfaces

The IFinance and IFinance2 interfaces are defined in separate type libraries, which are referenced by the components that implement the interfaces (in this case, versions 1.0 and 2.0 of Finance.dll), and by the applications that use the interfaces (Payroll and GeneralLedger).

System evolution is possible because future applications can take advantage of new interfaces. Existing applications will continue to work with new versions of components, as long as the components continue to implement the old interfaces as well as the new.

Applications that Work With Multiple Versions of Components

You can write applications that can use any of several versions of a component. For example, version 2.0 of GeneralLedger.exe could be written to use IFinance2 if that interface was available, and to use IFinance otherwise.

Obviously, GeneralLedger would be able to provide only a limited set of features in the latter case. This ability to provide limited functionality in the absence of the preferred interface is often referred to as degrading gracefully.

To work with either interface, GeneralLedger might contain code like the following:

Dim fnr As FinanceRules
Dim ifin As IFinance
Dim ifin2 As IFinance2

On Error Resume Next
Set fnr = New FinanceRules
' (Error handling code omitted.)
' Attempt to access the preferred interface.
Set ifin2 = fnr
If Err.Number <> 0 Then
   ' Access the more limited interface.
   Set ifin = fnr
   ' (Code to provide limited functionality,
   '   using the object variable ifin.)
Else
   ' (Code to provide full functionality,
   '   using the object variable ifin2.)
End If

For More Information "Providing Polymorphism by Implementing Interfaces," later in this chapter, discusses the use and naming of standard interfaces.


Starting and Ending a Component

Your component starts the first time a client application requests an object from one of your classes. A new instance of the class is created, and the client receives a reference to the newly created object.

If your component has a Sub Main procedure, it will execute when the first object is requested. The object will not be created until Sub Main has finished.

Important Don’t execute lengthy initialization activities in Sub Main, as this may cause object creation to time out and fail.

You should not show forms or user messages from your Sub Main, because they can cause the same problems as lengthy initialization tasks. Visual Basic does not allow forms as start up objects in ActiveX EXE and ActiveX DLL projects, for precisely this reason.

A desktop application that provides objects should test App.StartMode, and show its main form only if it was started standalone. In this case, Sub Main may happen long before the first object is requested.

Note If you have selected ActiveX Component for the Start Mode option, on the Component tab of the Project Properties dialog box, Sub Main will not execute immediately when you put your component into run mode. Visual Basic does this to allow accurate debugging of your startup code. Once your component is compiled, Sub Main will not execute until the first time a client requests an object, so the development environment exactly duplicates this behavior.

Handling Lengthy Initialization Tasks

You can perform initialization in the Class_Initialize event of the first object created. When initialization is complete, set a global flag so that subsequent instances of the class don’t execute the initialization task.

Alternatively, you can defer some initialization tasks until the service is actually needed. In this case, each object must test before using the service, to ensure it has been initialized — and to initialize it if it has not. This technique works best when initialization consists of many small, independent tasks.

A very large initialization task that cannot be broken up can be performed in the background, using a call-back timer. You can start the timer at the end of Sub Main, as discussed in "Building Code Components." In the timer’s Tick event, disable the timer and run the lengthy task. At the end of the Tick event, set a global flag to indicate that the component is initialized. You can then notify any objects that have been created while initialization was in progress.

Important Don’t use a Timer control for this purpose, because the Timer control requires a form to be loaded.

When you use this technique, make sure each object tests the global flag during its own Initialize event. If the component is not initialized, each object should insert itself into a global collection, so it can be notified when background initialization is complete.

You can add a notification method — for example NotifyInitComplete — to each class. Make this a Friend method, so it’s only visible within your component.

For More Information Friend methods are discussed in "Private Communications Between Your Objects," later in this chapter.

Component Shutdown

Your component should shut down when all clients have released all references to the objects the component provides. (A desktop application that provides objects is an exception to this rule.)

Visual Basic does a number of things to make it easy for your component to shut down properly. The most important of these is keeping track of references to your public objects, because your component should never close while clients still hold references to objects.

Important Only references to public objects will keep a component running. References to private objects — objects provided by Visual Basic, or objects from classes marked Private — will not prevent component shutdown and subsequent catastrophic failure of the client. References to private objects should never be passed to clients.

The rules Visual Basic uses to determine when to shut down are different for in-process and out-of-process components.

Out-of-Process Component Shut Down

An out-of-process component written with Visual Basic will shut down when:

  • The component has no forms loaded.

    A loaded form will keep your component running past the point at which all references have been released. Forms should be unloaded when the object that created them terminates.

  • No out-of-process client applications are holding references to the component’s public objects.

    Internal references to public objects will not keep a component running if clients have released all their references. Neither will references held by an in-process component the out-of-process component is using.

  • No code in the component’s modules is currently executing, or in the calls list waiting to be executed.

  • The component is not in the process of being started in response to a client application’s request for an object.

In-Process Component Shut Down

In the long run, the lifetime of an in-process component is controlled by its client, because the component runs in the client’s process. When the client closes, the component is unloaded regardless of any outstanding object references, open forms, and so on.

A client may ask an in-process component to unload when the client is no longer using objects the component provides. The following rules describe the way a Visual Basic in-process component determines whether it should unload in response to a client request.

  • The component has no forms visible.

    A visible form will keep your component in memory, even if there are no object references; an invisible form will not.

  • There are no references to the component’s public objects.

    If your in-process component is holding a reference to one of its own public objects, Visual Basic will not unload it. Visual Basic cannot tell whether the reference is internal or external.

  • No code in the component’s modules is currently executing, or in the calls list waiting to be executed.

  • The component is not in the process of being started in response to a client application’s request for an object.

Note A client application written in Visual Basic may not attempt to unload an in-process component immediately after the last reference is released. The frequency of attempts depends on how frequently idle time becomes available; in general it will be around two minutes.

For More Information A more complete discussion of the rules for component shutdown can be found in "ActiveX Component Shutdown" in "ActiveX Component Standards and Guidelines."


Adding Classes to Components

Only one thing distinguishes a component from other applications you author using Visual Basic: A component project has at least one public class from which client applications can create objects.

Like any other Visual Basic application, your component may have numerous class modules that encapsulate its internal functionality. When you allow clients to create instances of a class, objects created from that class can be manipulated by clients, and your application becomes a component.

Creating New Classes

From the Project menu, you can choose Add Class Module, Add User Control, or Add User Document to define a new public class. Other choices on the Project menu allow you to add objects that can be used within your application, but only UserControls, UserDocuments, and class modules can define public classes.

Each public class you add will be the blueprint for one kind of public object in your object model. You can provide a class name, define interfaces for the class, and set the Instancing property (or the Public property, in some cases) to determine how objects will be created from the class.

Name Property

Choose class names carefully. They should be short but descriptive, and formed from whole words with individual words capitalized — for example, BusinessRule.

The class name is combined with the name of the component to produce a fully qualified class name, also referred to as a programmatic ID or ProgID. For example, the fully qualified class name of a BusinessRule class provided by the Finance component, is Finance.BusinessRule.

The topic "What’s in a Name?" earlier in this chapter, outlines the rules for naming classes, properties, and methods.

Defining Interfaces

The default interface for a class is composed of the properties and methods you define for it, as discussed in "Adding Properties and Methods to Classes," later in this chapter.

The default interface of a class is an incoming interface, as explained in "Polymorphism, Interfaces, Type Libraries, and GUIDs." You can also add outgoing interfaces, or events, as described in "Adding Events to Classes."

Visual Basic includes information about the class module’s default interface and outgoing interfaces in the type library it creates when your component is compiled.

For More Information You can implement additional incoming interfaces on a class, as described in "Providing Polymorphism by Implementing Interfaces."

Public or Instancing Property

UserControl classes have a Public property that determines whether the class is public or private. UserDocument classes are always public. This is discussed in the in-depth chapters on ActiveX controls and documents, "Building ActiveX Controls" and "Building ActiveX Documents."

Class modules have a more complex public life, controlled by the Instancing property. For each class your component will provide to other applications, set the Instancing property of the class module to any value except Private, as discussed in the related topic "Instancing for Classes Provided by ActiveX Components."

You don’t have to make all your classes public; if there are objects you want to use only within your component, set the Instancing properties of the class modules that define them to Private. (For a UserControl, set the Public property to False.)

Related Class Creation Topics

The following topics provide detailed information about the features of classes in Visual Basic projects.

  • Instancing for Classes Provided by ActiveX Components The Instancing property of class modules determines whether an object can be created by clients, always starts a new instance of the component, is global, or (in some cases) is on a separate thread.

  • Coding Robust Initialize and Terminate Events What happens when an error occurs as an object is being created or destroyed? How you answer that question can affect the stability of programs that use your component.

  • Standard Modules vs. Class Modules Public variables in class modules and standard modules have different scope and visibility.

For More Information Class modules in Visual Basic are introduced in "Programming with Objects," in the Visual Basic Programmer’s Guide. Topics specific to classes defined in class modules, ActiveX controls, and ActiveX documents are discussed in depth in "Building Code Components," "Building ActiveX Controls," and "Building ActiveX Documents." Object models are discussed in "Organizing Objects: The Object Model," later in this chapter.


Instancing for Classes Provided by ActiveX Components

The value of the Instancing property determines whether your class is private — that is, for use only within your component — or available for other applications to use.

As its name suggests, the Instancing property also determines how other applications create instances of the class. The property values have the following meanings.

  • Private means that other applications aren’t allowed access to type library information about the class, and cannot create instances of it. Private objects are only for use within your component.

  • PublicNotCreatable means that other applications can use objects of this class only if your component creates the objects first. Other applications cannot use the CreateObject function or the New operator to create objects from the class.

  • MultiUse allows other applications to create objects from the class. One instance of your component can provide any number of objects created in this fashion.

    An out-of-process component can supply multiple objects to multiple clients; an in-process component can supply multiple objects to the client and to any other components in its process.

  • GlobalMultiUse is like MultiUse, with one addition: properties and methods of the class can be invoked as if they were simply global functions. It’s not necessary to explicitly create an instance of the class first, because one will automatically be created.

  • SingleUse allows other applications to create objects from the class, but every object of this class that a client creates starts a new instance of your component. Not allowed in ActiveX DLL projects.

  • GlobalSingleUse is like SingleUse, except that properties and methods of the class can be invoked as if they were simply global functions. Not allowed in ActiveX DLL projects.

Class Modules and Project Types

The value of the Instancing property is restricted in certain project types. Allowed values are shown in the following table:

Instancing Value | ActiveX EXE | ActiveX DLL | ActiveX Control
Private | Yes | Yes | Yes
PublicNotCreatable | Yes | Yes | Yes
MultiUse | Yes | Yes |
GlobalMultiUse | Yes | Yes |
SingleUse | Yes |  |
GlobalSingleUse | Yes |  |

Dependent Objects (PublicNotCreatable)

The value of the Instancing property determines the part an object plays in your component’s object model, as discussed in "Organizing Objects: The Object Model."

If the Instancing property of a class is PublicNotCreatable, objects of that class are called dependent objects. Dependent objects are typically parts of more complex objects.

For example, you might allow a client application to create multiple Library objects, but you might want Book objects to exist only as parts of a Library. You can make the Book class PublicNotCreatable, and let the user add new books to a Library object by giving the Library class a Books collection with an Add method that creates new books only within the collection.

Your component can support as many dependent objects as necessary. You can write code in the Add method of a collection class to limit the number of objects in the collection, or you can allow the number to be limited by available memory.

For More Information Dependent objects are discussed in detail in "Dependent Objects," later in this chapter.

Externally Creatable Objects

All values of the Instancing property besides PublicNotCreatable and Private define externally creatable objects — that is, objects that clients can create using the New operator or the CreateObject function.

MultiUse vs. SingleUse

In ActiveX DLLs, Instancing for an externally creatable class will most commonly be MultiUse. This setting allows an in-process component to supply any number of instances of the class to the client executable, and to any other in-process component.

For ActiveX EXEs, the Instancing values SingleUse and MultiUse define very different behaviors for a class. MultiUse makes the most efficient use of memory, because it allows one instance of your component to provide multiple objects to multiple client applications without duplication of resources or global data.

For example, suppose the SmallMechanicals component provides a Widget class, and the Instancing property of the class is set to MultiUse. If one client application creates two Widget objects, or if two client applications each create a Widget object, all the Widgets will be supplied from one instance of your component.

If the Instancing property of the Widget class is set to SingleUse, the result of both scenarios above is that a separate copy of your component will be loaded into memory for each Widget created. The uses and limitations of this behavior are discussed in "Building Code Components," and in "ActiveX Component Standards and Guidelines."

MultiUse and Multithreading

If your component is an ActiveX EXE marked for unattended execution (that is, it has no user interaction whatever), and the Instancing property of the Widget class is set to MultiUse, the result of both scenarios above is that two Widget objects are created in same copy of SmallMechanicals, each on its own thread of execution.

Apartment Model threading is used, meaning that each thread is like an apartment, and objects in different apartments are unaware of each other’s existence. This is accomplished by giving each Widget its own copy of the SmallMechanicals component’s global data.

For More Information The use of multithreading or SingleUse instancing to avoid blocked execution is discussed in "Building Code Components."

Global Objects

Frequently it’s useful to have utility functions that users of your component can employ without first creating an instance of one of your objects. In out-of-process components, such functions are frequently implemented as properties or methods of the Application object.

If the Instancing property for a class is marked GlobalMultiUse or GlobalSingleUse, then properties and methods of the class can be invoked without explicitly creating an instance of the object.

For example, suppose you want your SmallMechanicals component to provide a GlobalUtility object whose methods are general-purpose functions. You can add a class module to the SmallMechanicals project, set the Name property to GlobalUtility, and set the Instancing property to GlobalMultiUse.

Now you can add properties and methods to the class module. For example, you might implement a ReversePolarity method and a read-only WidgetCount property:

Public Sub ReversePolarity()
   ' (Code to reverse polarity on all Widgets.)
End Sub

In the client application, the ReversePolarity method can be invoked without first creating a GlobalUtility object:

Private Sub Command1_Click()
   ' No object variable is required.
   ReversePolarity
End Sub

Note The properties and methods of a GlobalMultiUse object are not part of the global name space of the component that provides the object. For example, within a project that contains the GlobalUtility class, you must explicitly create an instance of GlobalUtility in order to use the object's properties and methods. Other limitations of global objects are listed in "Global Objects and Code Libraries," in "Building Code Components."

Be careful when choosing names for the properties and methods of global objects. Using common or obvious names may result in name collisions with other components. Name conflicts must be resolved by qualifying the property or method with the type library name:

Private Sub Command1_Click()
   SmallMechanicals.ReversePolarity
   Esalen.ReversePolarity
End Sub

Important The "global" in global objects refers to the fact that all of the object’s properties and methods are available in the global name space of your project. It does not mean that one object is automatically shared by all clients. Each client that uses your component gets its own global object.

For More Information "Providing Named Constants for Your Component," later in this chapter, discusses the use of global objects to provide string constants and non-integer constants. Code components are discussed in depth in "Building Code Components."


Coding Robust Initialize and Terminate Events

Classes you define in Visual Basic, whether class modules, UserControls, or UserDocuments, have built-in Initialize and Terminate events. The code you place in the Initialize event will be the first code executed when an object is created.

For example, in the first part of the following code fragment the Widget class of the SmallMechanicals component sets the value of its read-only Created property at the moment an object is created.

' Code for the component's Widget class module.
' Storage for the read-only Created property.
Private mdatCreated As Date

' Implementation of the read-only Created property.
Public Property Get Created() As Date
   Created = mdatCreated
End Property

' Set the value for the read-only Created property when
' the object is created.
Private Sub Class_Initialize()
   mdatCreated = Now
End Sub

' Code for the client application.
Private Sub cmdOK_Click()
   Dim wdgX As New SmallMechanicals.Widget
   ' Display date/time object was created.
   MsgBox wdgX.Created
End Sub

In the last part of the code fragment, the client creates a Widget object. The variable wdgX will contain the reference to the Widget object; it is declared As New, so the Widget is created at the first use of wdgX in code. When the MsgBox function is executed, the Widget is created, and the very first code it executes is its Class_Initialize event procedure. When the read-only Created property of the newly created Widget is evaluated, its value has already been set, and therefore MsgBox correctly displays the time the Widget was created.

Errors that occur in the Class_Initialize event procedure are returned to the point in the client at which the object was requested. Thus, adding the following line to the Widget’s Class_Initialize event procedure will cause error 31013 to occur on the client’s MsgBox statement.

   Err.Raise Number:=31013

Handling Errors in the Terminate Event

The Terminate event is the last event in an object’s life. You can place cleanup code in the Class_Terminate event procedure, and this code will be executed when the last reference to the object has been released, and the object is about to be destroyed. Complex objects that contain dependent objects should release references to their dependent objects in the Terminate event.

Errors in the Terminate event require careful handling. Because the Terminate event is not called by the client application, there is no procedure above it on the call stack. This means that an unhandled error in a Terminate event will cause a fatal error in the component.

Important For in-process components, your fatal error is your client’s fatal error. Because the component is running in the client’s process, the client application will be terminated by a component’s fatal error.


Standard Modules vs. Class Modules

Classes differ from standard modules in the way their data is stored. There is never more than one copy of a standard module’s data. This means that when one part of your program changes a public variable in a standard module, and another part of your program subsequently reads that variable, it will get the same value.

Class module data, on the other hand, exists separately for each instance of the class.

Avoid making the code in your classes dependent on global data — that is, public variables in standard modules. Many instances of a class can exist simultaneously, and all of these objects share the global data in your component.

Static Class Data

Using global variables in class module code violates the object-oriented programming concept of encapsulation, because objects created from such a class do not contain all their data. However, there may be occasions when you want a data member to be shared among all objects created from a class module. For example, you might want all objects created from a class to share a property value, such as the name or version number of your component.

This deliberate violation of encapsulation is sometimes referred to as static class data. You can implement static class data in a Visual Basic class module by using Property procedures to set and return the value of a Public data member in a standard module, as in the following code fragment:

' Read-only property returning application name.
Property Get ComponentName() As String
   ' The variable gstrComponentName is stored in a
   '   standard module, and declared Public.
   ComponentName = gstrComponentName
End Property

You can implement static class data that is not read-only by providing a corresponding Property Let procedure — or Property Set for a property that contains an object reference — to assign a new value to the standard module data member.

Important When designing a class that uses static data, remember that your component may be providing objects simultaneously to several client applications (if it’s an out-of-process component) or to a client and several in-process components (if it’s an in-process component). All the objects created from the class will share the static data, even if they’re being used by different clients.


Adding Properties and Methods to Classes

The default interface of a class in your component is simply the set of all public properties, methods, and events in the class module, UserControl, or UserDocument that defines the class.

Adding properties and methods is easy — a method is any Public Sub or Public Function procedure you declare in the module that defines your class; a property is any public property procedure or public variable you declare.

The following related topics examine the issues you should be aware of when declaring interface elements in a component.

  • Implementing Properties in Components Factors to be aware of when creating properties for classes provided by components.

  • Implementing Methods in Components Factors to be aware of when creating methods for classes provided by components.

  • Data Types Allowed in Properties and Methods The interface for a class provided by a component follows more strict rules than those required for private objects; at stake is the stability of client programs.

  • Choosing a Default Property or Method for a Class You can select either a property or a method as the default member for a class.

Important The following names cannot be used as property or method names, because they belong to the underlying IUnknown and IDispatch interfaces: QueryInterface, AddRef, Release, GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. These names will cause a compilation error.

For More Information The mechanics of property and method declaration are discussed in "Adding Properties to a Class" and "Adding Methods to a Class," in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Implementing Properties in Components

Adding Properties to a Class," in "Programming with Objects," in the Visual Basic Programmer’s Guide discusses in detail the many kinds of properties you can add to your classes, including simple data values, read-only properties, and property arrays.

"Adding Properties to a Class" also describes the two ways you can declare properties: as public variables, or as property procedures.

In general, properties of objects provided by components should be implemented as property procedures. Property procedures are more robust than data members. A property whose type is an enumeration, for example, cannot be validated unless implemented as a Property Get and Property Let.

The only exception to this rule is a simple numeric or string property which requires no validation and which, when changed, does not immediately affect other properties of the object.

An object property — that is, any property that contains an object reference instead of an ordinary data type — should almost always be implemented with property procedures. An object property implemented as a public object variable can be set to Nothing accidentally, possibly destroying the object. This is discussed in "Organizing Objects: The Object Model," later in this chapter.

Note Internally, Visual Basic generates a pair of property procedures for every public variable you declare. For this reason, declaring public variables doesn’t provide any size or performance benefits.

For More Information See "Adding Properties to a Class," in "Programming with Objects," in the Programmer’s Guide.


Persisting a Component's Data

Most components have properties; in most cases you'll want to establish default values for those properties in the Initialize event of the class. Those default values are frozen when you compile the component, so how do you allow a developer to change the default values to meet their own special conditions? Classes have a special property, Persistable, that allow you to store a component's values between instances.

Suppose that you had an ActiveX DLL that calculates loans, with an InterestRate property used in the calculations. You could initialize the InterestRate to some arbitrary value, but since interest rates periodically go up or down, the InterestRate property would need to be modified each time the component is run. With class persistence, you can store the InterestRate value and modify it only when the interest rate changes. Each time your component is run it can retrieve the InterestRate from storage, so the component will always provide the latest rate.

While ActiveX controls have always been able to persist their data; persistence for ActiveX components is slightly different. A control stores property settings inside it's .cls file, but a component can't do that. Instead, it uses a PropertyBag object that can be saved just about anywhere – in a file, a database, a cell in a spreadsheet, or even in the registry.

For More Information To learn more about persisting ActiveX controls, see "Saving the Properties of a Control," in "Building an ActiveX Control".

Setting Up Class Persistence

In order to be persistable, a class must meet two conditions: it must be public and creatable. If you think about, this makes sense – after all, persistence wouldn't be useful in a private component. If a class meets both conditions, the Persistable property appears in the Properties window.

By default, the Persistable property is set to 0 (NotPersistable). By changing this value to 1 (Persistable), three new events are added to the class: ReadProperties, WriteProperties, and InitProperties. As you might guess, these events are use to read, write, and initialize the class's properties.

Persisting a Property

You can mark a property as persistable by implementing the PropertyChanged method in a Property Let or Property Set procedure, as in the following example:

Private mInterestRate As Single
Public Property Let InterestRate(newRate As Single)
   mInterestRate = newRate
   PropertyChanged "InterestRate"
End Sub

Calling the PropertyChanged method marks the InterestRate property as dirty. The WriteProperties event will fire when the class is terminated if any property in the class has called PropertyChanged.

The ReadProperties, WriteProperties and InitProperties Events

The WriteProperties event procedure is used when a class is terminating to write the current property values to a private storage known as a PropertyBag object. The following code is used to save a property to the built-in PropertyBag:

Private Sub Class_WriteProperties(PropBag As PropertyBag)
   PropBag.WriteProperty "InterestRate", mInterestRate, conDefaultRate
End Sub

The Property Bag's WriteProperty method in the above code takes three arguments: the name of the property to save ("InterestRate"), the value to save (mInterestRate), and a default value (DefaultRate). If the new value matches the constant conDefaultRate, the WriteProperty method doesn't have to write out the value.

The ReadProperties event is fired when a class is initialized – but only if the PropertyBag has something in it. If the PropertyBag is empty, the InitProperties event will be fired instead. Code in the ReadProperties and InitProperties events is used to set the initial property values:

Private Sub Class_ReadProperties(PropBag As PropertyBag)
   mInterestRate = PropBag.ReadProperty("InterestRate", conDefaultRate)
End Sub
Private Sub Class_InitProperties ()
   mInterestRate = conDefaultRate
End Sub

Note that the constant conDefaultRate is used in both procedures to provide a default value. By using a constant to define the default values you eliminate the risk of accidentally defining different default values in different procedures.

When you first create an instance of the Loan class using the New keyword, the InitProperties event will be fired; once the class has been persisted and is then recreated, the ReadProperties event will be fired instead.

Using the PropertyBag Object to Persist an Object

In order to persist an ActiveX component, you need to create an instance of a PropertyBag object. This may seem redundant — after all, the class already has it's own PropertyBag. Why can't you just use that? Simple. When the object goes away, so does its PropertyBag. It only exists in memory; for persistence you need to store a copy of the object somewhere so you can retrieve it later.

Think of a PropertyBag as a sack that you can fill up with stuff and stash away somewhere for safe keeping. Where you stash it is entirely up to you. The following form code demonstrates how you can persist an object to a text file:

Private pb As PropertyBag      ' Declare a PropertyBag object.
Private LoanObject As Loan      ' Declare a Loan object.

Private Sub Form_Unload(Cancel As Integer)
   Dim varTemp as Variant

   ' Instantiate the PropertyBag object.
   Set pb = New PropertyBag
   ' Save the object to the PropertyBag using WriteProperty.
   pb.WriteProperty "MyLoanObject", LoanObject
   ' Assign the Contents of the PropertyBag to a Variant.
   varTemp = pb.Contents
   ' Save to a text file.
   Open "C:\Loandata.txt" For Binary As #1
   Put #1, , varTemp
   Close #1
End Sub

The Contents property of the PropertyBag object contains the Loan object stored as an array of bytes. In order to save it to a text file, you first must convert it to a data type that a text file understands — in this case, a Variant.

Depersisting An Object

Once the object is contained inside a text file (or any other type of storage), it can easily be transported to another location. Imagine that our Loan object contains not only the InterestRate, but also property values to represent all of the fields in a loan application. You could take the Loandata.txt file and send it to the central office for approval. The code for a form that would reuse the Loan object would look something like this:

Private pb As PropertyBag      ' Declare a PropertyBag object.
Private LoanObject As Loan      ' Declare a Loan object.

Private Sub Form_Load()
   Dim varTemp As Variant
   Dim byteArr() as Byte

   ' Instantiate the PropertyBag object.
   Set pb = New PropertyBag
   ' Read the file contents into a Variant.
   Open "C:\Loandata.txt" For Binary As #1
   Get #1, , varTemp
   Close #1
   ' Assign the Variant to a Byte array.
   ByteArr = varTemp
   ' Assign to the PropertyBag Contents property.
   Pb.Contents  = ByteArr
   ' Instantiate the object from the PropertyBag
   Set LoanObject = pb.ReadProperty("MyLoanObject")
End If

You may have noticed that the object had to be assigned three times: first from the text file to a Variant, then from a Variant to a Byte array, then to the Contents property. That's because the Contents property will only accept a Byte array — if you tried to assign any other data type you would get an error.

So what's going on here? Can you actually take an object created in one place and reuse it in another, complete with its data? Well, not exactly. The original object is long gone. What you are passing in a PropertyBag is an exact copy of the object, not the object itself. This ability to "clone" an object for reuse is a powerful concept, especially when it comes to designing workflow applications.


Implementing Methods in Components

When you declare a method, declare all of its arguments as explicit data types whenever possible. Arguments that take object references should be declared as specific class types — for example, As Widget instead of As Object or As Variant.

Strongly typed arguments allow many user errors to be caught by the compiler, rather than occurring only under run-time conditions. The compiler always catches errors, while run-time testing is only as good as the test suite coverage.

This is as true of optional parameters as it is of the method’s fixed parameters. For example, the Spin method of a hypothetical Widget object might allow either direct specification of spin direction and speed, or specification of another Widget object from which angular momentum is to be absorbed:

Public Sub Spin( _
   Optional ByVal SpinDirection As Boolean = True, _
   Optional ByVal Torque As Double = 0, _
   Optional ByVal ReactingWidget As Widget = Nothing)
   ' (Code to ensure that a valid combination of
   '  arguments was supplied.)
   ' (Implementation code.)
End Sub

For More Information See "Adding Methods to a Class," in "Programming with Objects," in the Visual Basic Programmer’s Guide. The choice of ByVal or ByRef arguments may affect the performance of your component, as discussed in "How Marshaling Affects ActiveX Component Performance," in "Building Code Components."


Data Types Allowed in Properties and Methods

Classes can have properties and methods of any public data type supported by Automation. This includes all arguments of properties and methods, as well their return values. The allowed data types include:

  • Public objects provided by another component, such as DAO or a component authored using Visual Basic.

  • Public objects provided by Visual Basic for applications, such as the Error and Collection objects.

  • Objects defined in public classes in the component.

  • Public enumerations declared in public class modules.

  • Standard system data types defined by Automation, such as OLE_COLOR and OLE_TRISTATE.

  • The intrinsic data types provided by Visual Basic.

  • User-defined types.

On the Evils of Returning Private Objects

The following data types are not allowed, and references to them should never be returned to client applications:

  • All of the objects provided in the Visual Basic (VB) object library — for example, controls. Use the Object Browser to view the entire list.

  • All forms.

  • All class modules whose Instancing property is set to Private.

  • References to ActiveX controls.

Visual Basic prevents you from passing non-visual private objects to or from out-of-process components. Attempting to do so causes error 98, "A property or method call cannot include a reference to a private object, either as an argument or as a return value." This error is always received by the client.

In other cases, it is possible to trick Visual Basic and pass private objects to client programs. Don’t do this. References to private objects will not keep a component running.

If your component shuts down, because all references to your public objects have been released, any remaining private objects will be destroyed, even if clients still hold references to them.

Subsequent calls to the properties and methods of these objects will cause errors, in the case of out-of-process components. In the case of in-process components, a fatal program fault may occur in the client.

Important Private objects are private for a reason, usually because they were not designed to be used outside your project. Passing them to a client may decrease program stability and cause incompatibility with future versions of Visual Basic. If you need to pass a private class of your own to a client, set the Instancing property to a value other than Private.

For More Information "Building Code Components" shows how events can eliminate the need for public call-back classes, allowing Standard EXE projects to use call-backs without providing public objects or passing out references to private objects.


Choosing a Default Property or Method for a Class

You can mark the most commonly used public property or method of a class as the default method. This allows the user of a class to invoke the member without naming it.

To set a property or method as the default

  1. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  2. Click Advanced to expand the Procedure Attributes dialog box.

  3. In the Name box, select the property or method that is currently the default for the class. If the class does not currently have a default member, skip to step 5.

    Note You can use the Object Browser to find out what the current default member of a class is. When you select the class in the Class list, you can scroll through the members in the Members list; the default member will be marked with a small blue circle beside its icon.

  4. In the Procedure ID box, select None to remove the default status of the property or method.

  5. In the Name box, select the property or method you want to be the new default.

  6. In the Procedure ID box, select (Default), then click OK.

Important A class can have only one default member. If a property or method is already marked as the default, you must reset its procedure ID to None before making another property or method the default. No compile errors will occur if two members are marked as default, but there is no way to predict which one Visual Basic will pick as the default.

Fixing Defaults You Have Accidentally Made Private or Friend

The Procedure Attributes dialog box only allows you to select public properties and methods as the default for a class. If you make a public property or method the default for a class, and later change the declaration to Private or Friend, the property or method may continue to behave as if it were still declared Public.

To correct this problem, you must make the property or method Public again, because the Procedure Attributes dialog box will not show procedures declared Private and Friend. Once you have changed the declaration to Public, you can use the Procedure Attributes dialog to remove the Default attribute. You can then change the declaration back to Friend or Private.


Adding Events to Classes

You can add events to any class in your component. Events declared in classes provided by your component can be handled by clients regardless of whether your component is running in process or out of process. All events are public.

You declare an event using the Event keyword:

Event SomethingHappened(   ByVal HowMuch As Double, _
      ByVal When As Date)

Note You can declare event arguments just as you do arguments of procedures, with the following exceptions: Events cannot have named arguments, optional arguments, or ParamArray arguments. Events do not have return values.

You raise the event from within your class module’s code, whenever the circumstances that define the event occur.

   If blnSomethingHappened Then
      RaiseEvent SomethingHappened(dblPriceIncrease, _
         Now)
   End If

When the event is raised in an instance of the class, code in the SomethingHappened event procedures of any clients that are handling the event for that particular object will be executed. Events must be handled on an object-by-object basis; a client cannot elect to handle an event for all currently existing objects of a particular class.

If multiple clients have references to the same object, and are handling an event it raises, control will not return to your component until all clients have processed the event.

You can allow clients to respond to events by declaring a parameter ByRef instead of ByVal. This allows any client to change the value of the argument. When execution resumes, on the line after RaiseEvent, you can examine the value of this argument and take appropriate action.

This capability is frequently used for Cancel arguments, as with the QueryUnload event of Visual Basic forms.

Note Visual Basic raises a separate QueryUnload event for each form; if one form cancels the event, events for subsequent forms are not raised.

Events can be used instead of call-back functions, as discussed in "Building Code Components." The capabilities of the two approaches are identical, but implementation of events is much simpler, for you and for the user of your component.

Events cannot be handled within the class that declared them.

For More Information Raising events in controls is discussed in detail in "Building ActiveX Controls." The syntax for raising and handling events is covered in "Adding Events to a Class" in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Providing Named Constants for Your Component

Enumerations provide an easy way to define a set of related named constants. For example, the built-in enumeration VbDayOfWeek contains numeric constants with the names vbMonday, vbTuesday, and so on.

You can use an enumeration as the data type of a property or method argument, as in the following example:

Private mdowDayOfWeek As VbDayOfWeek

Property Get DayOfWeek() As VbDayOfWeek
   DayOfWeek = mdowDayOfWeek
End Property

Property Let DayOfWeek(ByVal NewDay As VbDayOfWeek)
   If (NewDay < vbUseSystemDayOfWeek) _
         Or (NewDay < vbSaturday) Then
      Err.Raise Number:=31013, _
         Description:="Invalid day of week"
   Else
      DayOfWeek = mdowDayOfWeek
   End If
End Property

When users of your component enter code that assigns a value to this property, the Auto List Members feature will offer a drop down containing the members of the enumeration, as shown in Figure 6.3.

Figure 6.3 Auto List Members displays enumerations

Tip You might think that you could save space by declaring the internal variable mdowDayOfWeek As Byte instead of As VbDayOfWeek — since the latter effectively makes the variable a Long. However, on 32-bit operating systems the code to load a Long is faster and more compact than the code to load shorter data types. Not only could the extra code exceed the space saved, but there might not be any space saved to begin with — because of alignment requirements for modules and data.

You can make the members of an enumeration available to users of your component by marking the enumeration Public and including it in any public module that defines a class — that is, a class module, UserControl, or UserDocument.

When you compile your component, the enumeration will be added to the type library. Object browsers will show both the enumeration and its individual members.

Note Although an enumeration must appear in a module that defines a class, it always has global scope in the type library. It is not limited to, or associated in any other way with the class in which you declared it.

General Purpose Enumerations

The members of an enumeration need not be sequential or contiguous. Thus, if you have some general-purpose numeric constants you wish to define for your component, you can put them into a catch-all Enum.

Public Enum General
   levsFeetInAMile = 5280
   levsIgnitionTemp = 451
   levsAnswer = 42
End Enum

Avoiding Enumeration Name Conflicts

In the preceding code example, both the Enum and its members were prefixed with four lowercase characters chosen to identify the component they belong to, and to reduce the chance that users of the component will encounter name conflicts. This is one of the general naming rules discussed in "What’s in a Name?" earlier in this chapter.

For More Information Enumerations are discussed in detail in "More About Programming" and "Programming with Objects," in the Visual Basic Programmer’s Guide.

Providing Non-Numeric and Non-Integer Constants

The members of an Enum can have any value that fits in a Long. That is, they can assume any integer value from -2,147,483,648 to 2,147,483,647. When you declare a variable using the name of an Enum as the data type, you’re effectively declaring the variable As Long.

Occasionally you may need to provide a string constant, or a constant that isn’t an integer value. Visual Basic doesn’t provide a mechanism for adding such values to your type library as public constants, but you can get a similar effect using a global object with read-only properties.

If your component doesn’t contain a global object, such as Application, add a public class module named GlobalConstants to your project. Set the Instancing property to GlobalMultiUse.

For each constant you want to provide, add to the GlobalConstants class module a Property Get procedure that returns the desired value. For example, the following code provides Avogadro’s Number as a constant, and mimics the vbCrLf constant in Visual Basic.

Public Property Get Avogadro() As Double
   Avogadro = 6.02E+23
End Property

Public Property Get vbCrLf() As String
   vbCrLf = Chr$(13) & Chr$(10)
End Property

Because the Instancing property is GlobalMultiUse, a user of the component doesn’t have to explicitly create an instance of the GlobalConstants class in order to use the constants. The constants can be used as if they were part of Visual Basic:

strNewText = "Line1" & vbCrLf & "Line2"

Note A user of Visual Basic, Microsoft Excel, or any other application that hosts Visual Basic for Applications would never see this version of the vbCrLf constant, because the VBA type library is always higher in the References dialog than the type library of any component.

For More Information Global objects are discussed in "Instancing for Classes Provided by ActiveX Components" in this chapter, and in "Building Code Components."


Private Communications Between Your Objects

There may be circumstances in which you want your component’s objects to be able to communicate with each other, without interference from users of your component. For example, you might want your Widgets collection class to set the Parent property of a newly created Widget, and thereafter to have Parent be read-only.

Public methods on a class can be called by other objects, but they can also be called by clients. Private methods cannot be called from outside the component, but neither are they visible to other objects within your component.

The solution is to use Friend methods. In the following code fragment, the hypothetical Widget object exposes a public read-only Parent property, and a Friend method (called SetParent) that the Widgets collection can use to set the value of the Parent property after creating a new Widget.

' A Widget is always part of a mechanism.
Private mmchParent As Mechanism

Public Property Get Parent() As Mechanism
   Set Parent = mmchParent
End Property

Friend Sub SetParent(ByVal NewParent As Mechanism)
   Set mmchParent = NewParent
End Sub

When a method is declared with the Friend keyword, it’s visible to other objects in your component, but is not added to the type library or the public interface. This is illustrated in Figure 6.4.

Figure 6.4 Friend methods have project scope

At run time, the Widgets collection class (within the project) sees a different interface from that seen by clients. The view of the Widget’s interface within the project (and the compiled DLL) includes the Friend method SetParent, which the Widgets collection calls.

The client only sees the public properties and methods of the Widget’s interface, because Friend methods are not added to the type library.

Using the Friend Keyword with Properties

You can also declare property procedures with the Friend keyword. In fact, the different property procedures that make up a property can have different scope. Thus the earlier code example can be rewritten as a pair of property procedures:

' A Widget is always part of a mechanism.
Private mmchParent As Mechanism

Public Property Get Parent() As Mechanism
   Set Parent = mmchParent
End Property

Friend Property Set Parent(ByVal NewParent As _
      Mechanism)
   Set mmchParent = NewParent
End Sub

From within the component, Parent is a read/write property. To clients, it’s a read-only property, because only the Property Get appears in the component’s type library.

You can think of Friend as a different scope, halfway between Public and Private.

Important In order to invoke Friend methods and properties, you must use strongly typed object variables. In the example above, the Widgets collection must use a variable declared As Widget in order to access the SetParent method or the Property Set Parent. You cannot invoke Friend methods from variables declared As Object.

Hiding Object Properties that Return Private Objects

"Using Properties and Collections to Create Object Models" describes the use of private objects in object models. When linking such objects to the public objects in the object model, you can declare all parts of the property procedure using the Friend keyword.

For example, each Widget object might have Socket object, which for some reason you don’t want to expose to users of your component. You could add the following object property to the Widget object, so that from inside your component you could access the Socket, without adding the property to the type library or the public interface:

' Create the Socket object on demand (As New).
Private msoc As New Socket

Friend Property Get Socket() As Socket
   Set Socket = msoc
End Property

For More Information Friend methods are introduced in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Providing Polymorphism by Implementing Interfaces

One of the most striking features of the Component Object Model (COM) is the ability of an object to implement multiple interfaces. In addition to enabling polymorphism, multiple interfaces provide a mechanism for incremental or evolutionary development, without the need to recompile all the components in the system when changes occur.

By defining features in terms of interfaces, composed of small groups of closely-related functions, you can implement component features as needed, knowing that you can expand them later by implementing additional interfaces.

Maintaining compatibility is simplified, because new versions of a component can continue to provide existing interfaces, while adding new or enhanced interfaces. Succeeding versions of client applications can take advantage of these when it makes sense for them to do so.

Inheritance and Polymorphism

As explained in "Polymorphism" in "Programming with Objects," in the Visual Basic Programmer’s Guide, most object-oriented programming tools provide polymorphism through inheritance. This is a powerful mechanism for small-scale development tasks, but has generally proven to be problematic for large-scale systems.

In part, these difficulties arise as a result of necessary changes to classes deep in the inheritance tree. Recompilation is required in order to take advantage of such changes, and failure to recompile may lead to unpleasant surprises when the time finally arrives for a new version.

More seriously, an over-emphasis on inheritance-driven polymorphism typically results in a massive shift of resources from development tasks to up-front design tasks, doing nothing to address development backlogs or to shorten the time before the end user can discover — through hands-on experience — whether the system actually fulfills the intended purpose.

As a consequence, tools for rapid prototyping and Rapid Application Development (RAD) have gained wider acceptance than OOP tools.

Visual Basic and COM

Visual Basic follows the COM example, emphasizing multiple interfaces as a more flexible way to provide polymorphism. Software can evolve interface by interface, rather than having to be derived from all necessary antecedents during a lengthy design process.

Objects can begin small, with minimal functionality, and over time acquire additional features, as it becomes clear from actual use what those features should be. Legacy code is protected by continuing to support old interfaces while implementing new ones.

The Implements Feature

Visual Basic provides the Implements keyword as the means for incorporating a secondary interface. For example, if your project had a reference to a type library that described the IFinance interface, you could place the following code in a class module:

Implements IFinance

Because type libraries contain only interfaces, and no implementation, you would then add code for each of the properties and methods of the IFinance interface, as described in "Implementing and Using Standard Interfaces."

An Interface is a Contract

When you create an interface for use with Implements, you’re casting it in concrete for all time. This interface invariance is an important principle of component design, because it protects existing systems that have been written to an interface.

When an interface is clearly in need of enhancement, a new interface should be created. This interface might be called Interface2, to show its relationship to the existing interface.

While generating new interfaces too frequently can bulk up your components with unused interfaces, well-designed interfaces tend to be small and independent of each other, reducing the potential for performance problems.

Factoring Interfaces

The process of determining what properties and methods belong on an interface is called factoring.

In general, you should group a few closely-related functions on an interface. Too many functions make the interface unwieldy, while dividing the parts of a feature too finely results in extra overhead. For example, the following code calls methods on three different interfaces of the Velociraptor class:

Public Sub CretaceousToDoList(ByVal vcr1 As _
      Velociraptor, ByVal vcr2 As Velociraptor)
   Dim dnr As IDinosaur
   Dim prd As IPredator
   vcr1.Mate vcr2
   Set dnr = vcr1
   dnr.LayEggs
   Set prd = vcr1
   prd.KillSomethingAndEatIt
End Sub

In order to use methods on the IDinosaur and IPredator interfaces, you must assign the object to a variable of the correct interface type.

Where possible, interfaces designed to use flexible data structures will outlast interfaces based on fixed data types.

As noted above, it’s much harder to go wrong in designing interfaces than in creating large inheritance trees. If you start small, you can have parts of a system running relatively quickly. The ability to evolve the system by adding interfaces allows you to gain the advantages object-oriented programming was intended to provide.

Tools for Creating and Using Interfaces

The mechanics of interface creation are discussed in the following related topics, and in the documentation for the MkTypLib utility.

  • Creating Standard Interfaces with Visual Basic Discusses the tools and procedures Visual Basic provides for creating interfaces.

  • Implementing and Using Standard Interfaces How to reference type libraries to implement and use interfaces. Distribution requirements for type libraries.

For More Information The Implements feature is discussed in detail in "Polymorphism" in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Creating Standard Interfaces with Visual Basic

You can create standard interfaces for your organization by compiling abstract classes in Visual Basic ActiveX DLLs or EXEs, or with the MkTypLib utility, included in the Tools directory.

The MkTypLib utility may be more comfortable for you if you’re an experienced user of Microsoft Visual C++.

Basic programmers may find it easier to create an interface using a Visual Basic class module. Open a new ActiveX DLL or EXE project, and add the desired properties and methods to a class module. Don’t put any code in the procedures. Give the class the name you want the interface to have, for example IFinance, and make the project.

Note The capital "I" in front of interface names is an ActiveX convention. It is not strictly necessary to follow this convention. However, it provides an easy way to distinguish between abstract interfaces you’ve implemented and the default interfaces of classes. The latter are usually referred to by the class name in Visual Basic.

The type library in the resulting .dll or .exe file will contain the information required by the Implements statement. To use it in another project, use the Browse button on the References dialog box to locate the .dll or .exe file and set a reference. You can use the Object Browser to see what interfaces a type library contains.

Important The Implements feature does not support outgoing interfaces. Thus, any events you declare in the class module will be ignored.

As explained in "Providing Polymorphism by Implementing Interfaces," an interface once defined and accepted must remain invariant, to protect applications written to use it. DO NOT use the Version Compatibility feature of Visual Basic to alter standard interfaces.

For More Information The related topic "Providing Polymorphism by Implementing Interfaces" discusses such important concepts as interface invariance and factoring. "Implementing and Using Standard Interfaces" explains how interfaces are implemented and used in components.


Implementing and Using Standard Interfaces

Once you’ve defined a standard interface, either by creating a type library with the MkTypLib utility or by compiling a Visual Basic project containing abstract classes (that is, class modules with properties and methods that don’t contain any code), you can implement that interface in classes your components provide.

Suppose you had a LateCretaceous system that included a number of components, each of which provided objects representing flora, fauna, and business rules of that era. For example, one component might provide a Velociraptor class, while another provided a Tyrannosaur class.

You might create a standard interface named IPredator, which included Hunt and Attack methods:

' Code for the abstract IPredator class module.
Public Sub Hunt()

End Sub

Public Sub Attack(ByVal Victim As IDinosaur)

End Sub

Notice that the argument of the Attack method uses another interface, IDinosaur. One would expect this interface to contain methods describing general dinosaur behavior, such as laying eggs, and that it would be implemented by many classes — Velociraptor, Tyrannosaur, Brontosaur, Triceratops, and so on.

Notice also that there’s no code in these methods. IPredator is an abstract class that simply defines the interface (referred to as an abstract interface). Implementation details will vary according to the object that implements the interface.

For example, the Tyrannosaur class might implement IPredator as follows:

Implements IPredator

Private Sub IPredator_Hunt()
   ' Code to stalk around the landscape roaring, until
   '   you encounter a dinosaur large enough to
   '   qualify as a meal.
End Sub

Private Sub IPredator_Attack(ByVal Victim As IDinosaur)
   ' Code to charge, roaring and taking huge bites.
End Sub

Important As noted in "Providing Polymorphism by Implementing Interfaces," an interface is a contract. You must implement all of the properties and methods in the interface.

By contrast, the Velociraptor class might implement IPredator as shown here:

Implements IPredator

Private Sub IPredator_Hunt()
   ' Fan out and hunt with a pack, running down
   '   small dinosaurs or surrounding large ones.
End Sub

Private Sub IPredator_Attack(ByVal Victim As IDinosaur)
   ' Code to dart in from all sides, slashing the
   '   victim and wearing it down.
End Sub

Using Implemented Interfaces

Once you have classes that implement IPredator, you can upgrade your existing applications one by one to use the new, more competitive interface. You can access the Hunt and Attack methods by assigning a Velociraptor or Tyrannosaur object to a variable of type IPredator, as shown here:

   Dim tyr As New Tyrannosaur
   Dim prd As IPredator
   Set prd = tyr
   prd.Hunt

You can also declare procedure arguments As IPredator, and pass the procedure any object that implements the IPredator interface, as here:

Public Sub DevourTheCompetition(ByVal Agent As _
      IPredator, ByVal Target As IDinosaur)
   Agent.Hunt
   Agent.Attack Target
End Sub

The Sub procedure shown above could be called with any predatory dinosaur as the first argument, and any dinosaur at all as the second. The caller of the procedure can use whatever predatory dinosaur is most appropriate for the occasion. This kind of flexibility is important in maintaining a business advantage.

Setting References to Type Libraries

A type library containing abstract interfaces provides a reference point for both implementing and using interfaces.

In order to implement an interface, you must use the References dialog box to set a reference to the type library. This is because the type library contains the information required to specify the arguments and return types of the interface’s members.

In similar fashion, any application that uses objects which have implemented an abstract interface must also have a reference to the type library that describes the interface. Information about implemented interfaces cannot be included in the type libraries of components, because there is no way to resolve naming conflicts.

Important In order to marshal data between processes or between remote computers, out-of-process components must include in their Setup programs any type libraries that describe abstract interfaces. In-process components should also include these type libraries, because a developer may want to pass its objects to other applications, either on the local computer or on a remote computer. See "Deploying Components," in "Debugging, Testing, and Deploying Components."

Summary

The following list provides an outline for implementing multiple interfaces:

  1. Define a set of interfaces, each containing a small group of related properties and methods that describe a service or feature your system requires. This factoring process is discussed in "Providing Polymorphism by Implementing Interfaces."

  2. Create a type library containing abstract interfaces — abstract classes, if you create the type library by compiling a Visual Basic project — that specify the arguments and return types of the properties and methods. Use the MkTypLib utility or Visual Basic to generate the type library, as described in "Creating Standard Interfaces with Visual Basic."

  3. Develop a component that uses the interfaces, by adding a reference to the type library and then using the Implements statement to give classes secondary interfaces as appropriate.

  4. For every interface you’ve added to a class, select each property or method in turn, and add code to implement the functionality in a manner appropriate for that class. See "Polymorphism" in "Programming with Objects" in the Visual Basic Programmer’s Guide.

  5. Compile the component and create a Setup program, making sure you include the type library that describes the abstract interfaces.

  6. Develop an application that uses the component by adding references to the component and to the type library that describes the abstract interfaces.

  7. Compile the application and create a Setup program, including the component (and the abstract type library, if the component runs out of process or — with the Enterprise Edition — on a remote computer).

The next section discusses how the process outlined here can be used to gradually enhance a system.

Systems that Evolve Over Time

The observant reader will no doubt have noticed a bug in the code given earlier in this topic. If predatory dinosaurs only ate other dinosaurs, how did they keep the Mammals down? A more general IPredator interface might accept as a victim any object that implemented IAnimal.

This illustrates a key advantage of component software development using multiple interfaces: As the LateCretaceous system evolves into, say, the Pleistocene system, components that provide predatory dinosaur objects can be replaced by components that provide SaberTooth and DireWolf objects.

A legacy application compiled to use dinosaurs may be still be able to function quite nicely using the new predator classes, as long as it doesn’t include code specific to dinosaurs.

The key points to remember when using multiple interfaces in this fashion are:

  • Once an interface is defined and in use, it must never change. This concept of interface invariance is discussed in "Providing Polymorphism by Implementing Interfaces," in this chapter, and in "Polymorphism" in "Programming with Objects," in the Visual Basic Programmer’s Guide.

  • If an interface needs to be expanded, create a new interface. This is discussed in "Polymorphism, Interfaces, Type Libraries, and GUIDs," earlier in this chapter.

  • New versions of components can provide new features by implementing new and expanded interfaces.

  • New versions of components can support legacy code by continuing to provide old interfaces.

  • New versions of applications can take advantage of new features (that is, new and expanded interfaces), and if necessary can be written so as to degrade gracefully when only older interfaces are available. (See "Polymorphism, Interfaces, Type Libraries, and GUIDs.")

Implements and Code Reuse

The Implements statement also allows you to reuse code in existing objects. In this form of code reuse, the new object (referred to as an outer object) creates an instance of the existing object (or inner object) during its Initialize event.

In addition to any abstract interfaces it implements, the outer object implements the default interface of the inner object. (To do this, use the References dialog box to add a reference to the component that provides the inner object.)

When adding code to the outer object’s implementations of the properties and methods of the inner object, you can delegate to the inner object whenever the functionality it provides meets the needs of the outer object.

For example, the Tyrannosaur class might implement the interface of a Dinosaur object (instead of an abstract IDinosaur interface). The Dinosaur object might have a LayEggs method, which the Tyrannosaur class could implement by simple delegation:

Private dnoInner As Dinosaur

Private Sub Class_Initialize()
   Set dnoInner = New Dinosaur
End Sub

Private Sub Dinosaur_LayEggs()
   ' Delegate to the inner object.
   dnoInner.LayEggs
End Sub

This is an extremely powerful and flexible way to reuse code, because the outer object can choose to execute its own code before, after, or instead of delegating to the inner object.

For More Information Code reuse with the Implements statement is discussed in more detail in "Polymorphism," in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Organizing Objects: The Object Model

An object model defines a hierarchy of objects that gives structure to an object-based program. By defining the relationships between objects that are part of the program, an object model organizes the objects in a way that makes programming easier.

The public object model of a component is especially important because it’s used by all the programmers who employ the component as part of their applications.

Note Users of C++ or other object-oriented programming languages are used to seeing class hierarchies. A class hierarchy describes inheritance. That is, it shows how objects are derived from simpler objects, inheriting their behavior. By contrast, object models are hierarchies that describe containment. That is, they show how complex objects like Worksheets contain collections of other objects, such as Button, Picture, and PivotTable® objects. Object models can be created with Visual Basic, Visual C++, and other tools that support COM and ActiveX.

"Programming with Objects," in the Visual Basic Programmer’s Guide, includes an introduction to object models and a discussion of design considerations for collection classes. The following topics on object models for components assume familiarity with that material.

  • Do I Need an Object Model? Many components provide one or more unrelated or loosely related objects that clients can create and use.

  • Externally Creatable Objects Discusses the objects you allow clients to create for themselves, using the New operator or the CreateObject function.

  • Dependent Objects Discusses objects clients cannot create, but can use once the component has created them.

  • Combining Externally Creatable and Dependent Objects How the relationship between the two kinds of objects shapes the object model.

  • Using Properties and Collections to Create Object Models Mechanics of fitting objects together into an object model, using properties and collections as glue.

  • Dealing with Circular References Linking objects together into an object model means that they keep references to each other. This has interesting consequences.

For More Information Further information on object models can be found in "ActiveX Component Standards and Guidelines."


Do I Need an Object Model?

You don’t have to create an elaborate object model for your component. A control component (.ocx file) might contain three UserControl objects, and no class modules at all. A code component meant to be used as a simple library of functions might have one global object with a zillion methods.

Again, you might create a code component named Finance with three classes in it, each class representing a self-contained business rule. If the rules are independent of each other, there’s no reason to link them into a hierarchy. A client application that uses these rules simply creates one or more objects of each class, as needed.

Each class module in such a component would have its Instancing property set to MultiUse, so that client applications could create objects from the class, and so that the component could handle multiple objects from each class. Figure 6.5 shows such an object model.

Figure 6.5 A flat object model with several externally creatable objects

Object Models and Interfaces

As the functionality of an object increases, so does the complexity of its interface. This can make the object hard to use.

You can reduce the complexity of an object’s default interface by factoring out groups of related functions, and defining an interface for each group. A client can work with only those interfaces that provide needed features.

By defining standard interfaces in this fashion, you can implement interfaces on other objects, gaining the benefits of polymorphism. This approach to software design in discussed in "Providing Polymorphism by Implementing Interfaces," earlier in this chapter.

Sometimes an object is too large even when its features are factored into separate interfaces. When an object becomes very complex, as for example the TreeView and Toolbar controls, breaking pieces of it off as separate objects may make sense.

Once the whole is divided, you need a way of organizing its constituent parts. Splitting the Node object off from the TreeView control gains you nothing if you can’t show the relationship between them. Object models make it easy to provide this organization to the user of your component.

For More Information Topics relating to object models are listed in "Organizing Objects: The Object Model." The use of multiple interfaces is covered in "Providing Polymorphism by Implementing Interfaces."


Externally Creatable Objects

Part of the additional importance of object models in components comes from the fact that components can provide objects in two different ways — as externally creatable objects or as dependent objects.

In an ordinary program that uses private objects, you can create objects from any class the program defines. A client application, however, can only create objects from some of the classes a component provides. Externally creatable objects are those that a client application can create using the New operator with the Set statement, by declaring a variable As New, or by calling the CreateObject function.

When a client uses one of these mechanisms to request an externally creatable object, the component returns a reference the client can use to manipulate the object. When the client sets the last variable containing this reference to Nothing, or allows it to go out of scope, the component destroys the object.

You can make a public object externally creatable by setting the Instancing property of the class module to any value except Private or PublicNotCreatable.

For More Information A discussion of the Instancing property can be found in "Instancing for Classes Provided by ActiveX Components." Dependent objects are discussed in "Dependent Objects."


Dependent Objects

Sometimes there is a clear relationship between two objects, such that one object is a part of the other. In Microsoft Excel, for example, a Button object is always part of another object, such as a Worksheet.

An object that’s contained in another object is called a dependent object. Client applications can manipulate dependent objects, just as they can manipulate externally creatable objects, but they cannot create dependent objects using CreateObject or New.

Set the Instancing property of a class module to PublicNotCreatable to make the objects created from that class dependent objects.

Note Dependent objects are also referred to as nested objects.

Getting References to Dependent Objects

If a client application can use dependent objects but can’t create them, how are they created?

A component can provide dependent objects in several ways. Most commonly an externally creatable object will have a collection with an Add method which the client can invoke. The component creates the dependent object in the code for the Add method, and returns a reference to the new object, which the client can then use.

For example, a Microsoft Excel Worksheet object has a collection of Button objects. A client application can add a new button to the worksheet by calling the Add method of the Buttons collection, as shown in the following code fragment:

' Note: The variable wsBudget contains a reference to
'   a Worksheet object.
Dim btnOK As Excel.Button
' Parameters of the Add method specify the top, left,
' width, and height of the new button. The return value
' is a reference to the new Button object.
Set btnOK = wsBudget.Buttons.Add(100, 100, 150, 125)
' Set the caption of the new Button object.
btnOK.Caption = "OK"

It’s important to remember that the variable btnOK contains a reference to the object, not the object itself.

Note The distinction between externally creatable objects and dependent objects is made for the benefit of the client applications that manipulate a component’s objects. From within a component, you can always create objects from any of the component’s classes, regardless of the value of the Instancing property.

For More Information "Combining Externally Creatable and Dependent Objects" discusses the process of identifying the types of objects needed for each part of an object model.


Combining Externally Creatable and Dependent Objects

The relationships between the externally creatable objects a component provides and the dependent objects they contain are expressed in the component’s object model. Once you’ve analyzed the functionality your component will provide, you can:

  1. Determine what objects you need to implement that functionality.

  2. List the properties and methods each object will require.

  3. Determine the relationships between the objects.

  4. Identify the top-level objects that need to be created by client applications.

Visual Basic gives you the flexibility to implement many possible object models. A component can provide several unrelated creatable objects, each containing one or more dependent objects; it can also provide a single hierarchy containing a number of objects, only one or two of which are externally creatable.

One characteristic common to all of these implementations is that they require more design time. It’s important to spend adequate time and effort determining how your objects will interact and how they will be used designing your object model to avoid having to redefine objects, or split one object into two, in a future version of your component.

Such changes make it much more difficult for applications that use your component to migrate to newer versions. Adding new objects, or exposing objects that were formerly private, does not cause such problems.

For More Information "Using Properties and Collections to Create Object Models" discusses techniques for linking dependent and externally creatable objects in an object model.


Using Properties and Collections to Create Object Models

Objects in a hierarchy are linked together by object properties, that is, properties that return references to objects. An object that contains other objects will have properties that return either references to the objects themselves, or references to collections of objects.

For example, consider a Bicycle object that contains two Wheel objects; each Wheel object might in turn contain a Rim object and a collection of Spoke objects.Figure 6.6 shows a possible object model for the externally creatable Bicycle object and its dependent objects.

Figure 6.6 An externally creatable object with a hierarchy of dependent objects

The Bicycle object would have a Frame property that returned a reference to its Frame object. The Frame object would have a FrontWheel and BackWheel property, each of which would return a Wheel object. The Wheel object would have a Spokes property that would return a Spokes collection object. The Spokes collection would contain the Spoke objects.

You may also have dependent objects that are used internally by classes in your component, and which you do not want to provide to users of your component. You can set the Instancing properties of the class modules that define these objects to Private, so they won’t appear when users browse your type library.

For example, both the Frame and Wheel objects might have collections of Bearing objects, but there may be no reason to expose the Bearings object, or the collections containing it, for manipulation by client applications.

Important In order to keep the Bearings property from appearing in the type library, you must declare it using the Friend keyword, as described in "Private Communications Between Your Objects," earlier in this chapter.

The Simple Way to Link Dependent Objects

Frequently it makes sense for a complex object to have only one instance of a dependent object. For example, a Bicycle object only needs one Frame object. In this case, you can implement the linkage as a simple property of the complex object:

Private mFrame As Frame

Public Property Get Frame() As Frame
   Set Frame = mFrame
End Property

Private Sub Class_Initialize()
   ' Create the Frame when the Bicycle is initialized.
   Set mFrame = New Frame
End Sub

It’s important to implement such properties as shown above, using a read-only property procedure, rather than simply declaring a public module-level variable, as shown below.

Public Frame As Frame      'Bad idea.

With the second implementation, a user of your component might set the Frame property to Nothing. If there are no other references to the Frame object, it will be destroyed. The effect of this on the Bicycle object is left to the reader’s imagination.

Linking a Fixed Number of Objects

Even when a complex object contains more than one instance of a dependent object, it may make more sense to implement the linkage with properties instead of with a collection. For example, a Bicycle object always has two wheels:

' Create the Wheel objects on demand (As New), instead
'   of in the Bicycle object's Initialize event.
Private mwhlFront As New Wheel
Private mwhlRear As New Wheel

Public Property Get FrontWheel() As Wheel
   Set FrontWheel = mwhlFront
End Property

Public Property Get RearWheel() As Wheel
   Set RearWheel = mwhlRear
End Property

Using Collections in Your Object Model

When the relationship between two objects in a hierarchy is such that the first object contains an indeterminate number of the second, the easiest way to implement the link is with a collection. A collection is an object that contains a set of related objects.

For example, the linkage between the FrontWheel object and its Spoke objects in Figure 6.6 is a collection class. A collection class is a class module that exists solely to group all the objects of another class. In this case, the collection class is named Spokes, the plural of the name of the class of objects it contains. (See "What’s In a Name?", earlier in this chapter, for more information on naming classes.)

Implementing this part of the object model example requires three class modules. From the bottom up, these are:

  • The Spoke class module, which defines the properties and methods of a single spoke.

  • The Spokes class module, which defines a collection object to contain Spoke objects.

  • The Wheel class module, which defines an entire wheel, with a collection of spokes.

Dependent Class: Spoke

The Spoke class module is the simplest of the three. It could consist of as little as two Public variables, as in the following code fragment:

' Properties for Spoke
Public PartNumber As Integer
Public Alloy As Integer

The Instancing property of the Spoke class is set to PublicNotCreatable. The only way for a client application to create a Spoke object is with the Add method of the Spokes collection, as discussed in the next section.

Note This is not a very robust implementation. In practice you would probably implement both of these properties as Property procedures, with code to validate the values that are assigned to them.

For More Information For details on using Property procedures, see "Programming with Objects," in the Visual Basic Programmer’s Guide.

Dependent Collection Class: Spokes

The Spokes class module is the template for a collection Spoke objects. It contains a Private variable declared as a Collection object:

Private mcolSpokes As Collection

The collection object is created in the Initialize method for the class:

Private Sub Class_Initialize()
   Set mcolSpokes = New Collection
End Sub

The methods of the Spokes class module delegate to the default methods of the Visual Basic Collection object. That is, the actual work is done by the methods of the Collection object. The Spokes class might include the following properties and methods:

' Read-only Count property.
Public Property Get Count() As Integer
   Count = mcolSpokes.Count
End Property

' Add method for creating new Spoke objects.
Public Function Add(ByVal PartNumber As Integer, _
               ByVal Alloy As Integer)
   Dim spkNew As New Spoke
   spkNew.PartNumber = PartNumber
   spkNew.Alloy = Alloy
   mcolSpokes.Add spkNew
   Set Add = spkNew
End Function

As with the Spoke class, the Instancing property of the Spokes class is set to PublicNotCreatable. The only way to get a Spokes collection object is as part of a Wheel object, as shown in the following section describing the Wheel class.

For More Information See "Object Models" in "Programming with Objects" in the Visual Basic Programmer’s Guide for a discussion of collections, including a more detailed explanation of delegation, a list of methods you need to implement, and instructions for creating a collection that works with For Each.

Externally Creatable Class: Wheel

The Wheel class module has Instancing set to MultiUse, so that any client application can create a Wheel object. The Wheel class module contains a Private variable of the Spokes class:

' Create the Spokes collection object on demand.
Private mSpokes As New Spokes

Public Property Get Spokes() As Spokes
   Set Spokes = mSpokes
End Property

Every Wheel object a client creates will have its own Spokes collection. The collection is protected against accidentally being set to Nothing by making it a read-only property (Property Get). A developer can access the methods and properties of the Spokes collection as shown in the following code fragment:

Dim whl As Wheel
Dim spk As Spoke
Set whl = New Wheel
Set spk = whl.Spokes.Add PartNumber:=3222223, Alloy:=7
' Call a method of the Spoke object.
spk.Adjust
MsgBox whl.Spokes.Count      ' Displays 1 (one item).

The Add method is used to create a new spoke in the Spokes collection of the Wheel object. The Add method returns a reference to the new Spoke object, so that its properties and methods can be called. A spoke can only be created as a member of the Spokes collection.

The difference between the Wheel object, which can be created by any client, and its dependent objects is the value of the Instancing properties of the classes.

For More Information The Spokes object is created on demand, while the Collection object mcolSpokes was explicitly created. "Programming with Objects" in the Visual Basic Programmer’s Guide discusses the use of As New for creating variables on demand, including performance implications.

Considerations for Linking Objects in an Object Model

Generally speaking, a simpler implementation will be faster. Accessing an item in a collection involves a series of nested references and function calls. Whenever you know that there will always be a fixed number of a dependent object type, you can implement the linkage as a property.

Regardless of how the object model is linked, the key difference between externally creatable objects and dependent objects is the value of the Instancing property of the class module. An object that can be created by other applications will have its Instancing property set to any value except Private or PublicNotCreatable.

All dependent objects, whether they are contained in other dependent objects or in objects that can be created by other applications, will have their Instancing properties set to PublicNotCreatable.

Using Externally Creatable Objects as Dependent Objects

At times you may want to use objects in both ways. That is, you may want the user to be able to create a Widget object independent of the object model, while at the same time providing a Widgets collection as a property of the Mechanism object.

In fact, you may even want to allow the user to create independent instances of the Widgets collection, to move independent Widgets into and out of any Widgets collection, and to copy or move Widgets between collections.

You can make the objects externally creatable by setting the Instancing property of the Widget class and the Widgets collection class to MultiUse.

Important If the Widget object can be created directly by client applications, you cannot depend on all Widget objects getting initialized by the code in the Add method of the Widgets collection. Objects that will be both creatable and dependent should be designed to require no initialization beyond their Initialize events.

Allowing free movement of Widgets requires implementation of Insert, Copy, and Move methods for your collection. Insert and Move are fairly straightforward, because moving or inserting a reference to an object is as good as moving the object. Implementing Copy, however, requires more work.

This is because client application never actually has the object in its possession. All the client application has is a reference to an object the component has created on its behalf. Thus, when you implement Copy, you must create a duplicate object, including duplicates of any dependent objects it contains.

For More Information See "Dealing with Circular References" for a discussion of problems that may arise when linking objects together. See "ActiveX Component Standards and Guidelines" for more information on object models.


Dealing with Circular References

Containment relationships allow you to navigate through a hierarchy from a high-level object to any of the objects it contains.

Object models that strictly express containment are like trees. Any given branch (object) may divide into smaller branches (dependent objects), but the smaller branches do not loop around and rejoin the trunk or lower branches.

Object models with loops, or circular references, result when a dependent object has a property or variable that holds a reference to one of the objects that contains it.

For example, an Order object might have a Contact property that contains a reference to a Contact object, representing the individual who placed the order. The Contact object might in turn have a Company property that contains a reference to a Company object.

Up to this point, the hierarchy is a tree. However, if the Company object has a MostRecentOrder property that contains a reference to the Order object, a circular reference has been created.

Note You could avoid the circular reference in this case by making the MostRecentOrder property a text key that could be used to retrieve the Order object from the component’s Orders collection.

Circular References in Visual Basic Components

Consider the simplest form of circular reference, a Parent property. The Parent property of a dependent object contains a reference to the object that contains it.

For example, in the Microsoft Excel object model, a Button object is contained by a Worksheet object. If you have a reference to a Button object, you can print the name of the Worksheet that contains it using code like the following:

' If the variable btnCurrent contains a reference to a
' Microsoft Excel Button object, the following line of
' code displays the Name property of the Worksheet
' object that contains the button.
MsgBox btnCurrent.Parent.Name

Microsoft Excel is written using C++ and low level COM interfaces, and it maintains the Parent properties of its objects without creating circular references. If you implement such a relationship in Visual Basic, you have to take into account the way Visual Basic handles the creation and destruction of objects.

Visual Basic destroys an object when there are no longer any references to it. If an object’s parent has a collection that contains a reference to the object, that’s enough to keep the object from being destroyed. In the same way, an object continues to exist if the parent has an object property that contains a reference to the object.

When a parent object is destroyed, the variables that implement its properties go out of scope, and the object references are released. This allows the dependent objects to terminate. If a dependent object has a Parent property, however, Visual Basic cannot destroy the parent object in the first place, because the dependent object has a reference to it.

The dependent object cannot be destroyed, either, because the parent has a reference to it. This situation is illustrated for an out-of-process component in Figure 6.7.

Figure 6.7 Circular reference prevents objects from being destroyed

Client application B has released its reference to its Widget object. The Widget object has a reference to a Knob object, whose Parent property refers back to the Widget object, keeping both objects from terminating.

A similar problem occurs if the Widget object contains a collection of Knob objects, instead of a single Knob. The Widget object keeps a reference to the Knobs collection object, which contains a reference to each Knob. The Parent property of each Knob contains a reference to the Widget, forming a loop that keeps the Widget object, Knobs collection, and Knob object alive.

The objects client B was using will not be destroyed until the component closes. For example, if client A releases its Widget object, there will be no external references to the component. If the component does not have any forms loaded, and there is no code executing in any procedure, then the component will unload, and the Terminate events for all the objects will be executed. However, in the meantime, large numbers of orphaned objects may continue to exist, taking up memory.

Note If a circular reference exists between objects in two out-of-process components, the components will never terminate.

Circular References and In-Process Components

If you implement your component as a DLL, so that it runs in the process of the client application, it’s even more important to avoid circular references. Because an in-process component shares the process space of the client application, there is no distinction between ‘external’ and ‘internal’ references to a public object. As long as there’s a reference to an object provided by the component, it stays loaded.

This means that a circular reference keeps an in-process component loaded indefinitely, and the memory taken up by orphaned objects cannot be reclaimed until the client application closes.

For More Information See "ActiveX Component Standards and Guidelines" for more information on object models.


Debugging, Testing, and Deploying Components

This chapter describes the procedures you’ll use to debug, register, and distribute your component, and the version compatibility features that allow you to enhance your component without breaking existing applications that use it.

In addition, you’ll find topics related to distributing components, including Help files, browser strings, and creating versions of your component for use internationally.

"General Principles of Component Design" contains a road map relating the topics covered in these two chapters to the general sequence of development tasks for components.

Topics

Testing and Debugging ActiveX Components

Creating a client project to test your component, plus in-process and out-of-process debugging techniques.
Generating and Handling Errors in ActiveX Components

How to return errors to clients that use your component, and how to handle errors from other components.
Providing User Assistance for ActiveX Components

Adding a Help file, linking objects and their interfaces to Help topics, and providing browser strings.
Deploying ActiveX Components

Using the Package and Deployment Wizard to create a Setup program for your component. Selecting a base address for your in-process component.
Version Compatibility in ActiveX Components

How version compatibility works, and how to ensure that existing clients can safely use new versions of your component.
Localizing ActiveX Components

Structuring your component so that it’s easy to create versions for other locales.

For More Information See "Debugging Your Code and Handling Errors," "International Issues," and "Distributing Your Applications" in the Visual Basic Programmer’s Guide.


Testing and Debugging ActiveX Components

Visual Basic provides two different component debugging scenarios. For in-process components, you can load a test project (Standard Exe or ActiveX Exe) and one or more component projects into the development environment as a project group. You can run all the projects in the group together, and step directly from test project code into in-process component code.

Out-of-process components can be debugged using two instances of the development environment. One instance of Visual Basic runs the test project, while the second runs the component project. You can step directly from test project code into component code, and each instance of Visual Basic has its own set of breakpoints and watches.

Combinations of these scenarios are possible. You can debug an application that uses both in-process and out-of-process components, as shown in Figure 7.1.

Figure 7.1 Debugging in-process and out-of-process components

When an ActiveX Exe project is in run mode, like MyComponent in Figure 7.1, the client application (MyApp) can create objects and access their properties and methods. Each out-of-process component a client uses must be in its own instance of the development environment. The client application and all of its in-process components — DLLs and OCXs — can run together in a single instance of the development environment.

The following topics explain the basics of debugging and testing components.

  • How to Test ActiveX Components By definition, a component is software used by other programs, so it cannot test itself. This topic covers testing requirements and things to watch out for.

  • Creating a Test Project for an In-Process Component Setting references and options for testing in a multiple-project environment.

  • Creating a Test Project for an Out-of-Process Component Starting a second instance of the development environment, and setting the necessary references and options.

  • Project Compatibility: Avoiding MISSING References While developing your component, you will sometimes find it necessary to make changes that break the link between your test program and your component project.

  • Debugging Out-of-Process Components Debugging techniques for working with two instances of the Visual Basic development environment.

  • Debugging In-Process Components Debugging multiple projects in a single instance of the development environment.

  • Using Break on Error in Components New keyboard shortcuts make it easier to debug components, especially those with class module code containing error handlers.

  • How to Test Compiled Components Switching between source projects and compiled components.

For More Information There are special considerations for debugging and testing ActiveX control projects, and other project types that include private controls. See "Setting Up a New Control Project and Test Project" and "Debugging Controls" in "Building ActiveX Controls." Information specific to debugging and testing ActiveX Documents can be found in "Building ActiveX Documents." The fundamentals of debugging are covered in "Debugging Your Code and Handling Errors" in the Visual Basic Programmer’s Guide.


How to Test ActiveX Components

To test a component, you need to create a client application. Components exist to provide objects for clients, which makes it hard to test them by themselves.

Your test project should invoke all the properties, methods, and events of each object provided by your component, testing both valid and invalid values of all arguments.

For example, rather than simply making one call to the Spin method of the Widget object, make a series of calls that try valid and invalid values of all arguments. Pay particular attention to the highest and lowest valid values, as these boundary conditions are a frequent source of problems.

Test for both functionality and error cases. Make sure your component behaves well in case of errors, such as unexpected input. It’s especially important to make sure you’ve covered all error conditions in event procedures of in-process components, because such errors can be fatal to client applications that use the component.

Tip Your test project can also be used to test the compiled component, as described in "How to Test Compiled Components."

Make the Test Program Generic for Better Coverage

You can improve your testing process by making the test program more generic. For example, if you create a text box for each argument of the Spin method, and a button to invoke the method, you can use an automated test tool to maintain and run comprehensive test suites. This makes it easier to test combinations of properties and methods.

Testing Components as Part of an Application

If you’re creating components as part of an application, you can use the application itself as the test program. In theory, thorough testing of the application will discover any problems with its components.

In practice, however, this is rarely true. An application may not exercise all the interfaces of the components it uses, even under stress testing.

It’s also a lot more work to set up test cases when you have to figure out what application behavior must be tested in order to test a particular feature of the component. You’ll be better served by a comprehensive test program that directly tests each element of each object’s interface.

If each component has been tested separately, testing your application with the components provides an extra level of quality assurance.

Creating a Test Project

The test project must be an Exe project. Unless you’re testing call-backs, your test project can be a Standard Exe project. To test call-backs, use an ActiveX Exe project, so it can include public classes that implement the call-back methods. Call-backs are discussed in "Asynchronous Call-Backs and Events" in "Building Code Components."

The way you set the test project up depends on whether you’re testing an in-process or out of process component. The reason for this is explained in "Testing and Debugging ActiveX Components."

The following related topics provide detailed procedures:

  • Creating a Test Project for an In-Process Component Setting references and options for testing in a multiple-project environment.

  • Creating a Test Project for an Out-of-Process Component Starting a second instance of the development environment, and setting the necessary references and options.

Note ActiveX Documents cannot be debugged without a browser or other active document container. See "Building ActiveX Documents" for details.

For More Information See "Testing and Debugging ActiveX Components" for a list of topics related to testing and debugging.


Creating a Test Project for an In-Process Component

This topic describes explains how to set up test projects to exercise most of the objects in-process components can provide. ActiveX documents, however, cannot be tested as described here. See "Building ActiveX Documents."

To create a test project for an in-process component

  1. The test project is loaded in the same copy of the development environment where your component project is loaded. On the File menu, click Add Project to open the Add Project dialog box, click the Standard Exe icon to select it, then click OK to add a Standard Exe project to the project group.

    The caption of the Project window changes to Project Group, with a default name, to indicate that multiple projects are loaded.

    As described in "How to Test ActiveX Components," use an ActiveX Exe project as your test project if your component implements call-backs.

  2. On the File menu, click Save Project Group to save the group containing the component and test project. From now on, you can open both projects simply by opening the project group.

  3. (ActiveX control components skip this step.) Make sure the test project is active — that is, that one of its files is highlighted in the Project window. On the Project menu, click References to open the References dialog box. Locate your component in the list, and check it.

    Note When setting up a test program for ActiveX control projects, don’t set a reference. A control project automatically adds itself to the Components dialog box the first time you place a control on a test project form. For additional information, see "Debugging Controls" in "Building ActiveX Controls."

    If your component still does not appear in the References dialog box of your test project, make sure at least one class module in the component has its Instancing property set to a value other than Private and PublicNotCreatable.

  4. In the Project window, right-click the test project, and click Set As Start Up on the context menu to make the test project the one that runs when you press F5.

    Note Because ActiveX control projects cannot be startup projects, a test project added to an ActiveX control project will automatically assume the startup role. If the test project entry in the Project window is in bold-face type, the test project is already the startup project.

  5. Add code to test the properties and methods of each public class provided by your component.

With the test project selected in the Project window, you can use the Object Browser to verify that the public classes, methods, and properties of your component are available. You can also use the Object Browser to examine and add description strings, and to verify that Help topics are correctly linked.

The view you get in the Object Browser differs depending on which project is currently active — that is, which one is selected in the Project window. When your component project is active, the Object Browser will show both public members and Friend functions. When the test project is active, only the public members are visible.

For More Information Friend functions are discussed in "Private Communications Between Your Objects" in "General Principles of Component Design." Special considerations for debugging ActiveX control projects, including running code at design time, are covered in "Debugging Controls" in "Building ActiveX Controls." ActiveX Documents cannot be debugged without a browser or other active document container. See "Building ActiveX Documents."


Creating a Test Project for an Out-of-Process Component

This topic describes explains how to set up test projects to exercise most of the objects out-of-process components can provide. ActiveX documents, however, cannot be tested as described here. See "Building ActiveX Documents."

To create a test project for an out-of-process component

  1. On the File menu of your out-of-process component (ActiveX Exe project), click Make <project name> to create an executable file.

    Important The reason you create this executable is to help your test project keep its reference to the component project. The executable is not required for debugging.

  2. On the Project menu, click <project> Properties to open the Project Properties dialog box. Select the Components tab, click ActiveX Component in the Start Mode box, and then click OK.

    Use the ActiveX Component setting when testing your component with a client test program. If your component can also function as a standalone desktop application, use the Standalone setting to test that mode of operation, as described in "Debugging Out-of-Process Components."

  3. Press CTRL+F5 to run your component project.

    Important Your component project must be in run mode! When you run your component in the development environment, Visual Basic switches its Windows registry entries from the executable (created in step 1) to the project, and switches them back when you return to design mode. You cannot add a reference to the component project unless it’s in run mode; a reference to the executable will not allow you to debug the component project in the development environment.

    Important Always start your component project by pressing CTRL+F5, or by clicking Start With Full Compile on the Run menu, so that all compilation errors are resolved before your component begins supplying objects to your test application. Compiling component code on demand (the default) can lead to situations in which you have to shut down the component, leaving the test program holding invalid object references. (To disable demand compilation, select Options from the Tools menu, select the General tab of the Options dialog box, and clear Compile On Demand. This affects the current session as well as future instances of Visual Basic.)

  4. Open a second instance of the Visual Basic development environment. In the New Project dialog box, click the Standard Exe icon to select it, and then click OK to open a new Standard Exe project.

    Note As described in "How to Test ActiveX Components," you must use an ActiveX Exe project as your test project if your component implements asynchronous call-backs.

  5. On the Project menu, click References to open the References dialog box. Locate your component in the list, and check it.

    If your component does not appear in the References dialog box, switch back to the component project and make sure it’s running. If the component project does not stay in run mode when you press ctrl+f5, click <Project> Properties on the Project menu, to open the Project Properties dialog box. Select the Component tab, and make sure that ActiveX Component is selected in the Start Mode box.

    If your component is running, but does not appear in the References dialog of your test project, stop the component and make sure at least one of its class modules has its Instancing property set to a value other than Private or PublicNotCreatable.

  6. Add code to test the properties and methods of each public class provided by your component.

    See "Programming with Components" in the Visual Basic Programmer’s Guide for more information on referencing other applications’ objects from within a client application.

    Important Unless you recompile the executable in step 1 each time you make changes to code in your component project, you can only make changes to the test project while the component project is in run mode.

    When the component project is not in run mode, the test project doesn’t have access to its type library, and will fall back on the type library in the compiled executable you created in Step 1.

    The executable created in step 1 prevents the test project from losing its reference to the component project, but does not provide access to the features you’ve added since creating the executable.

You can use the Object Browser in the test project to verify that the public classes, methods, and properties of your component are available. In the test project, the Object Browser will show only the public members.

For More Information "How to Debug Out-of-Process Components" describes debugging techniques. ActiveX Documents cannot be debugged without a browser or other active document container. See "Building ActiveX Documents." "Project Compatibility: Avoiding MISSING References" describes the Project Compatibility option, and explains how to restore missing references.


Project Compatibility: Avoiding MISSING References

In the course of building, testing, and debugging components, you may occasionally see the following error message when you attempt to run your test project: "Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference."

There are two reasons for this error: Either the type library for your component project is not available, or the GUID of your component’s type library has changed, and therefore your test project can no longer locate it. See "Polymorphism, Interfaces, Type Libraries, and GUIDs," in "General Principles of Component Design," for information about GUIDs and type libraries.

The solution varies, depending on the setting of the Version Compatibility option in your component project. You can find the Version Compatibility option on the Component tab of the Project Properties dialog box, available from the Project menu.

If Your Component Is Using Project Compatibility

When you make your component project into an executable (.exe, .dll, or .ocx file) for the first time, as described in "Creating a Test Project for an Out-of-Process Component," Visual Basic automatically sets the Version Compatibility option to Project Compatibility and inserts the path to your made executable. This ensures that your test project and component project remain connected from one development session to the next.

If you’re using Project Compatibility for your component project, the solution to the error depends on whether you’re debugging the component in the development environment, or attempting to test the made executable.

If You’re Debugging the Component Project Out of Process

When you’re debugging an out-of-process component (ActiveX Exe projects) in a second instance of the development environment, the test project loses its connection when the component project is in design mode, because the component project’s type library is not available.

Run your component project. Switch back to the test project and press Esc to dismiss the error message. You can then run the test project.

Note You may encounter this situation with an ActiveX DLL project if you’re debugging an in-process component in a separate instance of the development environment, for example when debugging an in-process component that provides ActiveX documents.

If the Component Project Is Running

If you’ve just made your component executable for the first time, or manually switched from No Compatibility to Project Compatibility — as described later in this topic — the GUID of your component’s type library will have changed, and the test project will be unable to locate it.

Click OK on the error message to open the References dialog box. You’ll see the word MISSING next to your component. Remove the check mark and click OK, to clear the missing reference. On the Project menu, click References to open the References dialog box again. Check your component and click OK.

If You’re Testing the Made Executable Component

In order to switch between the component project and the made executable component, you must make an up-to-date version of the executable, as described in "How to Test Compiled Components." If you haven’t done this, the type library GUIDs of the executable and the component project will not match, and the test project will not be able to find the executable.

Make the component executable. Switch back to the test project and press ESC to dismiss the error message. You can then run the test project.

If Your Component Project Is Using No Compatibility

If you for some reason you want to use the No Compatibility option for your project, you will have to refresh the reference each time you close your project group (in-process components) or projects (out-of-process components) and reopen them.

To refresh a test application’s reference to your component

  1. On the Project menu of your test project, click References to open the References dialog box.

  2. Locate the entry for your component, which will be marked MISSING. Clear the selection, and click OK.

    If there is no entry marked MISSING, locate the component project (the file name and path shown in the References dialog box must have the extension .vbp) and check it, then click OK. Skip to step 4.

    If your component project does not appear, click OK to close the dialog.

  3. Make sure your out-of-process component is running. If your component isn’t in run mode, there’s no type library for the test application to get a reference to.

  4. Open the References dialog box again. Locate the entry for your component project (.vbp file), and verify that it’s no longer marked MISSING. Check it and click the OK button.

  5. Press F5 to run the test application.

Setting the Project Compatibility Option

The easiest way to set Project Compatibility is to make your component executable (.exe, .dll, or .ocx) for the first time. As described earlier, Visual Basic automatically sets Project Compatibility the first time you make the executable.

If for some reason your project has been set to No Compatibility, you can manually change it to one of the other two Version Compatibility options. Binary Compatibility, described in "Version Compatibility," later in this chapter, is the option to use if you’re developing an enhanced version of an existing component.

The option you should use for all new development is Project Compatibility.

To set the Project Compatibility option

  1. On the File menu of your component project, click Make <project name> to create an executable file. If there’s an existing executable, overwrite it.

    The only reason to make an executable at this point is to help your test project keep its reference to the component project.

  2. On the Project menu of your component project, click <project> Properties to open the Project Properties dialog box.

  3. On the Component tab, click Project Compatibility in the Version Compatibility box.

  4. Type the name of your executable in the box at the bottom of the Version Compatibility box, or use the ellipsis button to browse for it, and then click OK.

Note The Project Compatibility option actually has nothing whatever to do with the Binary Compatibility option (described in "Version Compatibility.")

For More Information Type libraries and GUIDs are discussed in "Polymorphism, Interfaces, Type Libraries, and GUIDs," in "General Principles of Component Design." "Debugging Out-of-Process Components" explains important aspects of debugging, such as the fact that your component project must be in run mode when you edit your test project.


Debugging Out-of-Process Components

This topic describes debugging procedures for most objects provided by out-of-process components. ActiveX documents, however, cannot be debugged as described here. See "Building ActiveX Documents."

To debug an out-of-process component

  1. Set breakpoints and watch expressions as needed in your class module code.

  2. Run the component project by pressing CTRL+F5 or clicking Start With Full Compile on the Run menu.

    The component will compile, but Sub Main will not run until the first object is created in response to a client request.

  3. Start a second instance of Visual Basic, and open your test project. Set breakpoints and watch expressions as needed.

  4. Run the test project. When a breakpoint is encountered in the component code, the component project receives focus.

    Note When an out-of-process component enters break mode, focus may not immediately switch to the component project. If you click anywhere on the client, the Component Busy dialog box will be displayed. Click Switch To button to give the focus to the component project.

  5. When you’re finished with the debugging session, close your test project before stopping the component.

    Important If you stop the component project before closing your test project, all of the objects the component was providing will be destroyed. Your test project may get errors when you try to close it.

Start With Full Compile

Always start your out-of-process component project by pressing CTRL+F5, or by clicking Start With Full Compile on the Run menu, so that all compilation errors are resolved before your component begins supplying objects to your test application.

The default in Visual Basic is to compile code on demand. This means that there may be code in your component which is not compiled until the client calls it. Some compile errors cannot be fixed without returning to design mode, which means shutting down the component. In that case, the test program is left holding invalid object references.

Note To disable demand compilation, select Options from the Tools menu, select the General tab of the Options dialog box, and clear Compile On Demand. This affects the current session as well as future instances of Visual Basic.

Editing the Test Project

As you develop your component, you’ll frequently add new functionality between debugging sessions. Before editing your test project to add code to exercise the new features, be sure to put your component project in run mode.

When the component project is not in run mode, the test project doesn’t have access to its type library. If you’re using Project Compatibility, the test project will fall back on the type library in the compiled executable. If it’s been a while since you made the executable, the type library won’t contain information on the features you’ve added.

If you’re using the No Compatibility option of Version Compatibility, there will be no type library information at all.

Your component project should always be in run mode when you’re editing the test project.

Shutting Down an Out-of-Process Component

When you’re debugging objects that have Terminate event code, remember that the Terminate event is not executed if you stop your component project using the End button on the toolbar, or if you choose End or Restart from the Run menu.

Shutting down test projects will release all the objects your component is providing to those projects. As those objects close down, they should release all references to private objects within your component.

This allows your component to meet the four rules for component shutdown, as listed in "Starting and Ending a Component," in "General Principles of Component Design."

Unfortunately, even when all objects have been released by your test projects, and all four shutdown conditions have been met, an out-of-process component will not return to design mode. To return to design mode, you will have to use the End button.

If your component maintains internal references to objects, you can simulate normal shutdown by creating a Sub procedure that releases all such references. You can call this procedure from the Immediate window before stopping your component — or place an End statement in the Sub. However, there is no guarantee that the order in which private objects are terminated will be the same under actual shutdown conditions.

The only way to accurately test shutdown behavior is with the compiled component.

Tip Debugging causes artificial changes in focus that may prevent your code from behaving as you expect. For debugging situations that are sensitive to focus or activation, such as mouse and key events, use Debug.Print to log debugging information.

Debugging Components as Standalone Desktop Applications

If your out-of-process component can double as a standalone desktop application, like Microsoft Excel, you have two startup and shutdown modes to test.

Such applications typically have code like the following, to allow them to start up with or without a main window:

Sub Main
   If App.StartMode = vbSModeAutomation Then
      ' ...code to start invisibly...
   Else      ' (App.StartMode = vbSModeStandalone)
      ' ...code to show main form...
   End If
End Sub

StartMode is a read-only property of the App object which can be used at run time to determine whether your application was started in response to a request from a client application, or by the user, through the Start button on the Taskbar.

Note Do not place Sub Main in a class module. Placing Sub Main in a class module turns it into a method named Main, rather than a startup procedure. Your Sub Main procedure must be in a standard module.

While debugging, you can control the mode your component starts in using the settings in the Start Mode box, on the Component tab of the Project Properties dialog box. Use the Standalone setting to start your component as if the user had opened it using the Start button on Taskbar; use the ActiveX Component setting to compile your component so that Sub Main will run when the client (your test project) first creates an object.

Debugging with Multiple Clients

If you need to debug a problem that only occurs when multiple client applications are using objects from your component, you can start additional instances of Visual Basic, or compile your test project and run the .exe file to provide the extra instances. You can run as many client applications as you need.

Components can be debugged whether they’re being called from a different Visual Basic project or from a client written in another language. For example, if you’re designing a component specifically for Microsoft Excel users, you may want to use Microsoft Excel as the client when you’re debugging.

For More Information "Using Break on Error in Components" explains how to use the Error Trapping options with component projects. See "Testing and Debugging ActiveX Components" for a list of topics related to testing and debugging.


Debugging In-Process Components

This topic describes debugging procedures for most objects provided by in-process components. ActiveX documents, however, cannot be debugged as described here. See "Building ActiveX Documents."

To debug an in-process component

  1. Set breakpoints and watch expressions as needed in your class module or UserControl code.

  2. Run the project group by pressing CTRL+F5 or clicking Start With Full Compile on the Run menu.

    The in-process component will compile, but Sub Main will not run until the first object is created in response to a client request.

    While debugging, you can step from the test project directly into code in the component project.

  3. When you’re finished with the debugging session, click the Close button on test project’s main window to return to design mode.

    Important Closing the test project by clicking the End button will cause all projects to close. Objects in the component will not receive Terminate events. Shutting down in this fashion will not correctly test the shutdown behavior of your component.

Start With Full Compile

You may find it more convenient to start the project group by pressing CTRL+F5, or by clicking Start With Full Compile on the Run menu, so that all compilation errors are resolved before your component begins supplying objects to your test application.

The default in Visual Basic is to compile code on demand. This means that there may be code in your component which is not compiled until the client calls it. Some compile errors cannot be fixed without returning to design mode, which means returning the whole project group to design mode.

Note To disable demand compilation, select Options from the Tools menu, select the General tab of the Options dialog box, and clear Compile On Demand. This affects the current session as well as future instances of Visual Basic.

Shutting Down an In-Process Component

In-process components will not unload while running in the development environment, even if all references to objects are released by the test project, and all other component shutdown conditions are met. The only way to test DLL unloading behavior is with the compiled component.

For More Information "Using Break on Error in Components" explains how to use the Error Trapping options with component projects. ActiveX control projects must be debugged while the client is in design mode, as well as run mode. See "Debugging Controls" in "Building ActiveX Controls." ActiveX Documents cannot be debugged without a browser or other active document container. See "Building ActiveX Documents." See "Testing and Debugging ActiveX Components" for a list of topics related to testing and debugging.


Using Break on Error in Components

You can change the way Visual Basic enters break mode when an error occurs in your component by setting the Error Trapping option in your component project.

In your component project, choose Options from the Tools menu to open the Options dialog box, and select the General tab. There are three options in the Default Error Trapping State box, as described below.

Note The setting in the Default Error Trapping State box controls the default setting of the option when you start Visual Basic; it also toggles the option in the current session. If you wish to change the setting for the current session only, without affecting the default, select Toggle from the code window context menu.

Suppose you have a component that provides a Widget object that has a Spin method. The following descriptions assume that the test application has called the Spin method of the Widget object, and that an error has occurred in the Spin method’s code.

  • Break on All Errors: The component project is activated, and the Spin method’s code window receives the focus. The line of code that caused the error is highlighted. Visual Basic always enters break mode on such an error, even if error handling is enabled in the Spin method.

    Note You can press Alt+F8 or Alt+F5 to step or run past the error.

  • Break in Class Module: If error handling is not enabled in the Spin method, or if you are deliberately raising an error for the client by calling the Raise method of the Err object in the Spin method’s error handler, the component project is activated, and the Spin method’s code window receives the focus. The line of code that caused the error is highlighted.

    Note You can press Alt+F8 or Alt+F5 to step or run past the error.

    If error handling is enabled in Spin, then the error handler is invoked. As long as you don’t raise an error in the error handler, Visual Basic does not enter break mode.

  • Break on Unhandled Errors: Visual Basic never enters break mode in properties or methods of the component. If error handling is not enabled in the client procedure that called the Spin method, execution stops on the line of code that made the call.

    To understand the behavior of Break on Unhandled Errors in a component project, remember that the component’s properties and methods are always called by somebody else. An error in a property or method can always be handled by passing it up the call tree into the client procedure that called the property or method.

    Note When an out-of-process component enters break mode, focus may not immediately switch to the component project. If you click anywhere on the client, the Component Busy dialog box will be displayed. Click the Switch To button to give the focus to the component project.

For More Information See "Debugging Your Code and Handling Errors" in the Visual Basic Programmer’s Guide.


How to Test Compiled Components

When you choose Make from the File menu, your component will be registered automatically in the Windows Registry. You can switch your test application between the component project and the compiled component using the procedures in this topic.

In-Process Components

The following procedures perform the switch for an in-process component (ActiveX DLL project or ActiveX control project).

To switch from an in-process component project to the compiled .dll or .ocx file

  1. On the File menu, click Make <projectname> to create the compiled in-process component.

  2. In the Project window, select the component project.

  3. On the File menu, click Remove <projectname> to remove the component project from the project group.

    A warning message will appear: "The project is referenced from another project. Are you sure you want to remove it?" Click OK to remove the project.

  4. Press F5 to run the test project.

    Visual Basic automatically switches references to the compiled .ocx or .dll file.

To switch back to testing your in-process component project

  1. On the File menu, click Add Project to open the Add Project dialog box.

  2. Use the Recent or Existing tab to open your component project.

  3. Press F5 to run the test project.

    Visual Basic automatically switches references back to the component project.

Out-of-Process Components

Remember that an out-of-process component must be tested using two instances of the development environment. The following procedures perform the switch for an out-of-process component (ActiveX Exe project).

To switch from an out-of-process component project to the compiled executable

  1. On the File menu of the component project, click Make <projectname> to create an up-to-date version of the executable.

    Important If you neglect to make the executable, you’ll get the following error message when you attempt to run your test project: "Connection to type library or object library for remote process has been lost. Press OK for dialog to remove reference."

  2. In the test project, press F5 to run the project.

    When the component project is not running, Visual Basic automatically switches registry entries to point to the compiled executable.

To switch back to testing your out-of-process component project

  1. In the component project, press Ctrl+F5 to run the project.

  2. In the test project, press F5 to run the project.

    When the component project is running, Visual Basic automatically switches registry entries back to it.

You may also want to create an executable file from your test application, and run several instances of it to test your component with multiple client applications.

Testing Your Component with Other Applications

You can test your component from any application that can make Automation calls. For example, you can open a Microsoft Excel module, add a reference to your component by choosing References from the Project menu, and write procedures to create and use objects provided by your component.

Even if you do not expect your component to be used as an extension of end user software tools like Microsoft Excel and Microsoft Access, it’s a good idea to test it with such tools. The more programming tools your component works with, the more value it will have for your company or for your customers.

For More Information See "Testing and Debugging ActiveX Components" for a list of topics related to testing and debugging.


Generating and Handling Errors

Generating and Handling Errors in ActiveX Components

There’s no such thing as an unhandled error in a component. Untrapped errors in a method of your component, or errors you generate using the Raise method of the Err object, will be raised in the client application that called the method.

Raising errors or returning error codes to the client is the appropriate behavior for components. A well behaved component does not intrude on the client application’s user interface by displaying message boxes containing error text.

Users of an application may be blissfully unaware that your component is part of the application they’re using. Seeing error messages from a program unknown to them will not improve their day, or help them solve the problem.

The following topics discuss the strategies and mechanics of returning error information to the client application, for the application’s developer to deal with as she chooses.

  • Deciding How to Generate Error Messages Discusses two major strategies — raising errors (Basic-style) versus returning error codes (Windows API-style).

  • Guidelines for Raising Errors from Your Component Discusses the correct format for information in errors raised by components.

  • Handling Errors in a Component How to handle internal errors, in particular those raised by components your component is using.

For More Information The basics of error handling are discussed in "Debugging Your Code and Handling Errors," in the Visual Basic Programmer’s Guide.


Deciding How to Generate Error Messages

When an application calls a method of an object your component provides, there are two general ways in which the method can provide error information.

  • Basic-style: The method can raise an error. The client application can implement an error handler to trap errors that may be raised by the method.

  • Windows API-style: The value returned by the method can be an error code. The client application can examine the return value to determine whether an error has occurred.

There are a number of programming tradeoffs to consider when you select an error generation strategy for your component, but the most important consideration should be the convenience of the developer who will use your component.

For example, if your methods always return an error value, the developer using your component must use in-line error handling, that is, the developer must always test the return value after calling a method. This is the way Windows API calls work.

If you raise errors, on the other hand, the developer has the choice of implementing in-line error handling (On Error Resume Next) or writing error-handling routines (On Error GoTo). This flexibility is a hallmark of the coding style familiar to Basic developers.

Be Consistent

Whichever error generation strategy you adopt, be consistent. Developers will not appreciate having to test return values from some methods, and trap errors from others. The more difficult it is to use a component, the less benefit there is from re-using the code.

If you decide to return error values instead of raising them, it’s better for all return values to be error codes. This means that if a method also returns a data value, you must use a ByRef parameter for the data. While this is an inconvenience for the user of the method, it’s less of an inconvenience than having to test the data type of a return value, to see whether it’s an error, before using it as a data value.

Note Client applications that use out-of-process components should always employ some form of error handling, because failures in the underlying cross-process communication layer will be raised as errors in the client application.

For More Information "Raising Errors from Your Component" discusses standards and techniques for raising errors from ActiveX components.


Guidelines for Raising Errors from Your Component

Use the Raise method of the Err object to raise errors that can be trapped by client applications. When you call the Raise method in the error handler of one of your methods or Property procedures, or when error handling is turned off (On Error GoTo 0), the error will be raised in the client application, in the procedure that directly called your method.

If the procedure that called your method has no error handler, the error condition moves up the call tree of the client until it reaches a procedure that has an error handler, just as any other error would.

When raising an error condition from your component, you don’t need to worry about whether the client is written in Visual Basic, Microsoft Visual C++, or another programming language. Any client application can receive the errors your component raises.

Here are a few guidelines you should follow when raising errors from a component.

  • The error number you return to client applications is generated by adding an intrinsic constant (vbObjectError) to your internal error number. The resulting value is the one you should document for your users.

  • The internal error numbers you add to vbObjectError should be in the range 512 to 65535 (&H200 to &HFFFF). Numbers below 512 may conflict with values reserved for use by the system.

  • Establish a "fatal error" or "general failure" code and message for conditions from which your component can’t recover.

  • When calling Err.Raise, supply both an error number and a text string describing the error.

  • Document your errors in the Help file for your component. For the convenience of your users, you may want to show error numbers in both decimal and hexadecimal format.

For example, you might implement the SpinDirection property of the Widget object as a Property procedure, to ensure that it accepts only certain values, as in the following code fragment from the Widget class module of the hypothetical SmallMechanicals component.

' Enumeration for SpinDirection property values.
'   (The prefix "sm" identifies it as belonging to the
'   SmallMechanicals component.)
Public Enum smSpinDirection
   smSDClockwise
   smSDCounterClockwise
End Enum

' Module-level storage for SpinDirection property.
Private msdSpinDirection As smSpinDirection

' Implementation of the SpinDirection property.
Property Get SpinDirection() As smSpinDirection
   SpinDirection = msdSpinDirection
End Property

Property Let SpinDirection(ByVal sdNew As _
      smSpinDirection)
   ' The Select Case does nothing if sdNew contains
   ' a valid value.
   Select Case sdNew
      Case smSDClockwise
      Case smSDCounterClockwise
      Case Else
         Err.Raise _
            (ERR_SPN_INVALID + vbObjectError), _
            CMP_SOURCENAME, _
            LoadResString(ERR_SPN_INVALID)
   End Select
   ' If no error, assign the new property value.
   msdSpinDirection = sdNew
End Property

The code above assumes that ERR_SPN_INVALID and CMP_SOURCENAME are public constants declared in a standard module, and that error text strings are stored in a resource file.

Because there is no error handler in the Property Let procedure used to set the value of the SpinDirection property, the error is raised in the client application, in the procedure that attempted to set the invalid value.

The text string for the error message is loaded from the component project’s resource file, using the internal error number as an index. This technique reduces the amount of memory required to run the component. By concentrating all the text strings in one place, it also simplifies the creation of international versions of the component.

Note Programmers who have used the C++ language will recognize vbObjectError as facility interface (FACILITY_ITF), the base constant for the range of errors reserved for a component’s interface.

For More Information "Handling Errors in a Component" discusses the handling of internal errors, particularly those raised by components your component is using. For more on the Err object and resource files, see "Err object" in the Language Reference and "resource files" in the index.

Providing error messages in multiple languages is discussed in "Localizing Components," later in this chapter, and in "International Issues" in the Visual Basic Programmer’s Guide.


Handling Errors in a Component

When authoring a component, you should be prepared to handle three types of errors:

  • Errors generated in your component code that you handle internally.

  • Errors generated in your component code that you want to pass back to the client application.

  • Errors generated by another component from which your component has obtained object references.

Handling Errors Internally

Handling the first of these three error types is no different for a component than for any other application developed using Visual Basic. This type of error handling is covered in "Debugging Your Code and Handling Errors," in the Visual Basic Programmer’s Guide.

Passing Errors Back to the Client

As outlined in "Raising Errors from Your Component," you can use the Raise method of the Err object to raise errors in a client application that calls methods provided by your component. For the error to be raised in the client, you must call Raise from your method’s error-handling routine, or with error handling disabled, as in the following fragment of code from the Run method of a hypothetical Widget object.

Public Sub Run()
   ' Use in-line error handling.
   On Error Resume Next
   ' (Many lines of Run method code omitted.)
   ' If the following test fails, raise an error in
   ' the client that called the method.
   If intWidgetState <> STATE_RUNNING Then
      ' Disable in-line error handling.
      On Error Goto 0
      Err.Raise _
         Number:=(ERR_WDG_HALTED + vbObjectError), _
         Source:=CMP_SOURCENAME, _
         Description:=LoadResString(ERR_WDG_HALTED)
   End If
   ' (Run method code continues.)
End Sub

When you raise errors for invalid parameter values, you can simply place the Err.Raise statements at the beginning of the method, before the first On Error statement. The same is true of code to validate property values; place it at the beginning of the Property Let or Property Set.

Raising Errors from Error Handlers

Frequently your properties and methods will include code to handle internal errors. Such code may from time to time contain cases in which no sensible internal response is possible. In such cases, it’s reasonable for the property or method to fail and return an error to the client:

   On Error Goto ErrHandler
   ' (Code omitted.)
   Exit Sub

ErrHandler:
   Select Case Err.Number
      ' Errors that can be handled (not shown).
      Case 11
         ' Division by zero cannot be handled in any
         ' sensible fashion.
         Err.Raise ERR_INTERNAL + vbObjectError, _
            CMP_SOURCENAME, _
            LoadResString(ERR_INTERNAL)
      ' Other errors (not shown).
   End Select

In most cases it doesn’t make sense to return the original "Divide by zero" error to the client. The client has no way of knowing why such an error occurred within your property or method, so the message is of no use in finding a solution.

If the developer of the client was aware of the possibility of this internal error, because of the excellent documentation you provided, he may have included code to handle it, and shown the end user of the application a useful error message.

If the developer didn’t handle it, ERR_INTERNAL might at least serve a diagnostic purpose by including the method parameters in the error message text, as information to pass on to the author of the component.

Handling Errors from Another Component

If your component uses objects provided by another component, you must handle errors that may result when you call methods of those objects. You should be able to obtain a list of these errors from the second component’s documentation.

It’s important to handle such errors within your component, and not return them to the client application that called your component. The client was written to use your component, and to respond to your component’s error codes. The user or programmer who wrote it may have no knowledge of secondary components you’re using.

Encapsulation of Errors

The idea that a client application should receive errors only from the components it calls directly reflects the object-oriented concept of encapsulation. In the case of errors, encapsulation means that an object is self-contained. Although the object in your component may use other objects supplied by other components, the client application is ignorant of those objects.

For this reason, you should always specify the source argument of the Raise method when you handle an error from another component. Otherwise, the Source property of the client application’s Err object will contain the name of the component your component called.

Note You should not use the value of the Source property of the Err object for program flow decisions. The Source property may provide useful context information when you’re debugging your component, but the text it contains may be version dependent.

The following code fragment shows the error handler for a method that uses objects in another component. When it encounters an error it cannot handle, or an unanticipated error, the error handler raises an error for the client, using its own error numbers and descriptions.

' Code from the Spin method of a hypothetical Widget
' object.
Public Sub Spin(ByVal Speed As Double)
   On Error Goto ErrHandler
   ' Code containing calls to objects in the Gears
   ' component (omitted).
   Exit Sub

ErrHandler:
   Select Case Err.Number
      ' Other errors (not shown).
      ' --- Errors from the Gears component. ---
      Case vbObjectError + 2000
         ' Error 2000 is handled by shifting to
         ' another gear (code not shown).
      Case vbObjectError + 3000
         ' Error 3000 causes spin-down; error must be
         ' returned to caller.
         Err.Raise ERR_SPN_SPINDOWN + vbObjectError, _
            CMP_SOURCENAME, _
            LoadResString(ERR_SPN_SPINDOWN)
      ' --- Unanticipated errors from components. ---
      Case vbObjectError To (vbObjectError + 65536)
         Err.Raise ERR_SPN_FAILURE + vbObjectError, _
            CMP_SOURCENAME, _
            LoadResString(ERR_SPN_FAILURE)
      ' Other errors (not shown).
   End Select
End Sub

Note Some components use older error return mechanisms. Such components may return error numbers in the range 0–65535.

When the Gears component raises Error 3000, the Spin method must return an error to the application that called it. The error returned is an error of the Spin method. The application using the Spin method doesn’t know that Spin is implemented using the Gears component, and could not be expected to deal with Error 3000. All it needs to know is that a spin-down condition has occurred.

In the preceding example a global constant, CMP_SOURCENAME, is used for the source argument of the Raise method. If you raise errors outside an error handler, and do not specify the source argument, Visual Basic uses the name you entered in the Project Name field of the General tab in the Project Properties dialog box, combined with the Name property of the class module.

While this may be useful for you to know while debugging your component, it’s better for the compiled component to specify a consistent value for the source argument in all errors it raises.

Note It’s frequently easier to use in-line error handling when making calls to components, because the same error may require a different response depending on the method that was called, or the circumstances in which it was called. If you’re calling two different components, they may use the same number for different errors.

Bending Encapsulation Rules

There may be cases in which some of the error messages returned by a secondary component may contain information of use to the end user of the client application. For example, an error message might contain the name of a file.

In such cases, you may want to bend encapsulation rules to pass along this information. The safest way to do this is to include the text of the Description property of the Err object in your own error message text, as in the following fragment of error-handling code:

' Error 3033 is returned from a call to another
' component; the message text contains a file name that
' will be useful in resolving the problem.
Case vbObjectError + 3033
   ' Raise a Print Run Failure error to the client,
   ' and append the message text from Error 3033.
   Err.Raise _
      ERR_PRINTRUNFAILURE + vbObjectError, _
      CMP_SOURCENAME, _
      LoadResString(ERR_PRINTRUNFAILURE) _
         & Err.Description

In some cases, it may be useful to the end user to know what component originated the error, and the original error number and message text. You can include all of this information in the description of the error you raise.

For More Information See "Debugging Your Code and Handling Errors," in the Visual Basic Programmer’s Guide, or see Error or Err in the Language Reference.


Providing User Assistance for ActiveX Components

Creating a Help file for your component is highly recommended. One of the most important benefits of components is enabling code reuse, a goal you will be much more likely to achieve if developers and end users can easily get Help for the objects you’ve authored. ActiveX components support both Windows Help and HTML Help files.

You can also provide browser strings that briefly describe your objects and their properties, methods, and events. Users of your component can view these description strings using the Object Browsers in their programming tools, and jump to the Help topics you’ve provided.

The following topics explain how to use these user assistance features.

  • How to Specify a Help File for Your Component Set the name of a Help file containing topics you can link to your classes and their members (properties, methods, and events).

  • Providing Help and Browser Strings for Objects Use the Object Browser to enter browser strings and links to Help topics for your classes and their properties, methods, and events.

  • Providing Help and Browser Strings for Properties, Methods, and Events Use the Procedure Attributes dialog box to enter browser strings and links to Help topics for properties, methods, and events.

For More Information See "Adding Help to Your Application" in the Programmer's Guide.

When it comes to user assistance, don’t underestimate the importance of choosing good names for your objects and their interfaces. See "What’s in a Name?" in "General Principles of Component Design," and "Object Naming Guidelines" in "ActiveX Component Standards and Guidelines."


How to Specify a Help File for Your Component

To specify a Help file for your component

  1. On the Project menu, click Project Properties to open the Project Properties dialog box, then click the General tab.

  2. In the Help File Name box, enter the path and name of the Help file you‘ve created for your component.

  3. In the Project Help Context ID box, enter the context ID for the specific Help topic to be called when the user clicks the "?" button while your component’s type library is selected in the Object Browser.

For More Information "Providing Help and Browser Strings for Objects" gives the procedure for linking objects and their interface members to Help topics. User assistance features available for components are listed in "Providing User Assistance for ActiveX Components."


Providing Help and Browser Strings for Objects

Objects are created from classes. The Object Browser displays information about the classes from which objects are created.

The following procedure can be used to link Help topics and provide browser strings for classes, and for their properties, methods, and events. When Visual Basic creates the type library for your component, it includes this information. Users of your component can view the description strings using the Object Browsers in their programming tools, and jump to your Help topics.

For an alternate method of supplying Help and browser strings for properties, methods, and events, see "Providing Help and Browser Strings for Properties, Methods, and Events."

To enter description strings and link your classes and their members to Help topics

  1. Press F2 to open the Object Browser. In the Project/Library box, select your project.

    If you’re not sure which is the Project/Library box, hover the mouse pointer over the boxes until you see the tool tip.

  2. In the Classes list, right click the name of a class to bring up the context menu, and click Properties to open the Member Options dialog box.

    Alternatively, in the Members list, right click the name of a property, method, or event to bring up the context menu, and click Properties to open the Member Options dialog box.

  3. In the Help Context ID box, type the context ID of the Help topic to be shown if the user clicks the "?" button when this class or member is selected in the Object Browser.

    The path and name of the Help file for the project should appear in the Help File box. If it does not, see "How to Specify a Help File for Your Component" for instructions on setting it.

  4. In the Description box, type a brief description of the class or member.

  5. Click OK to return to the Object Browser. The description string you entered should appear on the panel at the bottom of the browser.

  6. Repeat steps 2 through 5 for each class and for each member of each class.

Figure 7.2 shows the Object Browser and Member Options dialog box as they would appear when setting the description and Help context ID for a class in a hypothetical test application.

Figure 7.2 Setting the description and Help context ID

Note You can enter Help context IDs and descriptions for private classes in your component, but this information will only be available to you when you’re actually working on your component project. It will not appear in the type library for your component.

You cannot supply browser strings or Help topics for enumerations.

For More Information Creating Help files is discussed in the Microsoft Windows Help Authoring Kit, available from Microsoft Press. User assistance features available for components are listed in "Providing User Assistance for ActiveX Components."


Providing Help and Browser Strings for Properties, Methods, and Events

You can use the Procedure Attributes dialog box to enter description strings for your properties, methods, and events, and to provide links to topics in your Help file. When Visual Basic creates the type library for your component, it includes this information. Users of your component can view the description strings using the Object Browsers in their programming tools, and jump to the Help topics.

You can use the Visual Basic Object Browser to enter description strings for your classes, as described in "Providing Help and Browser Strings for Objects." The Object Browser can also be used to enter this information for properties, methods, and events.

To enter description strings and link Help topics to your properties, methods, and events

  1. In the Project window, select a module and press F7 (or click View Code on the Project window toolbar) to open its code window.

  2. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  3. Select a property, method, or event in the Name box.

  4. In the Help Context ID box, type the context ID of the Help topic to be shown if the user clicks the "?" button when this member is selected in the Object Browser.

    The path and name of the Help file for the project should appear in the Help File box. If it does not, see "How to Specify a Help File for Your Component" for instructions on setting it.

  5. In the Description box, type a brief description of the member.

  6. Click the Apply button to save the information.

  7. Repeat steps 3 through 6 for each property and method in the module.

Figure 7.3 shows the Procedure Attributes dialog box as it would appear when setting the description and Help context ID for a member of a class in a hypothetical test application.

Figure 7.3 Setting the description and Help context ID

Note You can enter Help context IDs and descriptions for members of private classes in your component, but this information will only be available to you when you’re actually working on your component project. It will not appear in the type library for your component. You cannot supply browser strings or Help topics for enumerations.

For More Information Creating Help files is discussed in the Microsoft Windows Help Authoring Kit, available from Microsoft Press. User assistance features available for components are listed in "Providing User Assistance for ActiveX Components."


Deploying ActiveX Components

In addition to providing objects for use by client applications, some components can function as standalone desktop applications, in the way Microsoft Excel does. If your component is in this category, you can distribute it as you would any Visual Basic application.

"Distributing Your Applications," in the Visual Basic Programmer’s Guide, contains all the information you need to use the Package and Deployment Wizard, or to create a custom Setup for your application.

This topic and the following related topic discuss the distribution of components that are not standalone applications.

  • Setting Base Addresses for In-Process Components In-process components use memory more efficiently if you choose a good base address, thus improving performance on your users’ computers.

Ways to Distribute Components

There are several ways to distribute a component. For example:

  • As part of your own Visual Basic applications.

  • As a tool users can access from Automation-enabled desktop applications such as Microsoft Excel and Microsoft Access.

  • As a component other developers can include in their applications, or use with the Internet.

  • As part of an enterprise application, running on a remote computer (requires the Enterprise Edition of Visual Basic).

For all of these distribution scenarios except the first, you can create a standalone Setup for your component.

Distributing a Component as Part of a Visual Basic Application

To distribute your component as part of a Visual Basic application, you can use Package and Deployment Wizard to create a setup program for the application. If your application has a reference to the component, Package and Deployment Wizard will locate the component using its registry entries, and include it — along with its support files — in the list of files needed to create distribution media.

As with components that are also standalone desktop applications, this scenario is largely covered by ordinary application setup. The only additional consideration is the use of implemented interfaces.

Including Type Libraries for Implemented Interfaces

If you’ve used the Implements keyword to add additional interfaces to your classes, as described in "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design," you may need to include the type libraries for those interfaces in Setup for your component.

You need to include the type library that includes a particular interface if:

  • Any classes the interface has been added to are provided by out-of-process components.

  • The application will provide objects to other applications, and some of those objects implement the interface.

The reason type libraries need to be included with your application in these two situations is that invoking an object’s properties and methods cross-process requires marshaling their arguments. In order to marshal the arguments, type library information must be available.

Standalone Setup for a Component

To distribute your component for use by other developers, by Internet providers, or as part of an Enterprise application, use Package and Deployment Wizard to create a standalone setup program. The Package and Deployment Wizard will automatically include necessary support files. Be sure to include your Help file.

Important For in-process components, see the related topic "Setting Base Addresses for In-Process Components," which contains important information regarding base addresses and their effect on the performance of your component.

Developers who use your component can install it on their computers, and then use the Package and Deployment Wizard or the Setup Toolkit to include it in the distribution media for their applications.

The steps required to produce Setup for your component will also give you the file dependency information you need to provide to developers who want to use your component with Microsoft Excel, Microsoft Visual C++, or other Automation-enabled development tools.

Distributing Type Libraries for Implemented Interfaces

If you’ve used the Implements keyword to add additional interfaces to your classes, as described in "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design," you need to include the type libraries for those interfaces in Setup for your component.

Special Considerations

Distribution issues particular to ActiveX controls — such as licensing — and to ActiveX documents can be found in "Building ActiveX Controls" and "Building ActiveX Documents."

If you plan to use your component for Internet development, the Package and Deployment Wizard can create CAB files for you. You can obtain the most up-to-date information on Internet setup options from the Microsoft Visual Basic Web site.

For More Information See "Distributing Your Applications," in the Visual Basic Programmer’s Guide.


Setting Base Addresses for In-Process Components

In 32-bit operating systems, the code pages for an in-process component (.dll or .ocx file) are shared between processes that use the component, as long as the component can load at its base address. Thus three clients could be using the controls in your component, but the code would be loaded into memory only once.

By contrast, if the memory locations used by an in-process component conflict with memory locations used by other in-process components or by the executable, the component must be rebased to another logical memory location in the executable’s process space.

Rebasing requires the operating system to dynamically recalculate the logical memory locations where code and data are loaded. This recalculation slows down the load process, and code that is dynamically relocated generally cannot be shared between executables.

You can greatly improve your component’s memory use by choosing a good base address.

Setting the Base Address

To enter the base address for your component, open the Project Properties dialog box and select the Compile tab. The address is entered in the DLL Base Address box, as an unsigned decimal or hexadecimal integer.

The default value is &H11000000 (285,212,672). If you neglect to change this value, your component will conflict with every other in-process component compiled using the default. Staying well away from this address is recommended.

Choosing a Base Address

Choose a base address between 16 megabytes (16,777,216 or &H1000000) and two gigabytes (2,147,483,648 or &H80000000).

The base address must be a multiple of 64K. The memory used by your component begins at the initial base address and is the size of the compiled file, rounded up to the next multiple of 64K.

Your program cannot extend above two gigabytes, so the maximum base address is actually two gigabytes minus the memory used by your component.

Note Executables will usually load at the 4 megabyte logical address. The region below 4 megabytes is reserved under Windows 95, and regions above two gigabytes are reserved by both Windows 95 and Windows NT.

Use a Good Random Number Generator

Because there is no way to know what base addresses might be chosen by other in-process components your users might employ, the best practice is to choose an address at random from the indicated range, and round it up to the next multiple of 64K.

If your company produces many in-process components, you may wish to randomly calculate the base address of the first, and then arrange the others above or below the first, thus guaranteeing at least that your company’s components will not have memory conflicts.


Version Compatibility

Version Compatibility in ActiveX Components

A component can be part of another application because it provides Automation interfaces that the other application can manipulate. Each public class module has a default interface that includes all the properties and methods you added to the class module, plus any secondary interfaces implemented using the Implements feature.

Once your component has been used in an application — or, in the case of ActiveX controls, embedded in a document or on a Web page — you can change its interfaces only at the risk of breaking the client application.

Suppose, for example, that the Spin method of your Widget object has one argument, Speed. If you distribute a new version of your component, in which you redefine the Spin method so that it also requires a Direction argument, you could cause run-time errors in existing applications.

At the same time, a successful component will inevitably spark requests for enhancements. You will want to provide new objects, or add new properties and methods to existing objects. Occasionally you will even want to change the arguments of existing methods of existing objects.

The following topics describe the Version Compatibility feature of Visual Basic, which is designed to allow components to be enhanced without causing existing applications to fail.

  • When Should I Use Version Compatibility? Describes the Version Compatibility options and when to use them. Describes an alternative technique for enhancing software components.

  • Maintaining Binary Compatibility Describes the versioning system Visual Basic uses to prevent compatibility problems.

  • Levels of Binary Version Compatibility Describes the degrees of binary compatibility Visual Basic measures.

  • Providing a Reference Point for Binary Version Compatibility Describes when and how to specify a version of your component as a reference point for version compatibility.

  • Using Binary Version Compatibility Describes when and how to use the feature, problems you may encounter, and messages you may get from Visual Basic.

For More Information See "Polymorphism, Interfaces, Type Libraries, and GUIDs" in "General Principles of Component Design" for background information and concepts.


When Should I Use Version Compatibility?

Visual Basic provides two mechanisms for maintaining backward compatibility while enhancing software components — the Version Compatibility feature and the Implements feature.

Version Compatibility

Visual Basic’s Version Compatibility feature is a way of enhancing your components while maintaining backward compatibility with programs that were compiled using earlier versions. The Version Compatibility box, located on the Component tab of the Project Properties dialog box, contains three options:

  • No Compatibility: Each time you compile the component, new type library information is generated, including new class IDs and new interface IDs. There is no relation between versions of a component, and programs compiled to use one version cannot use subsequent versions.

  • Project Compatibility: Each time you compile the component the type library identifier is kept, so that your test projects can maintain their references to the component project. All class IDs from the previous version are maintained; interface IDs are changed only for classes that are no longer binary-compatible with their earlier counterparts.

    Note This is a change in Project Compatibility from Visual Basic 5.0, where all class IDs and interface IDs in the project changed if any one class was no longer binary-compatible.

    Important For the purpose of releasing compatible versions of a component, Project Compatibility is the same as No Compatibility.

  • Binary Compatibility: When you compile the project, if any binary-incompatible changes are detected you will be presented with a warning dialog. If you choose to accept the warning, the component will retain the type library identifier and the class IDs. Interface IDs are changed only for classes that are no longer binary-compatible. This is the same behavior as Project Compatibility.

    If, however, you choose to ignore the warning, the component will also maintain the interface IDs. This option is only available when the compiler determines that the change was in the procedure ID or signature of a method.

    Caution You should only choose the Ignore button if you are absolutely sure that the changes you have made won't break compatibility. If you aren't absolutely sure, take the safe alternative and choose the Accept button to allow the interface ID's to be changed.

    Important The option to override the compiler's warning represents a change in behavior from Visual Basic 5.0. It is important that you fully understand the implications of incompatible changes before proceeding with this option.

    Note When people talk about Version Compatibility, they’re usually referring to Binary Compatibility.

The appropriate use of these options is described below.

Using the Implements Statement for Compatibility

The Implements statement allows you to add multiple interfaces to class modules, as described in "Polymorphism, Interfaces, Type Libraries, and GUIDs" and "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design," and in "Polymorphism" in "Programming with Objects," in the Visual Basic Programmer’s Guide.

Multiple interfaces allow your systems to evolve over time, without breaking existing components or requiring massive re-compiles, because a released interface is never changed. Instead, new functionality is added to a system by creating new interfaces.

This approach is much more in keeping with the design philosophy of the Component Object Model (COM), on which the ActiveX specification is based.

Note The Binary Compatibility option of Version Compatibility is useful in conjunction with Implements and multiple interfaces, to prevent changes to the default interfaces of your classes.

When to Use Version Compatibility Options

If you decide to use the Version Compatibility feature, you may find the following rules helpful in determining when to use the different options:

Use No Compatibility to Make a Clean Break

When you begin working on a new version of an existing component, you may decide that the only way to make necessary enhancements is to break backward compatibility. In this case, set No Compatibility the first time you compile your project. This guarantees that you’ll start with a clean slate of identifiers, and that existing programs won’t mistakenly try to use the incompatible version.

Before compiling an existing project with No Compatibility, you must also:

  • Change the file name of your component, so that the incompatible version won’t over-write earlier versions on your users’ hard disks.

  • Change the Project Name on the General tab of the Project Properties dialog box, so that the incompatible component will have a different type library name. This ensures that the objects the component provides will have unique programmatic IDs.

These items are discussed in more detail in "Levels of Binary Version Compatibility."

After compiling once with No Compatibility, switch to Project Compatibility to simplify your development tasks.

Use Project Compatibility for New Development

Use the Project Compatibility setting when you’re developing the first version of a component. Project Compatibility preserves the type library identifier, so that you’re not continually setting references from your test projects to your component projects.

Using Project Compatibility also makes it easier to switch between the component project and the compiled component when you’re testing.

Project Compatibility is discussed in "Project Compatibility: Avoiding MISSING References."

Use Binary Compatibility for New Versions of Existing Components

Switch to Binary Compatibility mode when you begin work on the second version of any component, if you want applications compiled using the earlier version to continue to work using the new version.

Switching to Binary Compatibility is discussed in the related topic "Providing a Reference Point for Compatibility."

Don’t Mix Binary Compatibility and Multiple Interfaces

If you use multiple interfaces and the Implements statement to provide backward compatibility, don’t use Binary Compatibility to modify the abstract interfaces you’ve defined for use with Implements.

If you enhance any of the interfaces in a component, Visual Basic will change their interface IDs. The technique of evolving component software by adding interfaces depends on interface invariance. That is, an interface once defined is never changed — including the interface ID.

For More Information See "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design" for information about component software design using multiple interfaces. "Maintaining Binary Compatibility" describes the versioning system Visual Basic uses to prevent compatibility problems.


Maintaining Binary Compatibility

Visual Basic maintains backward compatibility by preserving class ID and interface ID information from previous versions of your component, as described in "Polymorphism, Interfaces, Type Libraries, and GUIDs," in "General Principles of Component Design." This information is not maintained in the type library, but is stored elsewhere in your component.

When a client application is compiled using a particular version of your component, the class ID and interface ID of each object it uses will be compiled in. When the client is run, the class ID is used to create an instance of the class, and the interface ID is used to verify that it’s safe to make the property and method calls that were compiled into the client.

If you make a new version of your component, using the Binary Compatibility option, the new version will contain the class IDs and interface IDs the old client needs to create objects and use their properties and methods, as well as class IDs and interface IDs of the enhanced versions of the classes.

New client applications compiled using the new version of your component can make use of the enhancements, because they compile in the new class IDs and interface IDs.

For example, suppose that in version 1.0 of your component, your Widget object has a Spin method, and that you’ve compiled a client application that creates a Widget object and calls the Spin method.

Now suppose you set the Binary Compatibility option and compile a new version of your component, in which the Widget object also has an Oscillate method. Visual Basic creates a new interface ID for the enhanced Widget. Because adding a method doesn’t break binary compatibility, Visual Basic also maintains the class ID and interface ID of the old Widget.

When your previously compiled application uses these old IDs, it gets an enhanced Widget — which is fine, because the new Widget still has the Spin method the old application needs to call.

Note Binary Compatibility applies only to the default interface of a class — that is, the Public Sub, Function, and Property procedures you add to the class module. Interfaces you add using Implements are ignored.

Incompatible Interface Changes

Suppose that instead of adding an Oscillate method, you changed the arguments of the Spin method. For example, you might add a Direction argument.

If you could compile your component using the old class ID and interface ID for the Widget class, your old client application would be in trouble. It would be able to create the new Widget, but when it called the Spin method it would put the wrong arguments on the stack. At the very least, a program error would occur. Even worse, data could be corrupted.

Preventing Incompatibility

If you’ve selected the Binary Compatibility option, Visual Basic warns you when you’re about to compile an incompatible version of your component. You can reverse the edits that would make your component incompatible, or change the file name and Project Name so that the new version will not replace the old when users run Setup.

If you choose to disregard the warnings and compile an incompatible version of your component with the same file name and Project Name, Visual Basic may dump some of the interface IDs from previous versions of your component.

When the incompatible component is installed on a computer that has a client application compiled using an earlier version, it will overwrite the earlier version. Subsequently, when the client application attempts to create objects, it will receive error 430, "Class does not support Automation or does not support expected interface."

This averts more serious and subtle errors that might occur when the application attempts to invoke the properties and methods of the incompatible interface.

Limited Protection For Late-Bound Client Applications

Late binding is used when variables are declared As Object, because the compiler doesn’t know the class ID of the objects and interfaces that may be assigned to the variable at run time. Applications that use late binding create instances of your classes using the CreateObject function and the programmatic ID, as shown here:

Dim obj As Object
Set obj = CreateObject("MyComponent.MyObject")

The CreateObject function looks up the class ID in the Windows Registry, and uses it to create the object. Thus it will always create the most recent version of the object.

As long as you preserve binary compatibility, late-bound clients will continue to work successfully with your component.

If you make an incompatible version of your component using the same programmatic IDs for your objects, late-bound clients can still create the objects, because they’re looking up the class ID instead of having it compiled in. When they call methods whose arguments have changed, or methods you’ve deleted, program failure or data corruption may occur.

For More Information See "Levels of Binary Version Compatibility" for a description of the degrees of compatibility employed by Visual Basic. See "Version Compatibility" for a list of topics related to this feature. See "Polymorphism, Interfaces, Type Libraries, and GUIDs," in "General Principles of Component Design" for background information and concepts.


Levels of Binary Version Compatibility

Visual Basic defines three levels of version compatibility for the interfaces you describe in your class modules.

  • Version identical means that the interfaces are all the same, so the new version of the type library is exactly the same as the old one. The code inside methods or Property procedures may have been changed or enhanced, but this is transparent to client applications.

  • Version compatible means that objects and/or methods have been added to the type library, but no changes were made to existing properties or methods. Both old and new client applications can use the component.

  • Version incompatible means that at least one property or method that existed in the old type library has been changed or removed. Existing client applications that have references to the component cannot use the new version.

Version-Identical Interfaces

Once your component has been distributed as part of an application, there are several situations that might cause you to release an update. You might want to optimize the performance of a method that had turned out to be a bottleneck for users. You might also need to change the internal implementation of an object’s method to reflect changes in the business rule on which the method was based.

You can change the code in existing Property procedures or methods, and still have a version-identical interface, as long as you do not change the names or data types of their parameters, the order of the parameters, the name of the property or method, or the data type of the return value.

When you create the executable for a version-identical upgrade, you can use the same file name for the executable. Visual Basic uses the same version number for the type library.

Important When you release a new version of your component with a version-identical or version-compatible interface, and retain the same file name for the executable, you should always use the Make tab of the Project Properties dialog box to increment the file version number. This ensures that the setup programs for applications that use your component will replace old versions during setup.

Version-Compatible Interfaces

When you enhance your component by adding new classes, or new properties and methods to existing classes, you can continue to use the same name for your executable. As long as you make no changes to existing properties and methods, Visual Basic updates the version number of the type library but keeps it compatible with the old version number.

Client applications that are built using the new version of your component will compile with the new version number, and can make use of all the new features. They cannot be used with earlier versions of your component, however, because type library versions are only upward-compatible.

As with version-identical releases, remember to increment the file version number of the executable.

Version-Incompatible Interfaces

Sometimes design decisions made in an earlier version of a component fail to anticipate future needs. If you want the code in the component to be useful in new development projects, you have to change the interface.

For example, the CupsPerAnnum parameter of the Coffee method might be implemented as an Integer in the first version of a component. It may become apparent, after the component has been in use for some time, that some clients need to pass a larger value than can be contained in an Integer.

Changing the declaration of a method is only one of several actions that will cause Visual Basic to make the version number of the type library incompatible, rendering the new version unusable with client applications compiled with earlier versions. The following changes will cause a version incompatibility:

  • Changing the Project Name field on the General tab of the Project Properties dialog box.

  • Changing the Name property of any class module whose Public property is True (controls), or whose Instancing property is not Private (class modules).

  • Deleting a public class module, or setting its Instancing property to Private.

  • Deleting a public variable, procedure, or Property procedure from a public class module or control, or changing it to Private or Friend.

  • Changing the name or data type of a public variable, procedure, or Property procedure in a public class module or control.

  • Changing the names, data types, or order of the parameters of a public procedure or Property procedure in a public class module or control.

  • Changing the Procedure ID (DispID) or other parameters in the Procedure Attributes dialog box.

Time to Take Stock

When you’ve identified a necessary change that will cause your component to be incompatible with earlier versions, it’s a good idea to take the time to evaluate the entire set of interfaces, before plunging ahead and creating an incompatible version of your component.

Consider Multiple Interfaces

Remember that there are alternatives to using Version Compatibility. Consider enhancing your component by adding new interfaces with the Implements statement, as described in "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design."

Multiple interfaces, a key feature of the Component Object Model (COM) — on which the ActiveX specification is based — provide a more flexible way to enhance software components. They allow you to evolve your systems over time, without breaking existing components.

You don’t have to tackle the daunting task factoring your existing class module interfaces into small interfaces more suitable for use with Implements — one of the benefits of using multiple interfaces is that you can start small, adding new interfaces to the system only where new functionality is required.

Going Ahead with Incompatibility

If you decide to go ahead with an incompatible version, you can minimize future problems for the users of your component by concentrating in one release all the changes you can anticipate that might break compatibility again if they have to be made in later releases.

In planning for an incompatible change, treat the project as a fresh start. Devote as much care to planning as you would if you were creating a brand new component.

Creating an incompatible version requires three steps: changing the project name, changing the file name, and compiling with No Compatibility selected.

Changing the Project Name

The key change you must make, when you need to distribute an incompatible version of your component, is the project name. The project name, which is set on the General tab of the Project Properties dialog box, is the first part of the programmatic ID of each class your component provides.

For example, the SmallMechanicals component might provide a Widgets class. A client application would create a variable to contain a reference to a Widget object as follows:

Private wdgDriver As SmallMechanicals.Widget

The programmatic ID is the combination of project name and class name, and it must be unique. If you create a new version of this component, you might give it the project name SmallMechanicals200. Both versions of the Widget object could then be registered in the same Windows Registry without confusion.

Changing the File Name

You must change the file name of an incompatible component. If you use the old file name without incrementing the file version number, the incompatible component may not install on computers where the old file exists. If you increment the file version, the new file will over-write the old, and applications that used the old version will fail.

Compiling with No Compatibility

Before you compile the incompatible component for the first time, open the Project Properties dialog box (accessible from the Project menu), select the Component tab, then select No Compatibility in the Version Compatibility box.

Do not omit this step. Compiling with No Compatibility ensures that the new executable will not contain any GUIDs (for example, class IDs or interface IDs) that belong to the previous version. This is necessary for applications that use the original version to continue working correctly.

Tip After compiling once with No Compatibility, switch to Project Compatibility to simplify your development tasks.

Alternatives to Version-Incompatible Changes

If you prefer not to make the change to multiple interfaces, as described above, you can take a similar approach with classes.

That is, you can avoid changes that cause version incompatibility by adding new objects, properties, and methods, instead of changing existing ones. Existing applications continue using the old methods and objects, while developers of new applications can use the new objects.

For example, you might discover that to take advantage of enhancements to your General Ledger system, you need to add a SubAccount parameter to several business rules in your FinanceRules component.

If each rule is implemented as a method of the GL object, you could avoid creating an incompatible version of the component by adding a new object named GL97. This object would have the same methods as the GL object, but with a SubAccount parameter where appropriate.

If you need to add new versions of existing methods to an object, you can give the new methods the same name with a version or sequence number added — for example, ‘Execute2.’

This approach is not as flexible or efficient as implementing multiple interfaces. You may end up replicating entire classes, and class interfaces may become large and unwieldy — for example, you might find yourself using a Query class with an Execute method, an Execute2 method, an Execute3 method, and so on. However, it’s a step in the right direction.

For More Information "Providing a Reference Point for Binary Version Compatibility" describes when and how to specify a version of your component as a reference point for version compatibility. See "Version Compatibility" for a list of topics related to this feature. Software evolution using multiple interfaces is discussed in "Providing Polymorphism by Implementing Interfaces," in "General Principles of Component Design."


Providing a Reference Point for Binary Version Compatibility

To determine the degree of compatibility between two versions of a component, Visual Basic needs a reference point. You provide this reference point by entering the path to a previously compiled version of your component in the Version Compatibility box on the tab of the Project Properties dialog box.

You need to do this whenever you begin work on a new version of a component you have shipped, put into production, or used as part of an application.

To specify a reference version of the component type library

  1. Open the project.

  2. From the Project menu, choose Project Properties to open the Project Properties dialog box, and select the Component tab.

  3. Click Binary Compatibility to lock down the class IDs in the project.

    Note As explained in "Project Compatibility: Avoiding MISSING References," the Project Compatibility setting actually has nothing to do with the Version Compatibility feature.

  4. Update the box at the bottom of the Version Compatibility frame with the full path and name of the most recent version of your component.

Whenever you make a new executable from your component project, Visual Basic compares the new interfaces of your classes to the ones described in the file you have specified. Visual Basic updates the type library version number according to the level of compatibility between the interfaces.

For More Information "Using Binary Version Compatibility" describes when and how to use the feature, problems you may encounter, and messages you may get from Visual Basic. See "Version Compatibility" for a list of topics related to this feature.


Using Binary Version Compatibility

Whenever you begin work on a new version of an existing component, you need to specify a type library which Visual Basic can use as a reference point for determining compatibility. In most cases, this will be the type library included in executable (.exe, .dll, or .ocx file) for the last version of the component you distributed.

Each time you build an interim version of your updated component, Visual Basic extracts information about the old interfaces from this .exe file and compares it to the new interfaces of your class modules.

"Providing a Reference Point for Compatibility" explains the procedure for creating a reference point.

Keeping the Reference Version Separate from Interim Builds

Important Keep the copy of the .exe file you specify as your reference version separate from the build copy of the new version.

Each time you make an interim build, Visual Basic adds a new set of interface identifiers to the executable, one identifier for each class module. If you specify your build copy as the reference version, it will accumulate a complete set of interface identifiers for every version-compatible interim build you have ever done. (Interface identifiers do not change for version-identical builds.)

In addition to the sixteen bytes taken up by each interface identifier, having unused interface identifiers in your executable — only your test applications ever use the interim versions — can slow down cross-process access to your component in some situations, and the Windows Registry of any computer your component is installed on will be cluttered with unused interface identifiers.

If your reference version is a copy of your last released executable, all your interim builds will have the same interface version number, and your final build will have only the interface identifiers it needs: all the sets from the reference version (to provide backward compatibility) plus the set of interface identifiers for all the classes in your new release.

Note When you’re developing the first version of a component, using Project Compatibility instead of Binary Compatibility, exactly the opposite is true: The reference version should be your interim build. This does not bulk up the type library, because Project Compatibility never saves the interface identifiers.

Avoiding Version Trees

Because Visual Basic produces the version number for the new type library by incrementing the type library version number it finds in the reference version, the released versions of your component form a chain, each link derived from its predecessor. As mentioned earlier, each new release contains all the interface identifiers for preceding versions, so that a client application compiled with any previous version will still work with the latest.

What’s a Version Tree?

Version trees arise when your component’s version history acquires branches — that is, when you produce two physically distinct components based on the same source code. It’s important to keep the version history of your component straight, and avoid such branching.

Figure 7.4 shows some of the problems that can be caused by version trees. (The version numbers are for illustrative purposes only, and are not intended to represent actual type library version numbers.)

Figure 7.4 Problems with version trees

The long branch at the right shows four successive versions of a component executable, and a new executable that has been created by adding to the source code for the executable whose type library version is 1.3.

The correct continuation of this version history is for the latest executable to be compiled with the version 1.3 executable as its reference version. The new type library version number is 1.4. The .exe file maintains compatibility with client applications compiled using any of its predecessors.

Because it’s at the end of a chain of compatible versions, the new executable could also be compiled with the version 1.0 executable as its reference version. In this case, its type library version number will be 1.1. This could cause problems for clients compiled to take advantage of features of the new version. If they’re placed on a computer with the earlier version 1.1 executable, the new features will not be available, and the applications will fail.

A different problem arises when the new component is installed on computers that have client applications compiled with type library version numbers 1.1, 1.2, and 1.3. Standard practice is to increase the file version number of each new executable file, to ensure that Setup will replace earlier versions of the executable. (Remember that file version numbers are independent of type library version numbers.)

Thus the new executable, containing interface identifiers for type library versions 1.0 and 1.1, will replace older executables that contained interface identifiers for type library versions 1.0 through 1.3.

If the computer already has client applications compiled with type library versions 1.2 and 1.3, those clients will be unable to use the new version of the component.

Divergent Versions

The left side of the tree shows divergent versions. This can arise when the source code for an early version of your component is taken as the basis of a new component, and classes are added that do not exist in your main version history.

If the executable for your component is used as the reference version for the divergent version, the type library version numbers of the divergent version and its successors will overlap the version numbers of your components. The results for client applications will be disastrous.

Tip You can easily avoid the creation of version trees by always setting aside a copy of the previous version of your component’s executable file (.exe, .dll, or .ocx) as the reference version for the next release, as described earlier in this topic.

Tip If you decide to use the source code of an earlier version of your component as the basis of a new component, give the new component a different project name and executable name.

Version Trees with Project Compatibility

Version trees can also arise when you’re using Project Compatibility, the difference being that it’s the major version number of the type library that changes (instead of the minor version number, as shown in Figure 7.4). The consequences to client applications can be equally disastrous.

As with Binary Compatibility, the best way to avoid this is not to split your source tree. If you take a copy of your source code at a particular stage of the project as the basis for another component, use a different project name and executable name for this new project.

Version Compatibility Messages

For performance reasons, Visual Basic does not fully compare interfaces as you edit. When you run your component project, Visual Basic will always display a compatibility warning if the new version is incompatible with the old. (Version-identical and version-compatible interfaces will compile without compatibility warnings.)

Note Version compatibility is judged on a project-wide basis. A change in the declaration of just one method in one class module causes the entire project to be marked as incompatible with the previous version. See "Levels of Version Compatibility," earlier in this chapter, for a listing of changes that will cause a version incompatibility.

Version Incompatibility Warnings

Suppose you add a new argument to the Spin method of the Widget object. When you run the project, you’ll get a warning that binary compatibility has been broken. You can examine the old declaration by clicking the Declaration button on this message. If you made the change accidentally, you can click Edit to bring up the code and fix it.

If you choose to accept the warning, the component will retain the type library identifier and the class IDs. Interface IDs are changed only for classes that are no longer binary compatible. This is the same behavior as Project Compatibility.

If, however, you choose to ignore the warning, the component will also maintain the interface IDs. This option is only available when the compiler determines that there was a change in the signature of a method.

Caution You should only choose to ignore the warning if you are absolutely sure that the changes you have made won't break compatibility. If you aren't absolutely sure, take the safe alternative.

Important The option to override the compiler's warning represents a change in behavior from Visual Basic 5.0. It is important that you fully understand the implications of incompatible changes before proceeding with this option.

Incompatible EXE File

If you have not changed the project name, when you use the Make EXE File command, you will get a warning that your application is incompatible with the .exe file you specified as your reference version, as shown in Figure 7.5.

Figure 7.5 Warning for incompatible .exe file

Clicking Continue at this point creates an executable file that could cause existing client applications to fail. In order for existing clients to continue working, you must take the following steps when you create an incompatible version of a component:

  • Change the file name.

  • Change the project name.

  • Compile with Version Compatibility set to No Compatibility.

These steps are discussed in detail in "Levels of Binary Version Compatibility."

For More Information See "Version Compatibility" for a list of topics related to this feature.


Localizing ActiveX Components

You can localize components created using Visual Basic by first collecting all the text strings used in your component into a source file for the Windows Resource Compiler (.rc file). You can then create localized versions of this source file by translating the strings to each of your target locales.

Before you compile each localized version of your component, use the Resource Compiler to create a Windows resource (.res file) from the appropriate source file, and include the resource file in your project.

Components without a visible interface are extremely easy to localize in this fashion, because you only have to localize error messages. The code fragments in "Generating and Handling Errors" demonstrate this technique.

For components that display forms, the process is more complicated. You need to place the captions of forms and controls into the resource file, and retrieve them in the Form_Load event procedure for each form.

Satellite DLLs

In areas where multiple languages are in use, using resource files can be a problem. Not only do you have to compile separate versions of your component for each language, but they all share the same Project Name, and thus only one language can be installed on a user’s computer at any given time.

Localizing Controls" in "Building ActiveX Controls," describes a technique for using a satellite DLL to provide the translated strings for each language. Satellite DLLs accompany your component, and can be swapped in and out without recompiling the component.

If you use a LocaleID-based naming convention, multiple satellite DLLs can coexist on one computer, allowing the user to switch from one language to another by changing the system LocaleID.

Limitations

Unfortunately, code components can only make limited use of this feature. Unlike control components, they don’t receive a LocaleID from the client application. The best a code component can do is to use API calls to obtain the system LocaleID.

Out-of-process code components can never do any better than this, because they have to serve multiple clients. With in-process components, however, you can offer developers a better option by providing an object with a LocaleID property that can be set by client applications. When the property is set, your component can load text strings from the appropriate satellite DLL.

For More Information For information on writing code for applications that will be distributed world-wide, see "International Issues," in the Visual Basic Programmer’s Guide. To learn about the functions for retrieving text strings and other resources, see the LoadResString function in the Language Reference.


Building Code Components

Code components, formerly called OLE servers, are libraries of objects that provide an easy way to package your code for reuse. For example, you can create libraries of procedures that can be used with Microsoft Visual Basic or with desktop applications that host Visual Basic for Applications, such as Microsoft Office.

With the Enterprise Edition of Visual Basic, code components can become parts of enterprise systems, running unattended on remote computers to provide data services and business rules. The information in "Scalability and Multithreading" and "Asynchronous Call-Backs and Events" will be of particular interest in this context.

Code components can be wizards and add-ins for Visual Basic. Extending the Visual Basic Environment with Add-Ins, in the Component Tools Guide, describes how to create wizards and add-ins. Many of the topics in this chapter provide useful background information for creating add-ins.

"Building Code Components" assumes familiarity with the material in "General Principles of Component Design" and "Debugging, Testing, and Deploying Components," as well as "Programming with Objects," in the Visual Basic Programmer’s Guide.

The following topics describe key features of code components created with Visual Basic.

Topics

Global Objects and Code Libraries

Create libraries of procedures you can call without explicitly creating objects — as if the procedures were part of Visual Basic.
Scalability and Multithreading

When a component has to handle multiple requests, Visual Basic provides two ways to prevent long tasks from blocking short ones: multithreading and SingleUse classes.
Asynchronous Call-Backs and Events

Defines asynchronous processing, and discusses two ways to notify clients that asynchronous tasks are complete: by raising events, or by implementing interfaces that include call-back methods.
Displaying Forms From Code Components

Displaying modal and modeless forms from in-process and out-of-process components.
How Object Creation Works in Visual Basic Components

In certain circumstances — for example, with SingleUse objects — the way you create the object can affect its behavior.
How Binding Affects ActiveX Component Performance

The performance of your component is affected by the way clients declare the variables they use to keep references to your objects.
How Marshaling Affects ActiveX Component Performance

How you declare method arguments in out-of-process components can have a big effect on performance.

Sample application

Coffee2.vbp, CoffWat2.vbp, MTCoffee.vbp

Coffee2.vbp and CoffWat2.vbp demonstrate two techniques for asynchronous notifications — events and call-back methods on a secondary interface — and provide an example of all-code timers you can use instead of the Timer control. MTCoffee.vbp illustrates some features of multithreading. The sample applications are listed in the Samples directory.

For More Information See "Creating an ActiveX EXE Component," for step-by-step procedures that illustrate multithreading and asynchronous processing. Similar step-by-step procedures in "Creating an ActiveX DLL," illustrate global objects and modeless forms. Visual Basic code components can also provide ActiveX documents, as discussed in "Building ActiveX Documents."


Global Objects and Code Libraries

You can build libraries of general-purpose procedures in Visual Basic by making the procedures methods of a class module. Set the Instancing property of the class module to any value except Private or PublicNotCreatable, so that clients can create instances of the class.

When you choose the value GlobalMultiUse for the Instancing property of a class, and then make the project, you can subsequently use the properties and methods of the class without having to explicitly create an instance of the class.

Properties and methods of a GlobalMultiUse object (or global object) are added to the global name space of any project that uses the object. That is, in another project you can set a reference to the component, and the names of the global object’s properties and methods will be recognized globally, just as if they were part of Visual Basic.

Important The properties and methods of a global object only become part of the global name space when the component is referenced from other projects. Within the project where you created the GlobalMultiUse class module, objects created from the class are just ordinary objects.

The following code fragments show the difference between using MultiUse and GlobalMultiUse objects. Both assume that you’ve used the References dialog box to set a reference to a component that provides a Financials object whose methods are general-purpose financial functions.

Code required to use a MultiUse Financials object:

' This code goes in a standard module.
' Declare a global variable to contain the instance.
Public gfins As New Financials

' This code goes in a form that uses a method of the
'   Financials object.
Private Sub cmdCalculateResult_Click()
   txtResult.Text = gfins.LeastReasonableReturn( _
      CCur(txtBeginningBalance.Text))
End Sub

Note When you use the code above, the Financials object is created the first time the variable gfins is used in code. This incurs a slight increase in overhead on each function call. You can avoid this by explicitly creating the Financials object in Sub Main (for standalone executables) or on first object creation (for components).

By contrast, the code to use a GlobalMultiUse Financials object requires no global variable:

Private Sub cmdCalculateResult_Click()
   txtResult.Text = LeastReasonableReturn( _
      CCur(txtBeginningBalance.Text))
End Sub

Visual Basic creates a hidden instance of the Financials class the first time a line of code containing one of its methods is executed.

Note As explained later in this topic, the hidden instance incurs the same slight overhead per call as declaring gfins As New in the previous code example.

In the Object Browser, procedures that are part of a project’s global name space are displayed in the <globals> entry in the Classes list. Figure 8.1 shows this for a project group that uses the Financials class.

Figure 8.1 Methods of a GlobalMultiUse class appear in <globals>

The Object Browser displays the names of classes and members defined in your projects in bold type.

Tip You can move directly to the code that defines classes and members by double-clicking names displayed in bold type.

Components That Use Global Objects Internally

Setting the Instancing property of a class to GlobalMultiUse or GlobalSingleUse allows client programs to use the properties and methods of the class as if they were global functions, but within the project where you defined the GlobalMultiUse class module, objects created from the class are not global.

For example, suppose your MyUtilities project contains a Utilities class whose Instancing property is set to GlobalMultiUse, and that the class has an InvertMatrix method. Any client project that has a reference to MyUtilities can call InvertMatix as if it were a global function. However, within the MyUtilities project, you cannot use InvertMatrix as if it were a global function.

If you want to use a global instance of the Utilities class within the MyUtilities project, you can add the following declaration to a standard module in MyUtilities:

Public Utilities As New Utilities

Thereafter, whenever you need to use InvertMatrix (or any other procedure supplied by the Utilities class), you can qualify it with the class name:

   Utilities.InvertMatrix aintMyLargeMatrix

The first time you use a method of the Utilities class in this fashion, an instance of the class is created automatically, because the global variable is declared As New. Using the class name as the name of the variable makes it clear which of the modules within your component is supplying the procedure.

Note You must declare the global variable in a standard module, not a class module, in order for this technique to work.

Creating Object Properties for Global Objects

Visual Basic has properties of its <globals> that are themselves objects — for example, the App object and the Printers collection. You can create similar object properties for your own global classes.

Suppose you’ve created a Registry class for accessing the Windows registry, and that you have a Globals class whose Instancing property is GlobalMultiUse. The following code fragment shows how you can add a Registry property to your Globals class:

Private mRegistry As Registry

Private Sub Class_Initialize()
   Set mRegistry = New Registry
End Sub

Public Property Get Registry() As Registry
   Set Registry = mRegistry
End Sub

Once you’ve compiled your component, you can reference it from other projects. If the Registry class includes a FindKeyContaining method, you might write code like this to use it:

Private Sub Command1_Click()
   lblResult.Caption = _
      Registry.FindKeyContaining(txtInput.Text)
End Sub

This code assumes that the form contains a text box in which the user enters a string to be located (txtInput), and a label in which the registry key is displayed (lblResult). Notice that you didn’t have to create an instance of Globals before using the Registry object.

Tip Always use Property Get when you add an object property to your global object. If you use a public variable instead, you can accidentally destroy the object the property provides by setting the property to Nothing.

Guidelines for Using Global Objects

You may find the following useful in getting the most out of global objects:

  • Constants you provide using Enums are Long integers; you can use global objects to provide string and floating point constants, as described in "Providing Named Constants for Your Component," in "General Principles of Component Design."

  • When you encounter name conflicts, use the type library name of the component to resolve the conflict.

    For example, suppose you were using both MyUtilities and YourUtilities, and that YourUtilities had a GlobalMultiUse class named General that also included a method named LeastReasonableReturn. You would code the following to ensure that the method in YourUtilities was used:

    Private Sub cmdCalculateResult_Click()
txtResult.Text = _
YourUtilities.LeastReasonableReturn( _
CCur(txtBeginningBalance.Text))
End Sub

    Important If conflicting names are not qualified, Visual Basic uses the library that appears highest in the References dialog box. Unless the procedures have different arguments, no compile-time errors occur.

  • Choose procedure names carefully. While it may seem convenient to use common words or phrases, this increases the likelihood that other libraries will contain functions with the same name. Qualifying procedures with the type library name may be even less convenient than explicitly declaring a global object variable.

    Important Global name space pollution is a serious issue. For an example and discussion, see "Creating a Reference to an Object," in "Programming with Components," in the Visual Basic Programmer’s Guide.

  • Implicit creation isn’t the only way to use global objects. You can declare variables of a GlobalMultiUse class, and explicitly create instances. If you have two libraries with a lot of name conflicts, short global variable names may be easier to type than long, user-friendly type library names.

Important Each client that uses the properties and methods of a GlobalMultiUse class gets its own instance of the class. In other words, the "Global" in GlobalMultiUse does not mean "one global instance that all clients share."

Limitations of GlobalMultiUse Objects

When you’re deciding whether to use a global object or explicitly declare a variable to hold an instance of an object, the following may be of use:

  • You can’t set the hidden reference to a global object to Nothing when you’re done using it. Visual Basic does not provide any way to access the hidden global object directly.

  • GlobalMultiUse is a convenience for programmers; you incur the same slight overhead on each procedure call as you would if you used a global variable declared As New (for example, As New Financials).

  • As explained earlier in this topic, when you are authoring a component that provides a GlobalMultiUse class, you cannot use the properties and methods of the class if they were global procedures.

  • You can’t use global objects to replace global procedures in the VB and VBA libraries. These libraries always appear at the top of the References dialog box, before any other libraries. Procedures with the same name in a global object can only be accessed by qualifying the procedure name with the type library name.

GlobalSingleUse Objects

For out-of-process components only, you can set the Instancing property of a class module to GlobalSingleUse. If you do this, a separate instance of your component will be loaded into memory for each client. This requires a lot more memory than providing GlobalMultiUse objects.

If you simply want each client to have a separate thread of execution for your global procedures, consider using GlobalMultiUse for your global objects and making your out-of-process component multithreaded, as described in "Scalability and Multithreading."

Note GlobalSingleUse is not allowed in ActiveX DLL projects, because multiple instances of a DLL cannot be loaded into a clients process space.

For More Information See "Scalability Through Multiple Processes: SingleUse Objects."


Scalability and Multithreading

When a component has one thread of execution, code for only one object can execute at any given time. The Automation feature of the Component Object Model (COM) deals with this situation by serializing requests. That is, the requests are queued and processed one at a time until all have been completed.

In a multithreading operating environment, serialization protects single-threaded objects from overlapping client requests — that is, from code in a property or method being executed while one or more previous client requests are still being executed. Overlapping requests can cause internal data errors if objects aren't designed for reentrancy, as discussed in "Apartment-Model Threading in Visual Basic."

Serialization is thus an extremely important feature of Automation. However, serialization of single-threaded components means that requests are sometimes blocked. For example, suppose you're using a Widget object that has two methods:

  • The Spin method takes anywhere from several seconds to half an hour.

  • The Flip method is almost instantaneous.

Because 32-bit applications are preemptively multitasked, a second application could call the Flip method while the Spin method is already running. As shown in Figure 8.2, the short Flip method is blocked until the long Spin method is complete.

Figure 8.2 Blocking in a component with MultiUse objects

When short operations are blocked by long ones, productivity suffers and user frustration rises. Components that behave in this fashion are said to scale poorly. That is, they work poorly if many requests of mixed length are made.

Visual Basic has two component features for avoiding blocked calls — multithreading and SingleUse objects. These features are described in the following topics:

  • Apartment-Model Threading in Visual Basic To use threads effectively, it’s important to understand Visual Basic’s use of ActiveX apartment-model threading and its implications for global data.

  • Designing Thread-Safe DLLs Making in-process components safe for use with multithreaded clients.

  • Designing Multithreaded Out-of-Process Components Explains the three models available for assigning objects to threads.

  • Using Multiple Threads of Execution Introducing multithreading on a single-processor machine is not necessarily a blessing.

  • Event Logging for Multithreaded Components Your component can log suppressed message box and error dialog text to the system event log or to a log file.

  • Debugging Limitations for Multithreaded Components Describes the limitations of the development environment.

  • Scalability Through Multiple Processes: SingleUse Objects Visual Basic provides a second means of ensuring one thread per object, for cases where your component must show forms.


Apartment-Model Threading in Visual Basic

In Visual Basic, apartment-model threading is used to provide thread safety. In apartment-model threading, each thread is like an apartment — all objects created on the thread live in this apartment, and are unaware of objects in other apartments.

Visual Basic’s implementation of apartment-model threading eliminates conflicts in accessing global data from multiple threads by giving each apartment its own copy of global data, as shown in Figure 8.3.

Figure 8.3 Each thread has its own copy of global data

This means that you cannot use global data to communicate between objects on different threads.

Note Objects in different apartments can only communicate with each other if a client passes them references to each other. In this case, cross-thread marshaling is used to provide synchronization. Cross-thread marshaling is almost as slow as cross-process marshaling.

Note In addition to maintaining a separate copy of global data, a Sub Main procedure is executed for each new apartment (that is, for each new thread). Otherwise, there would be no way to initialize global data for the thread. This is discussed further in "Designing Thread-Safe DLLs" and "Designing Multithreaded Out-of-Process Components."

Single-Threaded Components and the Apartment Model

All components created with Visual Basic use the apartment model, whether they’re single-threaded or multithreaded. A single-threaded component has only one apartment, which contains all the objects the component provides.

This means that a single-threaded DLL created with Visual Basic is safe to use with a multithreaded client. However, there’s a performance trade-off for this safety. Calls from all client threads except one are marshaled, just as if they were out-of-process calls. This is discussed in "Designing Thread-Safe DLLs."

Ownership of Threads

A multithreaded in-process component has no threads of its own. The threads that define each apartment belong to the client, as explained in "Designing Thread-Safe DLLs."

By contrast, a multithreaded out-of-process component may have a thread pool with a fixed number of threads, or a separate thread for each externally created object. This is discussed in "Designing Multithreaded Out-of-Process Components."

Obtaining the Win32 Thread ID

In addition to maintaining a separate copy of your global data for each thread, Visual Basic maintains separate copies of the data supplied by global objects such as the App object. Thus the ThreadID property of the App object will always return the Win32 thread ID of the thread on which the property call was handled.

Where You Can Use Threading

Unlike earlier versions of Visual Basic, projects authored with Visual Basic 6 can take advantage of apartment-model threading without having to suppress visual elements such as forms and controls. Forms, UserControls, UserDocuments, and ActiveX designers are all thread-safe. You can select a setting for the Threading Model option without marking your project for Unattended Execution.

To set the threading model for an ActiveX DLL, ActiveX Exe, or ActiveX Control project

  1. On the Project menu, select <project> Properties to open the Project Properties dialog box.

  2. On the General tab, select the desired options in the Threading Model box.
    • For ActiveX DLL and ActiveX Control projects, you can select either Apartment Threaded or Single Threaded.

    • For ActiveX Exe projects, you can either specify that each new object is created on a new thread (Thread per Object), or limit your component to a fixed pool of threads. A thread pool size of one makes the project single-threaded; a larger thread pool makes the project apartment-threaded.

Note When you change the threading model for an existing project, an error will occur if the project uses single-threaded ActiveX controls. Visual Basic prevents the use of single-threaded controls in apartment-threaded projects, as described in "Converting Existing Projects to Apartment-Model Threading" below.

Note When you specify Thread per Object (or a thread pool greater than one) for an ActiveX Exe project, only externally created objects are created on new threads. (See "Designing Multithreaded Out-of-Process Components.") Thread per Object and Thread Pool are not available for ActiveX DLL and ActiveX Control projects, because thread creation is controlled by the client application.

Important For Professional and Enterprise Edition users, the consequences of selecting Thread per Object or Thread Pool are discussed in detail in "Designing Multithreaded Out-of-Process Components."

Setting Unattended Execution

Unattended Execution allows you to create components that can run without operator intervention on network servers. Selecting Unattended Execution doesn't affect the threading model of your component.

To mark your ActiveX DLL or EXE project for unattended execution

  1. On the Project menu, click <project> Properties to open the Project Properties dialog box.

  2. On the General tab, check Unattended Execution, then click OK.

Important Selecting the Unattended Execution option suppresses all forms of user interaction — including message boxes and system error dialogs. This is discussed in "Event Logging for Multithreaded Components."

Limitations of Apartment-Model Threading

The following limitations apply to apartment-model threading in Visual Basic.

  • The development environment doesn't support multithreaded debugging. If you have Microsoft Visual Studio, you can debug an apartment-threaded component with the Visual Studio debugger after compiling it to native code with debug information. You will also need a multithreaded client application to test and debug in-process components (.dll and .ocx files).

  • MDI parent and child forms must share data in ways that are difficult to make thread-safe. MDI forms, therefore, are not allowed in apartment-threaded projects. On the Project menu, Add MDIForm is unavailable in apartment-threaded ActiveX DLL projects, and in ActiveX Exe projects with Thread per Object selected (or with a thread pool larger than one). If you change the threading model to Apartment Threaded in a project that contains MDI forms, an error occurs.

  • In addition to performing poorly, single-threaded ActiveX controls can cause numerous problems in multithreaded clients. Visual Basic therefore prevents the use of single-threaded controls in projects where Threading Model has been set to Apartment Threaded. For details, see "Converting Existing Projects to Apartment-Model Threading" below.

  • Friend properties and methods can only be called by objects on the same thread. Because they are not part of an object's public interface, they cannot be marshaled between threads.

  • ActiveX Documents in ActiveX Exe projects will not be apartment-model thread-safe unless you select Thread per Object or Thread Pool with a pool size greater than one.

  • When a thread shows a form with vbModal, the form will be modal only to code and forms on that thread. Code running in other threads will not be blocked, and forms shown by other threads will remain active.

  • Drag and drop between forms and controls will work only if the drag source and the drag target are on the same thread. (OLE drag and drop, however, will work universally.)

  • DDE between forms will only work if the forms are on the same thread.

Converting Existing Projects to Apartment-Model Threading

You can add apartment threading to your existing projects by changing the Threading Model option, as described in Selecting a Threading Model for Your Project, and recompiling the project. For many projects, this is all you need to do.

If an existing ActiveX DLL, ActiveX EXE, or ActiveX Control project uses single-threaded constituent controls, attempting to change Threading Model to Apartment Threaded will cause an error. Because of the number and severity of problems that single-threaded ActiveX controls cause for multithreaded clients, Visual Basic does not permit them to be used in ActiveX component projects.

If you have an existing project that employs a single-threaded control, contact the vendor to see whether an apartment-threaded version is available.

Forcing the Use of Single-Threaded Controls

It is possible to trick Visual Basic into using a single-threaded control in an apartment-threaded project, by manually editing the .vbp file. Do not do this. The problems that single-threaded ActiveX controls can cause include:

  • If a user tabs to a single-threaded control on a form that's running on a different thread, she will not be able to tab off the control. This occurs because the single-threaded control's thread has no context for the focus on the form's thread. The only way for the user to change the focus in this situation is to use the mouse. Using a single-threaded control as a constituent of an apartment-threaded control causes similar problems.

  • In a multithreaded application, activating a form by clicking on a single-threaded control will fail if the form is on a different thread. This also occurs when clicking on a single-threaded constituent of an apartment-threaded control.

  • Using a single-threaded OCX in a multithreaded application causes performance problems, because all of the controls the OCX provides must run on the same thread. This means that for controls on forms running in different threads, all communication requires expensive cross-thread calls. An apartment-threaded ActiveX control that uses single-threaded constituent controls will cause similar performance problems.

  • In a multithreaded application, the tab key and access keys (for example, Alt+A) will not work for single-threaded controls that are not on the application's main thread. An apartment-threaded ActiveX control that uses single-threaded constituent controls will experience similar problems.

  • If your apartment-threaded control sets the Picture property of a single-threaded constituent control (or any other property that takes a Picture object), errors will occur in multithreading clients. This is because the Picture object cannot be marshaled between threads.

Important Single-threaded controls can cause these and other problems in any multithreaded component or application you build, using Visual Basic or any other development tool.

Reentrancy

In the apartment model, reentrancy refers to the following sequence of events:

  1. An apartment’s thread of execution enters an object’s code, because a property or method has been invoked.

  2. While the thread is in the property or method, another thread invokes a property or method of the object, and Automation serializes this request — that is, it queues the request until the thread that owns the object’s apartment finishes the member it’s currently executing.

  3. Before the thread reaches the end of the member, it executes code that yields control of the processor.

  4. Automation tells the thread to begin executing the serialized request, so that the thread reenters the object’s code.

The new request may be for the member the thread was already executing — in which case the thread enters the member a second time — or it may be for another member. If the second member doesn’t yield, it will finish processing before the first member. If it changes module-level data the first member was using, the result may be unfortunate.

By serializing property and method calls for each apartment, Automation protects you from reentrancy — unless your code yields control of the processor. Ways in which your code can yield control of the processor include:

  • Calling DoEvents.

  • Invoking the properties or methods of an object on another thread, or in another process.

  • Raising an event that’s handled by an object on another thread, or in another process.

  • Invoking a cross-thread or cross-process method from within a method.

  • Showing a form.

Unless you’ve carefully written all of an object’s code so that it doesn’t matter whether two members are executing at the same time, you should not include code that yields control of the processor.

For More Information Apartment-model threading affects in-process and out-of-process components differently, as described in "Designing Thread-Safe DLLs" and "Designing Multithreaded Out-of-Process Components."


Designing Thread-Safe DLLs

Visual Basic greatly simplifies the job of authoring in-process components that can be used safely and efficiently by multithreaded clients. All in-process components created with Visual Basic use apartment-model threading, to provide synchronous access to objects.

Figure 8.4 shows how a multithreaded client uses a single-threaded in-process component.

Figure 8.4 Multithreaded client using single-threaded DLL

Note For those who have done COM programming in C++, the client thread that handles all the calls to objects in the single-threaded in-process component will be the first thread that called OleInitialize().

Using a single-threaded DLL in this way is safe, but slow. Cross-thread marshaling is almost as slow as cross-process marshaling. You can improve the performance of an in-process component when it’s used with multithreaded clients by making the component multithreaded.

To make an ActiveX DLL project multithreaded, select the desired threading options on the General tab of the Project Properties dialog box.

Note In-process components can only use the threads on which their clients create objects and make method calls. You cannot create additional threads within an in-process component, so the other threading options are unavailable for DLL projects.

Important Selecting the Unattended Execution option suppresses all forms of user interaction — including message boxes and system error dialogs. This is discussed in "Event Logging for Multithreaded Components."

Marking an ActiveX DLL project for apartment-model threading provides the following benefits:

  • All of the objects a client creates on a given thread will be created in the same apartment (thread) in the DLL. Calls to these objects do not require cross-thread marshaling, making them more efficient.

  • Because an object is only accessed on the thread where it was created, calls are synchronized (serialized) so that a call is never interrupted by a call from another thread.

  • Arguments for cross-thread calls are marshaled, and the calling thread is blocked. This synchronization of data protects the calling thread’s state.

This points are illustrated in Figure 8.5.

Figure 8.5 Multithreaded client using multithreaded DLL

If Thread 2 is already making a call to one of the objects in its apartment in the DLL, a call from Thread 3 to one of Thread 2’s objects will be serialized. When Thread 2 is free, it will process the call, using the arguments marshaled from Thread 3. Thread 3 is blocked while the request is serialized.

When the call requested by Thread 3 is complete, the return value and any ByRef arguments will be marshaled back to Thread 3. This synchronization ensures that Thread 3’s data remains consistent.

Important In an apartment-model DLL, a new apartment is created for each client thread that requests objects that the DLL provides. The first time a client thread requests such an object, Sub Main executes for the new apartment. A DLL cannot create threads of its own.

For More Information In coding the properties and methods of your objects, it’s very important to observe reentrancy rules, as discussed in "Apartment-Model Threading in Visual Basic."

Forms and Controls in Thread-Safe DLLs

When you create an instance of a form at run time, Visual Basic follows the same rules it uses for other private objects. That is, the new form is created on the thread where the New operator is executed. If you create a form implicitly (for example, by accessing a form property using a variable that was declared As New Form1), the form is created on the thread where the code was executed.

Apartment-threaded DLLs cannot create their own threads; the first time a client thread requests an object provided by your DLL, a new apartment is created, and Sub Main executes for that apartment. All public objects requested by that client will reside in the same apartment, and will share global data. Any private objects (including forms) that are created by the public objects will also reside in the apartment.

Visual Basic does not provide any way for apartments to become aware of each other. However, a multithreaded client could obtain a reference to an object on Thread A, and pass that reference to an object on Thread B. If your DLL allows this, you should read the information on reentrancy in "Apartment Model Threading in Visual Basic."

Important Modal forms shown by a thread do not block execution of code on other threads, and are not modal to forms on other threads. Showing a form yields control, just as calling DoEvents would, and therefore may cause the code in an object to be reentered.

Unlike forms, ActiveX controls are public objects that can be created and used by client forms. An instance of an ActiveX control always resides on the same thread as the client form that uses it.

Note A special case of implicit form creation is the predeclared ID (for example, Form1), a hidden global form variable that Visual Basic maintains for every form class. Because Visual Basic duplicates global data for each apartment, there is a separate Form1 for every thread. To avoid confusion, you may find it helpful to avoid using predeclared IDs in apartment-threaded components, by explicitly declaring your own form variables.

Note Duplicate global data means that a variable you declare Public in a standard module is global only to the thread; each thread has an instance of the variable. It also means that the properties of the App object, such as App.ThreadID, have separate instances for each thread.


Designing Multithreaded Out-of-Process Components

""You can create a multithreaded out-of-process component that utilizes apartment-model threading. Objects the component provides can run on different threads of execution.

Visual Basic provides three models for assigning objects to threads in out-of-process components. You can select one of these models using the Unattended Execution box, on the General tab of the Project Properties dialog box.

Thread assignment model | Unattended Execution settings
One thread of execution | Select the Thread Pool option with one thread.
Thread pool with round-robin thread assignment | Select the Thread Pool option and specify the number of threads to allow.
Every externally created object is on its own thread | Select the Thread per Object option.

Note The model you select cannot be changed at run time.

Like other private objects, forms reside on the thread where they were created. Private objects cannot be created with the CreateObject function, so they cannot be used to start new threads.

Important In a multithreaded executable, Sub Main executes independently for each new thread. Visual Basic provides no way to determine which thread was created first.

One Thread of Execution

This is the default setting, when you select Unattended Execution. This option allows you to compile components authored with earlier versions of Visual Basic, without having to modify them to take threading into account.

By recompiling a component with the Unattended Execution option, you remove any possibility that it will show message boxes that require operator intervention. Instead, such messages can be logged to the Windows NT event log (or to a file of your choice), as described in "Event Logging for Multithreaded Components."

Round-Robin Thread Pool

As clients request objects, Visual Basic creates each object on the next thread in the pool, starting over with the first thread when it reaches the end of the pool.

For example, Figure 8.6 shows a component with three externally creatable classes (that is, classes whose Instancing property is set to any value except Private or PublicNotCreatable). The classes are named A, B, and C. To keep track of which object was requested by which client, the circle that represents each object contains the class name and the number of the client that created the instance.

Figure 8.6 Round-robin thread pool with five threads and four clients

Four clients have created instances of these classes. Client 1 requested the first object, of class A, which was created on thread T1. Client 2 then requested an object of class A, which was created on thread T2, and so on. Some clients have created multiple instances of the same class — for example client 1, which is using a total of four instances of class A.

Figure 8.6 illustrates an important point about the round-robin threading model: You cannot predict which objects will be on the same thread — and therefore sharing global data. Objects used by different clients may share global data, while objects used by the same client may not.

For example, the objects on thread T1 are used by clients 1, 2, and 3, yet they share the thread’s instance of global data. Objects used by client 1 can be found on threads T1, T3, T4, and T5. Of those objects, only the two on thread T5 share global data.

In addition, calls to the properties and methods of objects on the same thread will be serialized, and can therefore block each other — for example, if client 1 calls a method of its A object on thread T1 just before client 2 calls a method of its B object on thread T1, client 2 will have to wait until the call made by client 1 finishes.

The most serious aspect of this serialization of calls and sharing of global data is that the designer of a client application cannot predict when objects will share global data, or when they will block each other. The behavior of the round-robin thread pool algorithm is therefore referred to as nondeterministic.

Load Balancing

Another important point about round-robin threading is illustrated by Figure 8.7.

Figure 8.7 Round-robin threading lacks load balancing

Clients 2 and 3 have released their objects and closed. Thread T1 has only one object on it, and thread T2 has none — yet the next object will still be created on thread T3. In fact, objects will be created on threads T3, T4, and T5 before an object is created on either of the under-utilized threads!

One Important Advantage

The advantage that the round-robin threading model offers in exchange for these drawbacks is that it puts a limit on the total number of threads. This is a significant advantage, because multiprocessing works best if the total number of active threads roughly matches the number of processors.

For More Information Externally creatable objects are discussed in "Externally Creatable Objects," in "General Principles of Component Design."

Thread Per Object

As clients request objects, each object is created on a new thread. When the last client releases its last reference to objects on that thread, the thread is terminated.

Note that a client may have references to several objects on a thread. Suppose the client creates a Widget object, which in turn creates two dependent objects, a Sprocket and a Gear. If the client gets reference to the Sprocket object and then releases the Widget, the thread must continue to exist until the Sprocket is released.

Retaining unused threads in this fashion will eventually degrade system performance, so avoiding dangling object references is critical when you’re using a multithreaded component.

No Control Over Number of Threads

The big drawback to the thread-per-object model is that you have no control over the number of objects (and hence threads) that clients create. Creating too many threads will bog down the operating system in thread-maintenance overhead.

Too many active threads — that is, threads that are actively executing code — will bog down the operating system even more quickly. Generally speaking, you want about the same number of active threads as you have processors, to guarantee that there’s never an idle processor. When you have just one processor, that’s not very many active threads.

Internal and External Object Creation

If a client requests an instance of any externally creatable class, Visual Basic will create the object either on the next thread in the pool, or on a new thread. However, if that object turns around and requests an instance of an externally creatable class, what happens will depend on how the second instance is created:

  • If the new object is created using the New operator, or with a variable declared As New, then the sibling object will not be on another thread. It will share the thread (and global data) of the object that created it, just like any dependent object.

  • If the new object is created using the CreateObject function, it will be as if a client had requested the object. Visual Basic will create the object either on the next thread in the pool or on a new thread, depending on the thread assignment model.

Note If an object uses CreateObject to create an object on another thread, any calls it makes to the properties and methods of that object will be subject to cross-thread marshaling.

For More Information Dependent objects are discussed in "Dependent Objects," in "General Principles of Component Design." In coding the properties and methods of your objects, it’s very important to observe reentrancy rules, as discussed in "Apartment-Model Threading in Visual Basic."


Using Multiple Threads of Execution

Multithreading on a single-processor machine may not provide quite the results you expect. Consider, for example, the two methods shown in Figure 8.8.

Figure 8.8 Multithreaded method calls may appear to be slower

Method A and method B are called at the same time. In a single-threaded component, the requests are serialized, so that B doesn’t begin until A has finished. With multithreading, the two active threads duel for the processor’s attention.

Not only does the perceived average completion time increase, but more processor time is spent switching between threads.

The problem is that method A and method B take about the same amount of time. On a single-processor machine, multithreading results in a perceived performance improvement only with a mix of long and short tasks.

For example, if method B required only three time slices to complete, the user of the system would perceive a huge improvement in the responsiveness of method B — and only a slight degradation in the time required to execute method A.

The scenarios in which multithreading shows to best advantage are those in which most threads spend a substantial percentage of the time blocked — for example, waiting for file I/O — so that only one or two threads are actively executing code at any given time.

For More Information Multithreading can be enabled in components marked for unattended execution (that is, having no user interface), as described in "Scalability and Multithreading."


Event Logging for Multithreaded Components

Visual Basic only supports multithreading in components marked for unattended execution. In order to make it possible for a component to run unattended, the Unattended Execution option disables all user interaction, including message boxes and system error dialogs.

You can log such events to a file of your choice, or — on Windows NT — to the system event log. The LogMode property of the App object controls the way logging is done, and the LogPath property specifies the name of the log file.

Items entered in the log file include:

  • Text from message boxes your component attempts to display.

  • System errors, such as "Out of stack space."

  • Text strings entered using the App.LogEvent method.

Note When your component is running in the development environment, these entries go to the Immediate window instead of to the log file.

For More Information Search in the documentation index or the Object Browser for App Object, LogEvent, LogMode, LogPath, and StartLogging.


Debugging Limitations for Multithreaded Components

The Visual Basic development environment only supports one thread of execution. To debug the multithreaded behavior of your component, you must run your test application against the compiled component.

Out-of-Process Components

For out-of-process components, this means compiling both the component and the test program, and running multiple copies of the test program. (You can also run multiple copies of the test program using multiple instances of the development environment, but you cannot trace into the compiled component.)

In-Process Components

To test the multithreaded behavior of apartment-model DLLs, split your test program into two parts — a standard executable from which you control the tests, and a multithreaded out-of-process component. The standard executable is a client of the out-of-process component, which in turn is the test client for the DLL.

For example, the multithreaded out-of-process component might provide a TestMultiThreadDLL class. The standard executable might create varying numbers of TestMultiThreadDLL objects, passing each one a set of test parameters and then calling its BeginTest method.

The BeginTest method should enable a code-only timer, as demonstrated in "Creating an ActiveX EXE Component," and then return immediately, in order to avoid tying up the single-threaded Standard EXE. The TestMultiThreadDLL object’s timer would control creation and testing of the objects provided by the in-process component. Each TestMultiThreadDLL object would exercise one thread (apartment) in the in-process component.

Note To use this testing technique, you must compile both the multithreaded out-of-process component and the DLL.

Using a Native Code Debugger

If you’re compiling your component to native code, and you have Microsoft Visual C++, you can compile your component with symbolic debug information for the Visual Studio debugger, which supports multithreaded debugging.

Debug Messages

Because you can’t debug multithreaded behavior in the development environment, you can’t use Debug.Print and Debug.Assert to show debug message strings.

You can’t use message boxes to show debug messages, either, because the Unattended Execution option completely suppresses user interaction.

Approaches to sending debug messages include:

  • Create a single-threaded ActiveX DLL (that is, one that’s not marked for unattended execution) with a public, creatable class that displays a modeless form with a list box. Give the class a DebugMsg method that takes a string argument and adds it to the top of the list box.

    You can call this single-threaded DLL from any thread in your multithreaded ActiveX EXE project. The calls will be slow, because of cross-thread marshaling (as described in "Designing Thread-Safe DLLs"), but this is not a big concern when you’re debugging.

Tip Include the ThreadID in your debug message text.

  • Create a single-threaded out-of-process component that works in the fashion described above.

  • Create a program that handles Windows messages, using the AddressOf operator to subclass a Visual Basic form. Use the Windows API to create and register private messages which the component you’re debugging can use to send debug strings.

For More Information Compiling to native code is discussed in "Compiling Your Project to Native Code," in "More About Programming," in the Visual Basic Programmer’s Guide.Subclassing windows with the AddressOf operator is discussed in Accessing DLLs and the Windows API, in the Component Tools Guide.


Creating a Multithreaded Test Application

In order to test and debug in-process components (.dll and .ocx files), you will need a multithreaded client application. The steps to create a simple multithreaded application are as follows:

  1. Open a new ActiveX EXE project, and name the default class module MainApp. Set the Instancing property for MainApp to PublicNotCreatable. A MainApp object will occupy the application's first thread, and display the main user interface.

  2. On the General tab of the Project Properties dialog box, select Sub Main in the Startup Object box, select Thread per Object in the Threading Model box, and enter a unique value for the Project name. (Project Name determines the type library name; problems may arise if two applications have the same type library name.) "ThreadDemo" is the project name used in the example below. On the Component tab, select Standalone in the Start Mode box.

  3. Add a form, name it frmProcess, and set the Visible and ControlBox properties to False. This form functions as a hidden window, which Sub Main will use to identify the main thread for the process. No code is needed for this form.

  4. Add a standard module to the project. In this module, place the declarations, the Sub Main procedure, and the EnumThreadWndMain procedure shown below. As explained in the accompanying text and code comments, Sub Main will execute when your application starts, and every time you create a new thread. The Sub Main sample code demonstrates how to identify the first thread, so that you know when to create MainApp.

  5. Add a form and name it frmMTMain. This form provides the main user interface for the test application. Add to it the single declaration and the Form_Unload event immediately above the heading "Multiple Instances of the Test Application."

  6. In the Class_Initialize event procedure for MainApp, add code to show frmMTMain. Code to do this is provided below.

  7. To create additional test threads, you must have at least one class in your project whose Instancing property is set to MultiUse. Add a class module and form and insert the code provided under the heading "Creating New Threads." Because you selected Thread per Object for this project, every public object that is externally created will start on a new thread. This means that you can create a new thread by using the CreateObject function to create an instance of your MultiUse class from its programmatic ID (ProgID), as discussed in the accompanying text.

  8. Add code to frmMTMain to create new threads by creating instances of the MultiUse classes you defined. In the example below see the code under the heading "Creating New Threads."

  9. The development environment does not support multithreading. If you press F5 to run the project, all objects will be created on the same thread. In order to test multithreaded behavior, you must compile the ActiveX EXE project, and run the resulting executable.

Important To ensure that each new MultiUse object starts a new thread, you must use Thread per Object rather than Thread Pool.

Determining the Main Thread During Sub Main

Sub Main executes for every new thread. The reason for this is that Visual Basic maintains a separate copy of your global data for each thread (each apartment). In order to initialize global data for the thread, Sub Main must execute. This means that if your Sub Main loads a hidden form, or displays your application's main user interface, new copies of those forms will be loaded for every new thread you create.

The following code determines whether or not Sub Main is executing in the first thread, so that you can load the hidden form only once and display the test application's main user interface only once.

' Root value for hidden window caption
Public Const PROC_CAPTION = "ApartmentDemoProcessWindow"

Public Const ERR_InternalStartup = &H600
Public Const ERR_NoAutomation = &H601

Public Const ENUM_STOP = 0
Public Const ENUM_CONTINUE = 1

Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
   (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Declare Function GetWindowThreadProcessId Lib "user32"_
   (ByVal hwnd As Long, lpdwProcessId As Long) As Long
Declare Function EnumThreadWindows Lib "user32" _
   (ByVal dwThreadId As Long, ByVal lpfn As Long, ByVal lParam As Long) _
   As Long

' Window handle retrieved by EnumThreadWindows.
Private mhwndVB As Long
' Hidden form used to identify main thread.
Private mfrmProcess As New frmProcess
' Process ID.
Private mlngProcessID As Long

Sub Main()
   Dim ma As MainApp

   ' Borrow a window handle to use to obtain the process
   '   ID (see EnumThreadWndMain call-back, below).
   Call EnumThreadWindows(App.ThreadID, AddressOf EnumThreadWndMain, 0&)
   If mhwndVB = 0 Then
      Err.Raise ERR_InternalStartup + vbObjectError, , _
      "Internal error starting thread"
   Else
      Call GetWindowThreadProcessId(mhwndVB, mlngProcessID)
      ' The process ID makes the hidden window caption unique.
      If 0 = FindWindow(vbNullString, PROC_CAPTION & CStr(mlngProcessID)) Then
            ' The window wasn't found, so this is the first thread.
            If App.StartMode = vbSModeStandalone Then
               ' Create hidden form with unique caption.
               mfrmProcess.Caption = PROC_CAPTION & CStr(mlngProcessID)
               ' The Initialize event of MainApp (Instancing =
               '   PublicNotCreatable) shows the main user interface.
               Set ma = New MainApp
               ' (Application shutdown is simpler if there is no
               '   global reference to MainApp; instead, MainApp
               '   should pass Me to the main user form, so that
               '   the form keeps MainApp from terminating.)
            Else
               Err.Raise ERR_NoAutomation + vbObjectError, , _
                  "Application can't be started with Automation"
            End If
         End If
   End If
End Sub

' Call-back function used by EnumThreadWindows.
Public Function EnumThreadWndMain(ByVal hwnd As Long, ByVal _
   lParam As Long) As Long
   ' Save the window handle.
   mhwndVB = hwnd
   ' The first window is the only one required.
   ' Stop the iteration as soon as a window has been found.
   EnumThreadWndMain = ENUM_STOP
End Function

' MainApp calls this Sub in its Terminate event;
'   otherwise the hidden form will keep the
'   application from closing.
Public Sub FreeProcessWindow()
   Unload mfrmProcess
   Set mfrmProcess = Nothing
End Sub

Note This technique for identifying the first thread may not work in future versions of Visual Basic.

Notice that Sub Main takes no action for any thread after the first. When you add code that creates MultiUse objects (in order to start subsequent threads) be sure to include code to initialize those objects.

EnumThreadWindows is used with a call-back, EnumThreadWndMain, to locate one of the hidden windows Visual Basic creates for its internal use. The window handle of this hidden window is passed to GetWindowThreadProcessId, which returns the process ID. The process ID is then used to create a unique caption for the hidden window (frmProcess) that Sub Main loads. Subsequent threads detect this window, and thus can tell that they don't need to create the MainApp object. These gyrations are necessary because Visual Basic does not provide a way to identify the application's main thread.

The MainApp class, in its Initialize event, displays the test application's main form. MainApp should pass its Me reference to the main form, so that the form keeps MainApp from terminating. From the main user interface you can create all subsequent threads. Setting the Instancing property for MainApp to PublicNotCreatable helps you avoid displaying two main user interface forms.

A simple example of a MainApp class and its associated form (steps 5 and 6, above) might look like this:

' Code for a MainApp class.
Private mfrmMTMain As New frmMTMain

Private Sub Class_Initialize()
   Set mfrmMTMain.MainApp = Me
   mfrmMTMain.Caption = mfrmMTMain.Caption & " (" & App.ThreadID & ")"
   mfrmMTMain.Show
End Sub

Friend Sub Closing()
   Set mfrmMTMain = Nothing
End Sub

Private Sub Class_Terminate()
   ' Clean up the hidden window.
   Call FreeProcessWindow
End Sub

' Code for the form frmMTMain.
Public MainApp As MainApp

Private Sub Form_Unload(Cancel As Integer)
   Call MainApp.Closing
   Set MainApp = Nothing
End Sub

Multiple Instances of the Test Application

Including the process ID in the hidden window caption allows multiple instances of the test application to run without interfering with each other.

When you call CreateObject, the instance of the public class you create will be on a thread in the current application instance. This is because CreateObject always attempts to create an object in the current application before looking for other running Exe components that might supply the object.

Useful Properties for the Apartment

You may find it useful to expose the process ID as a read-only property of the module that contains Sub Main:

'This code not required for the test application
Public Property Get ProcessID() As Long
   ProcessID = mlngProcessID
End Property

This allows any object on the thread to get the process ID by calling the unqualified ProcessID property. You may also find it useful to expose a Boolean IsMainThread property in this fashion.

Creating New Threads

The Thread per Object option causes every public object that is externally created - that is, created using the CreateObject function - to start on a new thread. To create a new thread, simply use the programmatic ID (ProgID) of one of your MultiUse classes:

'This code not included in the test application
Dim tw As ThreadedWindow
Set tw = CreateObject("ThreadDemo.ThreadedWindow")

The variable tw now contains a reference to an object on a new thread. All calls to the properties and methods of this object that are made using tw will be subject to the extra overhead of cross-thread marshaling.

Note An object created with the New operator is not created on a new thread. It resides on the same thread where the New operator was executed. See "Designing Multithreaded Out-of-Process Components" and "How Object Creation Works in Visual Basic Components,."

To ensure that MainApp doesn't terminate until all of the other threads are finished, you can give each public class a MainApp property. When an object creates a MultiUse object on a new thread, it can pass the new object a reference to the MainApp object as part of the initialization process. (You can also pass MainApp a reference to the new object, so that MainApp has a collection of references to all objects that control threads; however, remember that this will create circular references. See "Dealing with Circular References.")

If you want a class that controls a thread to show a form, you should provide it with an Initialize method (not to be confused with the Initialize event) or a Show method that displays the form. Don't show the form in the Class_Initialize event procedure, as this could cause timing errors when you create instances of the class. In a very simple case, the code for a MultiUse ThreadedWindow class and its form, frmThreadedWindow, might look like this:

'Code for a MultiUse ThreadedWindow class.
Private mMainApp As MainApp
Private mfrm As New frmThreadedWindow

Public Sub Initialize(ByVal ma As MainApp)
   Set mMainApp = ma
   Set mfrm.ThreadedWindow = Me
   mfrm.Caption = mfrm.Caption & " (" & App.ThreadID & ")"
   mfrm.Show
End Sub

Friend Sub Closing()
   Set mfrm = Nothing
End Sub

'Code for the form frmThreadedWindow.
Public ThreadedWindow As ThreadedWindow

Private Sub Form_Unload(Cancel As Integer)
   Call ThreadedWindow.Closing
   Set ThreadedWindow = Nothing
End Sub

The following code snippet shows how you might initialize the ThreadedWindow object:

'Code for the test application's main form (frmMTMain).
Private Sub mnuFileNewTW_Click()
   Dim tw As ThreadedWindow
   Set tw = CreateObject("ThreadDemo.ThreadedWindow")
   ' Tell the new object to show its form, and
   '   pass it a reference to the main
   '   application object.
   Call tw.Initialize(Me.MainApp)
End Sub

If you have a number of classes that can control threads, you can make your code more generic by defining an IApartment interface to contain the Initialize method. When you implement IApartment in each class, you can provide the appropriate Initialize method code for that class. Your thread creation code might look like this:

'This code not required for the test application
Private Sub mnuFileNewObject_Click(Index As Integer)
   Dim iapt As IApartment
   Select Case Index
     Case otThreadedWindow
      Set iapt = CreateObject("ThreadDemo.ThreadedWindow")
     ' (other cases...)
   End Select
   ' Common code to initialize objects.
   Call iapt.Initialize(MainApp)
End Sub

Note You can make an IXxxxApartment interface that's known only to the multithreaded application by defining the interface in a separate type library. In the ActiveX Exe project, set a reference to the type library.

Keeping References to Threaded Objects

To ensure proper shutdown of a multithreaded application, you must keep careful track of all references to the MultiUse objects you use to create and control threads.

Define your object lifetime goals clearly. For example, consider the case of a MultiUse object that shows a form. The easiest way to manage object lifetime is to have the object pass the form a Me reference; the form then keeps the object alive. When the user closes the form, the form's Unload event must set all references to the MultiUse object to Nothing, so that the object can terminate and in turn clean up its reference to the form. (You may find it useful to give the MultiUse object a Friend method that cleans up the reference to the form, and all other internal object references; the form's Unload event can call this method.)

If the object that controls a thread creates additional objects on the thread, using the New operator, make sure you clean up references to those objects. The thread cannot close until all references to objects that were created on the thread have been released. Open threads consume system resources.

Friend Methods Cannot Be Used Cross-Thread

Because Friend properties and methods are not part of the public interface of a class, you cannot call them from another thread. Cross-thread calls between objects are limited to properties and methods that are declared Public.

Reentrancy

If a method of an object yields by calling DoEvents, showing a modal form, or making a secondary call to an object on another thread, then the code in the method can be entered by a second caller before the first call completes. If such a method uses or changes property values or module-level variables, this could result in an invalid internal state for the object. To protect against reentrancy, you can:

  • Avoid yielding.

  • Keep a module-level Boolean flag for each method. When a method begins, it tests this flag to determine whether a method call is in progress. If not, the method sets the flag to True and continues; otherwise it raises an error. You must be careful to turn this flag off when the method completes or exits for any reason.

  • Write reentrant methods - that is, methods that don't depend on module-level data.

Asynchronous Tasks

Visual Basic doesn't provide a way to fork execution - that is, to have one thread initiate a method call on a new thread and immediately resume processing on the original thread. You can simulate this behavior in your test application by having the original method call turn on a timer and then return immediately. When the timer event occurs, you can turn the timer off and perform the asynchronous processing. This technique is discussed in "Asynchronous Call-Backs and Events", and is demonstrated (see "Creating an ActiveX Exe Component") and in the Coffee sample application.

Using the Multithreaded Test Application

You must compile the multithreaded test application in order to test your apartment-threaded component, because the Visual Basic development environment does not currently support multiple threads of execution. If you have Visual Studio, you may find it useful to compile the test application with debugging information for native code debugging, so that you can use the Visual Studio debugger.


Scalability Through Multiple Processes: SingleUse Objects

Visual Basic provides a second mechanism for using multiple threads of execution. By setting the Instancing property of a class to SingleUse, you cause each instance of the class to run in a separate instance of your component. This means that even though your component is single-threaded, each instance of the SingleUse class has its own thread of execution.

SingleUse objects require a lot more overhead than multiple objects in a multithreaded component. However, there are several reasons you might want to use SingleUse objects:

  • If your component must show forms, active documents, or other user interface elements, you cannot mark it for unattended execution — and thus it cannot be multithreaded. Components that provide SingleUse objects can have forms and active documents.

  • Your component can function as a standalone desktop application, and you want it to have an Application object that’s not shared between clients.

  • High-risk activities can be isolated in separate processes with SingleUse objects. If the object suffers a fatal error, other processes are not affected. By contrast, a fatal error in a multithreaded component terminates all threads.

Using Createable SingleUse Objects to Avoid Blocking

Figure 8.9 shows how SingleUse objects have separate threads of execution, which can be preemptively multitasked.

Figure 8.9 A component with a SingleUse object

Components That Provide Multiple SingleUse Classes

The best way to provide exclusive use of an out-of-process component is to give it exactly one SingleUse class, with as many dependent objects as necessary.

If you set the Instancing property to SingleUse for more than one class module in your component, satisfying client requests for objects becomes somewhat complicated, and it becomes difficult to ensure that a client has objects from only one instance of the component.

You can think of each class as a hole, which will be filled when an object of that class is created. Figure 8.10 shows a component that provides three objects — Widget, Gear, and Sprocket. Each instance of the component can provide one object of each type.

Figure 8.10 Instancing with three classes marked SingleUse

The lettered rectangles represent the potential to satisfy a request for an object of one of the three classes. A dotted outline indicates that the component has provided an object of that type, and so cannot provide another.

If Client A now creates a Sprocket object, that object will not be supplied by the same instance of the component that provided its Widget object. If the point of making the classes SingleUse was to make it possible for one client to have exclusive use of one component, this scheme will not guarantee it.

As noted above, a better way to accomplish exclusive use of an out-of-process component is to provide one SingleUse object as the only externally creatable object, and as many dependent objects as you need.

No Way to Predict Which Instance Provides an Object

It might appear from Figure 8.10 that Client B has one object from each of two instances of the component because of the order in which it created the objects. However, even if Client B had created its Widget object first, so that the second instance of the component was already running when Client B requested a Sprocket, there’s no guarantee that the Sprocket would be provided by the second instance of the component.

As noted in the callout for Client C, when there are multiple instances of a component that could provide a particular object, the ActiveX specification does not guarantee which instance will provide the object. It’s best to assume random selection.

Internal and External Instancing of SingleUse Classes

When a client creates two instances of a class marked SingleUse, two instances of the component executable are started. Within the component, however, it’s possible to create multiple instances of such a class. Figure 8.11 shows the distinction between internal and external object creation.

Figure 8.11 Internal and external creation of SingleUse objects

When code within the component creates an object from one of the component’s own SingleUse classes using the Set statement with the New operator, or by declaring a variable As New, the object does not fill the ‘hole’ that allows a client to create an instance of the class. Code within the component in Figure 8.11 could create any number of Sprocket objects in this fashion.

When the CreateObject function is used to create an object from a SingleUse class, however, it is as if the object had been created by a client. If the client in Figure 8.11 now attempts to create a Sprocket object, a second instance of the component will have to be started. Another instance of the component will also be started if the component instance in Figure 8.11 uses CreateObject to create another Sprocket.

SingleUse Really Means SingleUse

Once a client application creates an object from a SingleUse class, no client can ever create an object of that class from that instance of the component, even if the first client releases the object. That is, once the ‘hole’ has been filled, it can never be empty — even if the object is destroyed.

In other words, marking a class module SingleUse means that during the lifetime of an instance of the component, only one instance of the class can be created externally — either by a client application, or by the component itself using the CreateObject function.

Debugging Limitations

When you run your component in the development environment, your client test programs can create only one instance of each SingleUse class during that debugging session. Once an instance of a class has been created, subsequent attempts to create an object from that class will cause Error 429, "OLE Automation server can’t create object."

For debugging purposes, you can change SingleUse to MultiUse. However, to test the SingleUse behavior of your component, you must make the component executable.

For More Information To run your component alternately as an executable file and in the Visual Basic development environment, see "How to Test Compiled Components" in "Debugging, Testing, and Deploying Components."


Asynchronous Call-Backs and Events

When a client makes a method call, it’s blocked until the call returns. That is, the client can’t execute any code while it’s waiting. This is known as synchronous processing. By using asynchronous processing, you can free the client to do other things while it’s waiting.

In asynchronous processing, the method call that starts a task returns instantly, without supplying a result. The client goes about its business, while the component works on the task. When the task is complete, the component notifies the client that the result is ready.

Asynchronous processing is also useful when clients need to be notified of interesting occurrences — for example, changes in database values, or the arrival of messages. A client tells a component it wants to be notified when certain things happen, and the component sends notifications when those things occur.

Both of these scenarios depend on asynchronous notifications. The client application is minding its own business, when out of the blue comes a notification that an asynchronous request is complete, or that something of interest has occurred.

Visual Basic provides two mechanisms — events and call-back methods — for implementing asynchronous notifications, as discussed in the following topics.

  • Asynchronous Notifications Using Events The simplest notification technique is for the component to raise an event. The client handles the event, and takes some action in response.

  • Asynchronous Notifications Using Call-Back Methods The client implements an interface containing a call-back method, which the component calls when notifications are required.

  • When to Use Events or Call-Backs for Notifications Explains how the decision to use events or call-backs affects the character of your notifications, and provides criteria for deciding which to use.

For More Information "Creating an ActiveX EXE Component" contains step-by-step procedures for implementing asynchronous notifications using both events and call-back methods.


Asynchronous Notifications Using Events

There are two parts to implementing asynchronous processing using events. The first part is the responsibility of the author of a component. The author must:

  1. Define the tasks or notifications to be performed.

  2. Provide one or more externally creatable classes to manage the tasks or notifications. This manager class may also do the work, or a worker class may be provided to do the actual processing.

  3. Provide the manager class with methods that clients can call to initiate tasks or to request notifications.

  4. Declare the events that clients must handle in order to receive notifications.

  5. Write code to start the task, or the process of watching for interesting occurrences.

  6. Write code to raise the event when the task is complete, or when the interesting occurrences are observed.

The second part is the responsibility of the developer who uses the component. The developer must:

  1. Create a WithEvents variable to contain a reference to the object that will provide the notification events.

  2. In the event procedures associated with the WithEvents variable, write code to handle the desired notification events.

  3. Write code to request an instance of the component’s manager class, and place the reference to it in the WithEvents variable.

  4. Write code to call the methods that initiate tasks or that request notifications.

Figure 8.12 shows how the author’s part and the developer’s part interact to enable asynchronous processing for the CoffeeReady example from the step-by-step procedures in "Creating an ActiveX EXE Component."

Figure 8.12 Asynchronous notifications using events

Note The numbers in Figure 8.12 indicate the order in which things happen in the finished application and component. They do not correspond to the numbers in the task lists.

A single event can be handled by multiple clients. One way to connect multiple clients to a single Coffee object would be to interpose a Connection object between the client and the Coffee object. In this way, each client would have its own Connection object, and each Connection object would supply its client with a reference to one central Coffee object.

The Coffee object’s CoffeeReady event would be received by all clients that had a WithEvents variable containing a reference to the Coffee object.

Note Visual Basic’s events can be thought of as anonymous. That is, the object that raises the event has no way of knowing whether a given event is handled by one, two, or two dozen objects — or by no objects at all. In addition, the object that raises an event has no way of knowing whether errors occur in the event-handling code of other objects.

Important When you’re designing a system in which multiple clients receive the same event, do not make assumptions about the order in which clients receive the event. The order is undefined, and may differ depending on version number or platform.

For More Information You can see these tasks carried out in the step-by-step procedures in "Creating an ActiveX EXE Component," which demonstrates asynchronous notifications using both events and call-back methods.


Asynchronous Notifications Using Call-Back Methods

There are two parts to implementing asynchronous processing using call-back methods. The first part is the responsibility of the author of a component. The author must:

  1. Define the tasks or notifications to be performed.

  2. Provide one or more externally creatable classes to manage the tasks or notifications. This manager class may also do the work, or a worker class may be provided to do the actual processing.

  3. Create a type library containing the interface (or interfaces) that clients must implement in order to receive notifications. This interface must include all methods the component will call to notify the client.

    Note You can create type libraries with Visual Basic, as explained in "Creating Standard Interfaces with Visual Basic," in "General Principles of Component Design."

  4. Provide the manager class with methods that clients can call to initiate tasks or to request notifications.

    Note Each of these methods must have one argument declared as the interface type defined in step 3, so the client can pass the interface containing its implementation of the appropriate call-back method.

  5. Write code to start the task, or the process of watching for interesting occurrences.

  6. Write code to invoke the appropriate call-back method when the task is complete, or when the interesting occurrences are observed.

Tip If a client provides a number of asynchronous services, you may want to group closely related call-backs on separate interfaces. This is because a client class must implement all the methods on an interface, whether it uses them or not. Having one big interface with all your call-back methods on it is thus inconvenient for clients.

The second part is the responsibility of the developer who uses the component. The developer must:

  7. Create a public class that implements the interface defined by the component author.

    Note Use PublicNotCreatable for the Instancing property of this class. The class must be public so the component can invoke the call-back methods, but there’s no reason to let other applications create instances of the class.

  8. In the call-back methods the client will use, write code to handle the notifications.

Tip All the methods of an interface must be implemented, but those you don’t use can simply contain a comment.

  9. Write code to request an instance of the component’s manager class.

  10. Write code to call the methods that initiate tasks or that request notifications.

Figure 8.13 shows how the author’s part and the developer’s part interact to enable asynchronous processing for the CoffeeReady example from the step-by-step procedures in "Creating an ActiveX EXE Component."

Figure 8.13 Asynchronous notifications using call-back methods

Note The numbers in Figure 8.13 indicate the order in which things happen in the finished application and component. They do not correspond to the numbers in the task lists.

Tip If the NotifyMe object is only going to be used once, the client doesn’t have to keep a reference to it. In this case, the client code to call TellMeReady could be written as follows:

Call mcof.TellMeReady(New NotifyMe)

Call-Backs to Multiple Clients

One way for the Coffee object to handle multiple clients would be for the component to interpose a Connection object between the client and the Coffee object — so that each client would have its own Connection object, and each Connection object would supply its client with a reference to one central Coffee object.

The Coffee object’s TellMeReady method would have to place the ICoffeeNotify references into a collection, so the timer could enumerate the collection and call each client’s CoffeeReady method.

For More Information You can see these tasks carried out in the sample applications for "Creating an ActiveX EXE Component," which demonstrate asynchronous notifications using both events and call-back methods. Implementing interfaces is discussed in "Providing Polymorphism by Implementing Interfaces," in "General Principles of Component Design." Polymorphism is introduced in "Polymorphism," in "Programming with Objects," in the Visual Basic Programmer’s Guide.


When to Use Events or Call-Backs for Notifications

The topics "Asynchronous Notifications Using Call-Back Methods" and "Asynchronous Notifications Using Events" demonstrate that call-backs are more work to implement than events. However, you should not base your decision about which to use on the amount of work involved. Call-backs and events represent different approaches to communication, and you should select the approach that best fits your needs.

You can characterize the difference between events and call-backs as follows: An event is like an anonymous broadcast, while a call-back is like a handshake.

The corollary of this is that a component that raises events knows nothing about its clients, while a component that makes call-backs knows a great deal.

Here’s what these things mean to you as a developer:

  • A client that has a reference to an object that raises events can handle those events by placing the reference into a WithEvents variable. The object that raises the events has no information about its clients. It's broadcasting to an unknown audience — perhaps to an empty theater.

    By contrast, a component that's making call-backs must have a reference to every object it's going to call. It has to know exactly how many of them there are.

  • An object that raises events has no control over the order in which clients receive its events. (And you should take care to avoid creating dependencies on any ordering you may observe.)

    By contrast, a component making call-backs can control the order in which it calls clients back. It might give some clients higher priority, for example.

  • When an object raises an event, all of its clients handle the event before the object that raised the event gets control again.

    By contrast, a component making call-backs gets control back after each call it makes to a client.

  • If an event contains a ByRef argument, that argument can be altered by any client that handles the event. The last client's changes are the only ones visible to the object that raised the event, because (as noted above) the object that raised the event doesn't get control again until all of its clients have handled the event.

    By contrast, a component making call-backs can examine changes to ByRef arguments after every call to a client, and can pass the next client fresh values for those arguments.

  • If an unhandled error occurs in a client's event handler, the object that raised the event cannot receive the error. If the object is provided by an in-process component running in the client’s address space, both client and component will terminate as a result of the unhandled error.

    By contrast, a component making call-backs will receive errors that occur in the call-back method, and must be prepared to handle them.

Note An additional difference between events and call-back methods is that events cannot have optional arguments, named arguments, or ParamArray arguments.

A component can provide some notifications using call-backs, and some using events. The nature of the notification determines which mechanism should be used. You should use an event to provide a notification when all of the following statements are true:

  1. The notification can be broadcast anonymously.

  2. The order in which clients receive the notification is not important.

  3. There’s no need for the component to get control back until all clients have received the notification.

  4. If the notification involves ByRef arguments, there’s no need for the component to test the values of those arguments after each client receives the notification. The component only needs to know the last value assigned. (You can plan for clients to cooperate in their use of a ByRef argument — for example, once a Cancel argument is set to True, no client changes it — but there’s no way to enforce this.)

  5. The component doesn't need to know about errors that occur in the client.

If any one of the statements above is false, then you should do the extra work necessary to provide the notification using call-back methods.

You may also want to do the extra work to use call-backs when performance is critical. You can get vtable binding with call-back methods by using the Implements statement to add the call-back interface to the client's call-back object. Events are not vtable bound. (This will be much more noticeable with an in-process component that provides events or call-backs.)


Displaying Forms from Code Components

Code components can display modal or modeless forms, subject to certain restrictions.

In-Process Components that Show Modeless Forms

Modeless forms displayed by in-process components cannot function correctly unless they can communicate with the client's message loop. Therefore, in-process components created with Visual Basic can display modeless forms only in client processes that support such communication.

The following applications support the display of modeless forms by in-process components:

  • Applications created with Visual Basic 5.0 or later.

  • Microsoft Office 97 or later.

  • Applications that have the Visual Basic Technology logo. (That is, that license Visual Basic for Applications version 5.0 or later.)

Applications that do not support the display of modeless forms by in-process components include:

  • Applications created with earlier versions of Visual Basic.

  • Earlier versions of Microsoft Office.

  • Version 3.x of Microsoft Internet Explorer.

Determining Support for Modeless Forms at Run Time

To allow in-process components to detect at run time whether a client application supports the display of modeless forms, Visual Basic provides the Boolean NonModalAllowed property of the App object.

An in-process component should test this property before showing a modeless form. If the value is True, the form can be shown vbModeless. If the value is false, showing a modeless form will cause run-time error 369. The component should degrade gracefully by showing the form vbModal instead.

Important Debugging Limitation

When you’re debugging an in-process component in the development environment, using a non–Visual Basic application as an out-of-process test program, modeless forms will appear to work even if the non-Visual Basic application doesn’t support them.

Testing the NonModalAllowed property of the App object is of no use in this situation; the property will return True regardless of the client. The reason for this is that when debugging an in-process component with an out-of-process test application, Visual Basic cannot determine whether the test application supports modeless forms.

Before attempting to write an in-process component that will show modeless forms, for use with a specific non–Visual Basic application, you should compile a small test DLL with a method that displays the value of the NonModalAllowed property of the App object in a message box. Call this method from the non-Visual Basic application, to determine whether it supports modeless forms.

Forms Displayed by Out-Of-Process Components

Modal and modeless forms displayed as a result of method calls to out-of-process components will not necessarily appear in front of the client application’s forms. The reason is that Automation does not define a z-order relationship between forms in different applications.

For More Information "Creating an ActiveX EXE Component" provides a demonstration of the problems with showing forms from out-of-process components.


How Object Creation Works in Visual Basic Components

In certain circumstances, the mechanism you use to create objects — that is, CreateObject, the New operator, or a variable declared As New — can have subtle effects on object creation. These effects depend on whether or not Visual Basic uses the object creation services provided by the Component Object Model (COM).

When you create objects from classes that are provided by other components, Visual Basic always uses COM object creation services. As a result, when you’re creating externally provided objects, there are no differences between the New operator, declaring a variable As New, and using the CreateObject function.

The CreateObject function always uses COM object creation services, whether you use it to create externally provided objects or to create instances of classes that are part of your project. So for CreateObject, there's no difference between external and internal object creation.

However, when you use the New operator (or a variable declared As New) to create an instance of a class that's part of your project, Visual Basic uses a very efficient private implementation of COM object creation.

In other words, Visual Basic uses the same mechanism for all object creation except creating objects with New or As New from classes in your project. This is summarized in the following table.

Object created from | Using CreateObject | Using New, As New
A class provided by another component | COM object creation services | COM object creation services
A class that’s part of your project | COM object creation services | Visual Basic private object creation

Note The CreateObject function cannot be used on classes whose Instancing property is Private or PublicNotCreatable. The New operator can be used on any class.

The following are specific examples of the subtle differences that can arise depending on how you create objects from classes that are part of your project:

  • If you use the New operator to create an instance of a SingleUse class, the object will be created in your current program instance — as if its Instancing property were MultiUse. Using CreateObject will start another instance of your component. (See "Scalability Through Multiple Processes: SingleUse Objects.")

  • In a multithreaded out-of-process component with thread-per-object or round-robin threading, an instance of any of your project's externally creatable classes created with the New operator will share the thread of the object that executed New — while one created using CreateObject will be on another thread.

  • In a component designed for use with Microsoft Transaction Server, an instance of any of your project's externally creatable classes created with the New operator will be unknown to Microsoft Transaction Server. Objects that will be used with Microsoft Transaction Server must be created using CreateObject.


How Binding Affects ActiveX Component Performance

Binding is the process of setting up a property or method call that’s to be made using a particular object variable. It’s part of the overhead of calling the property or method.

The time required to call a procedure depends on two factors:

  • The time required to perform the task the procedure was designed to do, such as finding the determinant of a matrix.

  • The overhead time required to place the arguments on the stack, invoke the procedure, and return.

As a component author, you’ll do everything you can to minimize the first item. The second item, however, is not entirely under your control.

The overhead for a method call depends on the type of binding Visual Basic uses for the method call, which in turn depends on the way a client application declares object variables, which in turn depends on the developer of the client application.

To ensure that developers who use your component get the best possible performance, you may want to include the information in this topic in the Help file for your component.

Note Binding affects all property and method calls, including those the objects in your component make to each other. Thus the binding issues discussed here can also affect the internal performance of your component.

Types of Binding

There are two main types of binding in Automation — late binding and early binding. Early binding is further divided into two types, referred to as DispID binding and vtable binding. Late binding is the slowest, and vtable binding is the fastest.

Late Binding

When you declare a variable As Object or As Variant, Visual Basic cannot determine at compile time what sort of object reference the variable will contain. Therefore, Visual Basic must use late binding to determine at run time whether the actual object has the properties and methods you call using the variable.

Note Late binding is also used for variables declared As Form or As Control.

Each time you invoke a property or method with late binding, Visual Basic passes the member name to the GetIDsOfNames method of the object’s IDispatch interface. GetIDsOfNames returns the dispatch ID, or DispID, of the member. Visual Basic invokes the member by passing the DispID to the Invoke method of the IDispatch interface.

For an out-of-process component, this means an extra cross-process method call, essentially doubling the call overhead.

Note You cannot call the methods of the IDispatch interface yourself, because this interface is marked hidden and restricted in the Visual Basic type library.

Early Binding

If Visual Basic can tell at compile time what object a property or method belongs to, it can look up the DispID or vtable address of the member in the type library. There’s no need to call GetIDsOfNames.

When you declare a variable of a specific class — for example, As Widget — the variable can only contain a reference to an object of that class. Visual Basic can use early binding for any property or method calls made using that variable.

This is the recommended way to declare object variables in Visual Basic components and applications.

Important Whether early or late binding is used depends entirely on the way variables are declared. It has nothing to do with the way objects are created.

Tip Early binding dramatically reduces the time required to set or retrieve a property value, because call overhead is a significant fraction of the total call time.

vTable Binding

In the fastest form of early binding, vtable binding, Visual Basic uses an offset into a virtual function table, or vtable. Visual Basic use vtable binding whenever possible.

Objects created from Visual Basic class modules support all three forms of binding, because they have dual interfaces — that is, vtable interfaces derived from IDispatch.

If client applications declare variables using explicit class names, Visual Basic objects will always be vtable bound. Using vtable binding to call a method of an in-process component created with Visual Basic requires no more overhead than calling a function in a DLL.

Note For in-process components, vtable binding reduces call overhead to a tiny fraction of that required for DispID binding. For out-of-process components the change is not as great — vtable binding is faster by a small but significant fraction — because the bulk of the overhead comes from marshaling method arguments.

DispID Binding

For components that have type libraries but don’t support vtable binding, Visual Basic uses DispID binding. At compile time, Visual Basic looks up the DispIDs of properties and methods, so at run time there’s no need to call GetIDsOfNames before calling Invoke.

Note While you can ensure that early binding is used (by declaring variables of specific class types), it’s the component that determines whether DispID or vtable binding is used. Components you author with Visual Basic will always support vtable binding.


How Marshaling Affects ActiveX Component Performance

A client and an in-process component share the same address space, so calls to the methods of an in-process component can use the client’s stack to pass arguments. This is not possible for an out-of-process component; instead, the method arguments must be moved across the boundary between the two processes. This is called marshaling.

A client and an out-of-process component communicate via a proxy/stub mechanism, as shown in Figure 8.14. The proxy and stub handle the marshaling and unmarshaling of arguments passed to methods of the component; they are completely transparent to the client.

Figure 8.14 A client and an out-of-process component

Marshaling is slower than passing parameters within a process, especially when parameters are passed by reference. It’s not possible to pass a pointer to another address space, so a copy must be marshaled to the component’s address space. When the method is finished, the data must be copied back.

Choosing Between ByVal and ByRef

Because of marshaling considerations, certain method arguments should be declared ByVal for out-of-process components and ByRef for in-process components, as described below.

Object References

When you’re declaring methods for objects provided by an out-of-process component, always use ByVal to declare arguments that will contain object references.

The reason for this is that cross-process marshaling of object references requires significantly less overhead if it’s one-way. Declaring an argument ByRef means that the object reference must be marshaled to your component, and then back to the client when the method is finished.

The only reason to use ByRef in the declaration of an argument that’s going to contain an object reference is when the method will replace the client’s object reference with a reference to another object.

Don’t Pass Object References When Properties Will Do

If the method you’re declaring requires a property value supplied by an object that belongs to the client, declare the argument with the data type of the property rather than the object’s class. Marshaling an object reference requires significantly more overhead than marshaling a simple data type.

Other Data Types

It’s common practice to declare parameters in Visual Basic procedures ByRef if they will be used to pass large strings and Variant arrays, even if the procedure doesn’t make any changes to the parameter. It’s much faster to pass a four-byte pointer to the data than to make a copy of the entire string or array and pass a pointer to the copy.

This practice works within the address space of your own process — that is, within your own program or with an in-process component — because the method to which you pass the parameter can use the pointer to access the data directly.

Cross-process marshaling reverses this practice. Data for a ByRef method argument is copied into the component’s address space, and the method is passed a pointer to the local copy of the data.

The method uses the pointer to modify the copy. When the method ends, the data for any ByRef parameters is copied back into the client’s address space. Thus a parameter declared ByRef will be passed cross-process twice per method call.

If you expect users to pass large strings or Variant arrays to a method of your component, and the method does not modify the data, declare the parameter ByVal for an out-of-process component and ByRef for an in-process component.

Important If you declare a parameter of a method ByRef in an out-of-process component, developers using the component cannot avoid the effects of marshaling by putting parentheses around the parameter, or by using the ByVal keyword when they call the method. Visual Basic will create a copy of the data in this situation, but since Automation has no way of knowing the data is just a copy, it will marshal the copy back and forth between processes. The data is thus copied a total of three times.

Passing Parameters vs. Setting Properties

Properties make objects easy to use, but setting properties for an out-of-process component can be slow. When a client is using an out-of-process component, the extra overhead of cross-process marshaling makes it faster to call one method with five parameters than to set five properties and then call a method.

In cases where users of your component will frequently set a group of properties before calling a particular method, you can add a group of optional parameters to the method, one for each property. If one of these parameters is supplied, the method sets the corresponding property before proceeding.

If you give the optional parameters the names of the properties they are used to set, they can be used as named parameters (also known as named arguments), as in the following code fragments:

' Code fragment from a hypothetical Widget class.
Public Load As Double
Public Torque As Double
' ...other property declarations...
Public Sub Spin(ByVal Iterations As Long, _
      Optional Load As Variant, _
      Optional Torque As Variant)
   ' Before spinning the Widget, set any property
   '   values that were supplied. The object self-
   '   reference Me distinguishes between parameter
   '   and property.
   If Not IsMissing(Load) Then Me.Load = Load
   If Not IsMissing(Torque) Then Me.Torque = Torque
   ' ...code to spin the Widget...
End Sub

' Code fragment from a client, calling the Spin method
'   to spin the Widget 10000 times. Note the use of
'   the named parameter to set the Torque.
Dim wdgNew As New Widget
wdgNew.Spin 10000, Torque:=27.6


Building ActiveX Controls

This chapter covers control creation in depth. The majority of the topics are organized according to the sequence of control development tasks outlined in "Control Creation Recap," at the end of the topic, "Creating an ActiveX Control."

First and most important, however, is an introduction to the terminology and concepts of control creation, in the topics "Control Creation Terminology," "Control Creation Basics," and "Interacting with the Container."

These are followed by topics associated with development tasks:

  1. Determine the features your control will provide.

    "Visual Basic ActiveX Control Features."

  2. Design the appearance of your control.

    "Drawing Your Control."

  3. Design the interface for your control — that is, the properties, methods, and events your control will expose.

    "Adding Properties to Controls," "Adding Methods to Controls," "Raising Events from Controls," and "Providing Named Constants for Your Control."

  4. Create a project group consisting of your control project and a test project.

    "Setting Up a New Control Project and Test Project."

  5. Implement the appearance of your control by adding controls and/or code to the UserControl object.

  6. Implement the interface and features of your control.

    "Creating Robust Controls."

  7. As you add each interface element or feature, add features to your test project to exercise the new functionality.

    "Debugging Controls."

  8. Design and implement property pages for your control.

    This subject is covered in "Creating Property Pages for ActiveX Controls."

  9. Compile your control component (.ocx file) and test it with all potential target applications.

    "Distributing Controls."

The chapter ends with "Localizing Controls," which discusses localizing your control for other languages. The complete list of top-level topics is:

Contents

  • Control Creation Terminology

  • Control Creation Basics

  • Interacting with the Container

  • Visual Basic ActiveX Control Features

  • Drawing Your Control

  • Adding Properties to Controls

  • Adding Methods to Controls

  • Raising Events from Controls

  • Providing Named Constants for Your Control

  • Setting Up a New Control Project and Test Project

  • Creating Robust Controls

  • Debugging Controls

  • Distributing Controls

  • Localizing Controls

Sample Application: CtlPlus.vbg

Includes a fully functional version of the ShapeLabel control created in the step-by-step procedures in Chapter 4, "Creating an ActiveX Control," and other controls that illustrate the control creation features in this chapter. The sample applications are listed in the Samples directory.

Topics

Control Creation Terminology

Briefly introduces terms that make discussions of control creation simpler, and draws some important distinctions between terms commonly used in other contexts.
Control Creation Basics

Introduces the UserControl object, the three ways to build controls, the two ways to package them, and key events in the lifetime of UserControl objects.
Interacting with the Container

Your control is never alone. This topic examines the container your control is placed on, and some important aspects of your control's relationship to it.
Visual Basic ActiveX Control Features

Explores the features of the UserControl object that allow you to write full-featured ActiveX controls.
Drawing Your Control

Discusses user-drawn controls and controls whose appearance is provided by constituent controls placed on the UserControl.
Adding Properties to Controls

Discusses the significant differences between adding properties to controls and adding them to other kinds of objects, including saving and retrieving property values.
Adding Methods to Controls

A short discussion of control methods.
Raising Events from Controls

Explains the creation of events, and techniques for taking advantage of UserControl events and constituent control events.
Providing Named Constants for Your Control

Specifics of adding named constants to controls.
Setting Up a New Control Project and Test Project

Discusses requirements for testing and debugging ActiveX control projects.
Creating Robust Controls

Lists and briefly explains some DOs and DON'Ts.
Debugging Controls

Discusses the finer points of running and debugging code at design time.
Distributing Controls

In addition to the obligatory discussion of Setup programs, this topic explains licensing and versioning issues for controls.
Localizing Controls

Outlines development strategies for creating controls that can be marketed in many language locales.

Sample application

CtlPlus.vbg

Includes a fully functional version of the ShapeLabel control created in the step-by-step procedures in "Creating an ActiveX Control," and other controls that illustrate the control creation features in this chapter. The sample applications are listed in the Samples directory.


Control Creation Terminology

Controls are unlike other objects you create with Microsoft Visual Basic. They're not just code; they have visual parts, like forms — but unlike forms, they can't exist without some kind of container. In addition, controls are used — in different senses — by both developers and end users of applications.

These characteristics of controls require some additional terminology.

Control Class vs. Control Instance

The control you develop in Visual Basic is actually a control class, a description from which controls will be created. When you put a control on a form, you're creating an instance of this control class.

To avoid confusion, remember that the control class you're designing is distinct from the control instances you place on forms.

Control vs. Control Component

Controls are objects provided by control components, also known as .ocx files. A control component may provide more than one kind of control.

An ActiveX control project contains one or more .ctl files, each of which defines a control class. When you build the project, Visual Basic gives the control component an .ocx extension.

A developer who buys your control component and installs it can use any of the controls you defined (and made public).

Containers and Siting

A control instance cannot exist by itself. It must be placed on a container object, such as a Visual Basic form. The process of hooking a control instance up to its container is called siting — that is, assigning the control a site on the container.

When a control instance has been sited, its events are available as event procedures in the form's code window, and it has access to other services the container provides, such as Extender and AmbientProperties objects.

Interface vs. Appearance

A control consists of three parts, two public and one private. The control's appearance is public, because users see and interact with it. The control's interface — the set of all its properties, methods, and events — is also public, because it's used by any program that includes instances of the control.

The private part of a control is its implementation, the code that makes the control work. The effects of a control's implementation can be seen, but the code itself is invisible.

Author vs. Developer

The author of a control compiles her project as a control component, or .ocx file, which may contain one or more controls. A developer uses the control (or controls) to create an application, and includes the .ocx file in their setup program. The user installs and uses the application.

These terms avoid confusion between the developer of a control and the developer who uses the control in an application. The former will be referred to throughout as the author of the control.

For example, the author of the control is the only person who can place code in the event procedure for the UserControl object's Click event.

If she ends that code by raising her own Click event, the developer who adds an instance of the control to one of his forms will have an event procedure in which to place code he wants to execute when the user of his application clicks on that control instance.

When the user of the application clicks on the control, the author's code and the developer's code get executed.

Note Developers are not the only direct consumers of ActiveX controls. You can design controls for users to place on documents in desktop applications such as Microsoft Office.

Design-Time Instance vs. Run-Time Instance

When a Visual Basic user puts a control on a form at design time, an actual instance of the control class is created. The user thinks of this control as a permanent fixture of the form. In fact, it's only a design-time instance; if the form is closed for any reason — clicking its Close button, closing the project, or pressing F5 to place the project in Run mode — this design-time instance is destroyed.

If the project is placed in Run mode, a run-time instance of the control is created when the form is loaded. This run-time instance is destroyed when the form is unloaded. When the form once again appears in design mode, a new design-time instance of the control is created.

A new design-time instance is also created the first time the form's .frm file is opened, after the project that contains the form is opened in the development environment. The .frm file contains all the values of the control instance's properties.

The property values in the in-memory copy of the .frm file are used by Visual Basic to re-create the control as modes change. When you make the project into an executable, the property values it contains are compiled in, so that a run-time instance of the control can be created when the compiled application is run.

Control lifetime is discussed in more detail in "Understanding Control Lifetime and Key Events," later in this chapter.


Control Creation Basics

If you're used to developing Visual Basic applications, the world of control creation may at first seem upside down and backward. Events flow in the opposite direction from what you're used to. Many properties you're used to setting as a consumer of controls are suddenly off limits. Focus is — well, we'll get to focus in due time.

In short, you're not the owner of an application any more — you're just a widget, a cog in somebody else's machine. Relax. Let things unfold naturally.

The topics in this section provide the basic facts and concepts you need to master this new way of thinking about controls.

  • The UserControl Object Introduces the UserControl object, which is the basis of all ActiveX controls created with Visual Basic. If you read no other topic, read this one.

  • Three Ways to Build ActiveX Controls Discusses the three models for building controls in Visual Basic, and their different purposes and requirements.

  • Two Ways to Package ActiveX Controls You can package your controls to be shared, or compile them into your executables.

  • Understanding Control Lifetime and Key Events If you're used to creating forms in Visual Basic, you'll need to adjust to a new set of key events, and new ideas of control lifetime.


The UserControl Object

An ActiveX control created with Visual Basic is always composed of a UserControl object, plus any controls — referred to as constituent controls — that you choose to place on the UserControl.

Like Visual Basic forms, UserControl objects have code modules and visual designers, as shown in Figure 9.1. You place constituent controls on the UserControl object's designer, just as you would place controls on a form.

Figure 9.1 UserControl designer and code window

Like forms, user controls are stored in plain text files that contain the source code and property values of the UserControl and its constituent controls. Visual Basic uses the extension .ctl for these source files.

The relationship of .ctl files and ActiveX control projects to finished controls and .ocx files is shown in Figure 9.2.

Figure 9.2 ActiveX control projects are built into .ocx files

If a UserControl or its constituent controls use graphical elements which cannot be stored as plain text, such as bitmaps, Visual Basic stores those elements in a .ctx file with the same name you give to the .ctl file. This is analogous to the .frx files used to store graphical elements used in forms.

The .ctl and .ctx files completely define an ActiveX control's appearance and interface (properties, methods, and events). You can include .ctl files in any of the project types. "Two Ways to Package ActiveX Controls," later in this chapter, discusses this subject in depth.

Delegating to the UserControl and Constituent Controls that Compose Your ActiveX Control

Your ActiveX control is said to be composed of a UserControl and its constituent controls because each instance will actually contain those objects.

That is, whenever you place an instance of your ActiveX control on a form, a UserControl object is created, along with instances of any constituent controls you placed on the UserControl designer. These objects are encapsulated inside your control.

The UserControl object has an interface — that is, properties, methods, and events — of its own. The interface of your ActiveX control can delegate to the UserControl object's interface members, which are hidden from the user of your control by encapsulation.

That is, rather than writing your own code to implement a BackColor property, you can delegate to the UserControl object's BackColor property, and let it do all the work. In practice, this means that the BackColor property of your ActiveX control simply calls the BackColor property of the UserControl object.

In the same manner, you can piggy-back your control's Click event on the existing functionality of the UserControl object's Click event.

The interface for your ActiveX control can also delegate to the properties, methods, and events of the constituent controls you place on the UserControl designer, as discussed in "Exposing Properties of Constituent Controls," "Adding Methods to Controls," and "Exposing Events of Constituent Controls," later in this chapter.

For More Information For a discussion of what controls you can place on a UserControl designer, see "Controls You Can Use As Constituent Controls," later in this chapter.


Three Ways to Build ActiveX Controls

There are three models for control creation in Visual Basic. You can:

  • Author your own control from scratch.

  • Enhance a single existing control.

  • Assemble a new control from several existing controls.

The second and third models are similar, because in both cases you put constituent controls on a UserControl object. However, each of these models has its own special requirements.

Authoring a User-Drawn Control

Writing a control from scratch allows you to do anything you want with your control's appearance and interface. You simply put code into the Paint event to draw your control. If your control's appearance changes when it's clicked, your code does the drawing.

This is the model you should select if you're creating a new visual widget, such as a button that crumbles to dust and disappears when clicked.

For More Information Creating a user-drawn control is discussed further in "Drawing Your Control," later in this chapter.

Enhancing an Existing Control

Enhancing an existing control means putting an instance of the control on a UserControl designer and adding your own properties, methods, and events.

You have complete freedom in specifying the interface for your enhanced control. The properties, methods, and events of the control you start with will only be included in your interface if you decide to expose them.

"Exposing Properties of Constituent Controls," later in this chapter, describes how to do this manually, and how to make it easier by using the ActiveX Control Interface Wizard.

Enhancing the appearance of an existing control is more difficult than enhancing the interface, because the control you're enhancing already contains code to paint itself, and its paint behavior may depend on Windows messages or other events.

Experienced Windows programmers can subclass the constituent control using the AddressOf operator described in "Using the Windows API," in the Component Tools Guide. This allows some control over the control's appearance, but there is no way to alter the control's paint code.

It's easier to work with the control's built-in paint behavior, and instead enhance it by adding properties, methods, and events, or by intercepting and altering existing properties and methods. This is discussed further in "Drawing Your Control," later in this chapter.

Assembling a Control from Several Existing Controls

You can construct your control's appearance and interface quickly by assembling existing controls on a UserControl designer.

For example, the ShapeLabel control provided in the CtlPlus.vbg sample application, and discussed in the step-by-step procedures in "Creating an ActiveX Control," uses a Shape control to provide its visual background and a Label control to display its caption.

Figures 9.3 and 9.4 show how multiple constituent controls can contribute to the appearance and interface of an ActiveX control.

Figure 9.3 Constituent controls provide ShapeLabel's appearance

Constituent controls contribute to the appearance of an instance of your control by their mere presence on the UserControl designer. They contribute to your control's interface by delegation, as shown in Figure 9.4.

Figure 9.4 Constituent controls contribute to ShapeLabel's interface

For example, ShapeLabel's Caption property delegates to the Caption property of the constituent control lblCaption as shown in the following code fragment.

Public Property Get Caption() As String
   Caption = lblCaption.Caption
End Property

Public Property Let Caption(NewCaption As String)
   lblCaption.Caption = NewCaption
   PropertyChanged "Caption"
End Property

For More Information "Drawing Your Control" and "Exposing Properties of Constituent Controls," later in this chapter, discuss control assemblies in more depth. The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," later in this chapter.


Two Ways to Package ActiveX Controls

An ActiveX control created with Visual Basic is defined by a UserControl module. The source code you add to this module, to implement your ActiveX control, is stored in a .ctl file.

You can include UserControl modules in most Visual Basic project types, but only ActiveX control projects can provide controls to other applications. Controls in all other project types are private.

Thus there are two ways to package controls:

  • Public controls can only exist in ActiveX control projects. You make a control public by setting the Public property of the UserControl object to True.

    Public controls can be used by other applications, once the ActiveX control project has been compiled into a control component (.ocx file).

  • Private controls can exist in any project type. You make a control private by setting the Public property of the UserControl object to False.

    After the project is compiled, private controls cannot be used by other applications. They can be used only within the project in which they were compiled.

    If you attempt to set the Public property of a UserControl object to True, and the UserControl is not in an ActiveX control project, an error occurs.

If one of the controls in an ActiveX control project is meant to be used only as a constituent of other controls in the project, you can set the Public property of the UserControl to False. The control will then be available only to the controls of which it is a constituent part. Other applications will not be able to use it.

Note You cannot include UserControl modules in a project marked for unattended execution. If the Unattended Execution box is checked on the General tab of the Project Properties dialog box, the project cannot contain any user interface elements.

Important If you package your controls as a control component, be sure to set the description for each control. Some clients, such as Visual Basic, represent the entire .ocx file using the string you enter in the Project Description box of the Project Properties dialog box, but others display the browser strings for the individual controls. See "Providing Help and Browser Strings for Objects," in "Debugging, Testing, and Deploying Components."

Including Controls as Compiled Code vs. Source Code

If you create your controls as public classes in an ActiveX control project, you can distribute the compiled control component (.ocx file) with any application you create. When you use SetupWizard to create a setup program for an application in which you've used such a control, the compiled .ocx file will be included automatically.

You can also create a setup program for the control component itself, and distribute it to other developers. "Licensing Issues for Controls," later in this chapter, discusses the licensing support available for control components authored using Visual Basic.

Changing the Packaging

Once you've authored a control, you can easily change the way the control is packaged.

For example, if you have some private controls that are part of a Standard EXE project, and you want to allow other applications to use them, you can add the .ctl files to an ActiveX control project, and compile it into a distributable control component (.ocx file).

Source Code

Instead of including the compiled control component in your applications, you can simply add the .ctl file to the project for the application. When the application is compiled, the control is compiled into the executable.

The primary advantages of including a control as source code are:

  • There is no additional .ocx file to distribute.

  • You don't have to debug your control for all possible test cases. You only have to debug the features used by your application.

  • You don't have to worry about whether your application will work with future versions of the control, because the version your application uses is compiled in.

Note Some developers may argue that avoiding the additional .ocx file is not really an advantage. All Visual Basic applications require support files, and SetupWizard automatically includes them in your setup program, so you're not avoiding any extra work.

Of course, there's no such thing as a free lunch. There are also disadvantages to including controls as source code:

  • If you discover a bug in the control, you cannot simply distribute an updated .ocx file. You must recompile the entire application.

  • Multiple applications will require more disk space, because instead of sharing one copy of an .ocx file, each application includes all the code for the control.

  • Each time you use the source code in an application, there will be an opportunity to fix bugs or enhance the code. It may become difficult to keep track of which version of a control was used in which version of which application.

  • Sharing source code with other developers may be problematic. At the very least, it's likely to require more support effort than distributing a compiled component. In addition, you give up control and confidentiality of your source code.


Understanding Control Lifetime and Key Events

Designing ActiveX controls involves a radical shift in perspective. The key events you must respond to are different — for example, your life will revolve around the Resize event — and there's no such thing as QueryUnload. But that's just the beginning.

"Control Creation Terminology," earlier in this chapter, introduced the idea that a control is not a permanent fixture of a form. Indeed, design-time and run-time instances of your control will be created and destroyed constantly — when forms are opened and closed, and when you run the project.

Each time an instance of your ActiveX control is created or destroyed, the UserControl object it's based on is created or destroyed, along with all of its constituent controls. ("The UserControl Object," earlier in this chapter, explains the basis of all ActiveX controls created with Visual Basic.)

Consider, for example, a day in the life of the ShapeLabel control used in the step-by-step procedures in "Creating an ActiveX Control."

  1. The user creates an instance of ShapeLabel — by double-clicking on the Toolbox, or by opening a form on which an instance of ShapeLabel was previously placed.

  2. The constituent controls, a Shape and a Label, are created.

  3. The UserControl object is created, and the Shape and Label controls are sited on it.

  4. The UserControl_Initialize event procedure executes.

  5. The ShapeLabel control is sited on the form.

  6. If the user is placing a new ShapeLabel, the InitProperties event of the UserControl object occurs, and the control's default property values are set. If an existing form is being opened, the ReadProperties event occurs instead, and the control retrieves its saved property values.

  7. The UserControl_Resize event procedure executes, and the constituent controls are resized according to the size the user made the new control instance, or the size they were before the form was closed.

  8. The Show and Paint events occur. If there are no constituent controls, the UserControl object draws itself.

  9. The user presses f5 to run the project. Visual Basic closes the form.

  10. The UserControl object's WriteProperties event occurs, and the control's property values are saved to the in-memory copy of the .frm file.

  11. The control is unsited.

  12. The UserControl object's Terminate event occurs.

  13. The UserControl object and its constituent controls are destroyed.

And that's not the half of it. The run-time instance of the form is now created, along with a run-time instance of the ShapeLabel control. When the user closes the form and returns to design mode, the ShapeLabel is destroyed and re-created once again.

The rest of this topic explains the key events in a UserControl object's life, and provides reference lists of the events you receive in several important scenarios.

Key UserControl Events

The meanings of the key events in the life of a UserControl object are as follows:

  • The Initialize event occurs every time an instance of your control is created or re-created. It is always the first event in a control instance's lifetime.

  • The InitProperties event occurs only in a control instance's first incarnation, when an instance of the control is placed on a form. In this event, you set the initial values of the control's properties.

  • The ReadProperties event occurs the second time a control instance is created, and on all subsequent re-creations. In this event, you retrieve the control instance's property values from the in-memory copy of the .frm file belonging to the form the control was placed on.

  • The Resize event occurs every time a control instance is re-created, and every time it is resized — whether in design mode, by the developer of a form, or at run time, in code. If your UserControl object contains constituent controls, you arrange them in the event procedure for this event, thus providing your control's appearance.

  • The Paint event occurs whenever the container tells the control to draw itself. This can occur at any time, even before the control receives its Show event — for example, if a hidden form prints itself. For user-drawn controls, the Paint event is where you draw your control's appearance.

  • The WriteProperties event occurs when a design-time instance of your control is being destroyed, if at least one property value has changed. In this event, you save all the property values a developer has set for the control instance. The values are written to the in-memory copy of the .frm file.

  • The Terminate event occurs when the control is about to be destroyed.

In addition to the events listed above, the Show and Hide events may be important to your control. Show and Hide occur as indicated in Figure 9.5.

Figure 9.5 Show and Hide Events

In order to draw to the screen in Windows, any control must have a window, temporarily or permanently. Visual Basic ActiveX controls have permanent windows. Before a control has been sited on a form, its window is not on the container. The UserControl object receives Show and Hide events when the window is added and removed.

While the control's window is on the form, the UserControl receives a Hide event when the control's Visible property changes to False, and a Show event when it changes to True.

The UserControl object does not receive Hide and Show events if the form is hidden and then shown again, or if the form is minimized and then restored. The control's window remains on the form during these operations, and its Visible property doesn't change.

If the control is being shown in an internet browser, a Hide event occurs when the page is moved to the history list, and a Show event occurs if the user returns to the page.

Note If your control is used with earlier versions of Visual Basic, the UserControl object will not receive Show and Hide events at design time. This is because earlier versions of Visual Basic did not put any visible windows on a form at design time.

For More Information The topic "Life and Times of a UserControl Object," one of the step-by-step procedures in "Creating an ActiveX Control," demonstrates the key events in the life of a control and illustrates how often control instances are created and destroyed.

The Incarnation and Reincarnation of a Control Instance

Let's follow a control instance from its placement on a form, through subsequent development sessions, until it's compiled into an application. We'll assume the control was already developed and compiled into an .ocx file, before the curtain opens.

The scenarios that follow mention both Resize and Paint events. Which event you're interested in depends on the control creation model you're using, as discussed in "Three Ways to Build ActiveX Controls," earlier in this chapter.

If your control provides its appearance using constituent controls, you'll use the Resize event to size the constituent controls. If you're authoring a user-drawn control, on the other hand, you can ignore the Resize event and remarks about constituent controls. User-drawn controls draw their appearance in the Paint event. This is discussed in "Drawing Your Control," later in this chapter.

Note In all of these scenarios, the order and number of Resize and Paint events may vary.

The Control Instance is Placed on a Form

When you double-click a control's icon in the Toolbox, a design-time instance of the control is placed on the form you're designing. The following events occur in the UserControl object at the heart of the control instance:

Event | What gets done
Initialize | Constituent controls have been created, but the control has not been sited on the form.
InitProperties | The control instance sets default values for its properties. The control has been sited, so the Extender and AmbientProperties objects are available. This is the only time the instance will ever get this event.
Resize, Paint | The control instance adjusts the size of its constituent controls, if any, according to its default property settings. A user-drawn control draws itself.

The developer of the form can now see the control, and set its properties in the Properties window. After the developer does this, she may press f5 to run the project.

From Design Mode to Run Mode

When F5 is pressed, the control's design-time instance on the form is destroyed. When the form is loaded at run time, the control is recreated as a run-time instance.

Event | What gets done
WriteProperties | Before the design-time instance is destroyed, it has a chance to save property values to the in-memory copy of the .frm file.
Terminate | Constituent controls still exist, but the design-time control instance is no longer sited on the form. It's about to be destroyed.
Initialize | Constituent controls have been created, but the run-time control instance has not been sited on the form.
ReadProperties | The control instance reads the property values that were saved in the in-memory .frm file. The control has been sited on the run-time instance of the form, so the Extender and AmbientProperties objects are available.
Resize, Paint | The control instance adjusts the size of its constituent controls, if any, according to its current property settings. A user-drawn control draws itself.

The developer tests the form by clicking the control, or taking other actions that cause the control's properties, methods, and events to be exercised.

From Run Mode to Design Mode

Finally the developer closes the form and returns to design mode. The run-time instance of the control is destroyed, and a design-time instance is created:

Event | What gets done
Terminate | The run-time instance never gets a chance to save property settings. Changes to property values while the program was running are discarded.
Initialize | Design-time instances of constituent controls have been created, but the design-time control instance has not been sited on the form.
ReadProperties | The control reads the property values that were saved in the in-memory copy of the .frm file. The control has been sited on the design-time instance of the form, so the Extender and AmbientProperties objects are available.
Resize, Paint | The control instance adjusts the size of its constituent controls, if any, according to its saved property settings. A user-drawn control draws itself.

Closing the Form

If the developer doesn't need to work on the form any more, she may close it. Or it may be quitting time, and she may close the whole project. In either case, the control instance on the form is destroyed.

Event | What gets done
WriteProperties | Before the design-time instance is destroyed, it has a chance to save property values to the in-memory copy of the .frm file.
Terminate | Constituent controls still exist, but the control instance is no longer sited on the form. It's about to be destroyed.

Note In all of the scenarios above, the control instance has been saving its property values to the in-memory copy of the .frm file. If the developer chooses not to save the project before closing it, those property settings will be discarded.

Additional Scenarios

When the developer re-opens the project, and opens the form to work on it again, the control is reincarnated as a design-time instance. It receives Initialize, ReadProperties, Resize, Paint, and WriteProperties events.

Note A WriteProperties event? Yes, indeed. When the project is opened, Visual Basic creates an in-memory copy of the .frm file. As each control on the form is created, it gets a ReadProperties event to obtain its saved property values from the .frm file, and a WriteProperties event to write those property values to the in-memory copy of the .frm file.

Compiling the Project

When the project is compiled into an application or component, Visual Basic loads all the form files invisibly, one after another, in order to write the information they contain into the compiled file. A control instance gets the Initialize, ReadProperties, and WriteProperties events. The control's property settings are compiled into the finished executable.

Running the Compiled Program or Component

Whenever a user runs the installed application or component, and the form is loaded, the control receives Initialize, ReadProperties, and Resize events. When the form is unloaded, the control receives a Terminate event.

Controls on World Wide Web Pages

A control on an HTML page is specified using the HTML <OBJECT> and </OBJECT> tags. When the HTML is processed, the control is created and positioned. If the <OBJECT> tag includes any <PARAM NAME> attributes, the property values supplied with those attributes are passed to the control's ReadProperties event using the standard PropertyBag object, as discussed earlier in this topic.

Once the HTML page is active, the control's property values may also be set by scripts attached to events that occur on the page.

Note If there are no <PARAM NAME> attributes other than those that set extender properties, the control may receive an InitProperties event rather than a ReadProperties event. This behavior is dependent on browser implementation, and should not be relied on.

Events You Won't Get in a UserControl object

Some events you're familiar with from working with forms don't exist in a UserControl object. For example, there is no Activate or Deactivate event, because controls are not activated and deactivated the way forms are.

More striking is the absence of the familiar Load, Unload, and QueryUnload events. Load and Unload simply don't fit the UserControl lifestyle; unlike a form, a control instance isn't loaded at some point after it's created — when a UserControl object's Initialize event occurs, constituent controls have already been created.

The UserControl object's Initialize and ReadProperties events provide the functionality of a form's Load event. The main difference between the two is that when the Initialize event occurs, the control has not been sited on its container, so the container's Extender and AmbientProperties objects are not available. The control has been sited when ReadProperties occurs.

Note ReadProperties doesn't occur the first time a control instance is placed on a container — in that case the InitProperties occurs instead.

The UserControl event most like a form's Unload event is Terminate. The constituent controls still exist at this point, although you no longer have access to the container, because your control has been unsited.

The WriteProperties event cannot be used as an analog of Unload, because it occurs only at design time.

UserControl objects don't have QueryUnload events because controls are just parts of a form; it's not up to a control to decide whether or not the form that contains it should close. A control's duty is to destroy itself when it's told to.

Events Peculiar to UserControls

The GotFocus and LostFocus events of the UserControl object notify user-drawn controls when they should show or stop showing a focus rectangle. These events should not be forwarded to the user of your control, because the container is responsible for focus events.

If your UserControl has constituent controls that can receive focus, the EnterFocus event will occur when the first constituent control receives the focus, and the ExitFocus event will occur when focus leaves the last constituent control. See "How to Handle Focus in your Control," later in this chapter.

If you have allowed developers to set access keys for your control, the AccessKeyPress event occurs whenever a user presses an access key. See "Allowing Developers to Set Access Keys for Your Control," later in this chapter. The AccessKeyPress event can also occur if your control is a default button or cancel button. This is discussed in "Allowing Your Control to be a Default or Cancel Button," later in this chapter.

The AmbientChanged event occurs whenever an Ambient property changes on the container your control has been placed on. See "Using the AmbientProperties Object to Stay Consistent with the Container," later in this chapter.


Interacting with the Container

As explained in "Control Creation Terminology," earlier in this chapter, instances of your control never exist by themselves. They are always placed on container objects, such as Visual Basic forms.

Container objects supply additional properties, methods, and events that appear to the user to be part of your control. This is discussed in the related topic, "Understanding the Container's Extender Object." You can use the Parent property of the Extender object to access the properties and methods of the container your control has been placed on.

You can also obtain information about the container through the UserControl object's Ambient property. The object returned by this property offers hints for property settings, such as BackColor, that can make your control's appearance consistent with that of its container. The AmbientProperties object is discussed in "Using the AmbientProperties Object to Stay Consistent with the Container."

Note The AmbientProperties and Extender objects are not available until your control has been sited on the container. Thus they are not available in the UserControl object's Initialize event. When the InitProperties or ReadProperties event occurs, the control instance has been sited.

All Containers are Not Created Equal

A consequence of your control's dependence on container objects is that some features may not be available in all containers. Many ActiveX control features require support from the container a control is placed on, and will be disabled if the container doesn't provide the required support.

The following features are supported by Visual Basic forms, but may not be supported by all containers:

  • Transparent control background, discussed in "Giving Your Control a Transparent Background," later in this chapter.

  • The ControlContainer property, discussed in "Allowing Developers to Put Controls on Your Control," later in this chapter.

  • Alignable controls, discussed in "Making Your Control Align to the Edges of Forms," later in this chapter.

  • Modeless dialog boxes your control may show.

Related Topics

  • Understanding the Container's Extender Object The developer using your control sees more properties, methods, and events than the ones you defined. The extensions are provided by the container your control is placed on.

  • Using the AmbientProperties Object to Stay Consistent with the Container The container provides information your control can use to synchronize itself with the container.


Understanding the Container's Extender Object

Understanding the Container’s Extender Object

When you view an instance of your control in the Properties window, you'll see a number of properties you didn't author. These extender properties are provided by the container your control is placed on, but they appear to be a seamless extension of your control, as shown in Figure 9.6.

Figure 9.6 Extender properties, methods, and events are provided by the container

A UserControl object can access extender properties through its Extender object. For example, the ShapeLabel control in "Creating an ActiveX Control" uses the following code to initialize its Caption property:

Private Sub UserControl_InitProperties()
   ' Let the starting value for the Caption
   '   property be the default Name of this
   '   instance of ShapeLabel.
   Caption = Extender.Name
End Sub

Extender properties are provided for the developer who uses your control. Generally speaking, the author of a control should not attempt to set them with code in the UserControl. For example, it's up to the developer to decide where a particular instance of your control should be located (Top and Left properties), or what icon it should use when dragged.

Extender Properties are Late Bound

When you compile your control component, Visual Basic has no way of knowing what kind of container it may be placed on. Therefore references to Extender properties will always be late bound.

Standard Extender Properties

The ActiveX control specification lists the following properties that all containers should provide:

Property | Type | Access | Meaning
Name | String | R | The name the user assigns to the control instance.
Visible | Boolean | RW | Indicates whether the control is visible.
Parent | Object | R | Returns the object which contains the control, such as a Visual Basic form.
Cancel | Boolean | R | True if the control is the cancel button for the container.
Default | Boolean | R | True if the control is the default button for the container.

Although it is highly recommended that containers implement these properties, containers do not have to do so. Thus you should always use error trapping when referring to properties of the Extender object in your code, even standard properties.

Many containers provide additional extender properties, such as Left, Top, Width, and Height properties.

Note If you wish your control to be invisible at run time, set the UserControl object's InvisibleAtRuntime property to True, as discussed in "Making Your Control Invisible at Run Time," later in this chapter. Do not use the Extender object's Visible property for this purpose.

Container-Specific Controls

If you design your control so that it requires certain Extender properties, your control will not work in containers that don't provide those properties. There is nothing wrong with building such container-specific controls, except that the potential market for them is smaller.

If you are creating a control designed to address a limitation of a particular container, such considerations may not matter to you. However, conscientious use of error trapping will prevent your control from causing unfortunate accidents if it is placed on containers it was not specifically designed for.

Working with Container Limitations

Visual Basic provides a rich set of extender properties and events, listed in the topic for the Extender object. Many containers provide only a limited subset of these.

In general, Extender properties, methods, and events are not the concern of the control author. Many Extender properties, such as Top and Left, or WhatsThisHelpID, cannot be implemented by a control, because the container must provide the underpinnings these properties require.

Collisions Between Control and Extender Properties

If an instance of your control is placed on a container that has an extender property with the same name as a property of your control, the user will see the extender property.

For example, suppose you gave your control a Tag property. When an instance of your control is placed on a Visual Basic form, a Tag property is supplied by the form's Extender object. If your control is called ShapeLabel, the user might write the following code:

ShapeLabel1.Tag = "Triceratops"

The code above stores the string "Triceratops" in the Tag property provided by the Visual Basic form's Extender object. If an instance of your control is placed on a container whose Extender object doesn't supply a Tag property, the same code will store the string in the Tag property you implemented.

In order to access the Tag property of your control on a Visual Basic form, the user could employ another Extender object property, as shown in the following code fragment:

ShapeLabel1.Object.Tag = "Triceratops"

The Object property returns a reference to your control's interface just as you defined it, without any extender properties.


Using the AmbientProperties Object to Stay Consistent with the Container

Containers provide ambient properties to give controls hints about how they can best display themselves in the container. For example, the ambient BackColor property tells a control what color to set its own BackColor property to in order to blend in with the container.

Visual Basic makes ambient properties available to your ActiveX control through an AmbientProperties object. The Ambient property of the UserControl object returns a reference to the AmbientProperties object.

The AmbientProperties object provided by Visual Basic contains all of the standard ambient properties defined by the ActiveX Controls Standard, whether or not they are actually provided by the container your control instance was placed on.

This means that you can safely access any of the properties of the AmbientProperties object visible in the Object Browser. If you access an ambient property not provided by the container, the AmbientProperties object returns a default value, as listed in the topics for the AmbientProperties object properties.

Containers That Provide Additional Ambient Properties

Control containers may define their own ambient properties. These container-specific ambient properties are not visible in the Object Browser, because they are not in Visual Basic's type library. You can learn about such properties in the documentation for a container, and access them as if they were properties of the AmbientProperties object.

Because these properties are not in the type library, Visual Basic cannot verify their existence at compile time. Therefore you should always use error handling when working with Ambient properties.

Another consequence of the lack of type library information is that calls to container-specific ambient properties are always late-bound. By contrast, calls to standard ambient properties are early-bound.

Important Ambient Properties

You can ignore many of the standard ambient properties. In a Visual Basic ActiveX control, you can ignore the MessageReflect, ScaleUnits, ShowGrabHandles, ShowHatching, SupportsMnemonics, and UIDead properties of the AmbientProperties object.

Ambient properties you should be aware of are listed below.

UserMode

The most important property of the AmbientProperties object is UserMode, which allows an instance of your control to determine whether it's executing at design time (UserMode = False) or at run time. Use of this property is discussed in "Creating Design-Time-Only or Run-Time-Only Properties," later in this chapter.

Tip To remember the meaning of UserMode, recall that at design time the person working with your control is a developer, rather than an end user. Thus the control is not in "user" mode, so UserMode = False.

LocaleID

If you're developing a control for international consumption, you can use the LocaleID ambient property to determine the locale. Use of this property is discussed in "Localizing Your Control," later in this chapter.

DisplayName

Include the value of the DisplayName property in errors your control raises at design-time, so the developer using your control can identify the control instance that is the source of the error.

ForeColor, BackColor, Font, and TextAlign

These properties are hints your control can use to make its appearance match that of the container. For example, in the InitProperties event, which each instance of your UserControl receives when it is first placed on a container, you can set your control's ForeColor, BackColor, Font, and TextAlign to the values provided by the ambient properties. This is a highly recommended practice.

You could also give your control properties which the user could use to keep a control instance in sync with the container. For example, you might provide a MatchFormBackColor property; setting this property to True would cause your control's BackColor property always to match the value of the BackColor property of the AmbientProperties object. You can provide this kind of functionality using the AmbientChanged event, discussed below.

DisplayAsDefault

For user-drawn controls, this property tells you whether your control is the default button for the container, so you can supply the extra-heavy border that identifies the default button for the end user.

If you didn't set your control up to be a default button, you can ignore this property. See "Allowing Your Control to be a Default or Cancel Button," in this chapter.

For More Information See "User-Drawn Controls," in this chapter. Also, a full list of the properties of the AmbientProperties object is available in the Language Reference.

The AmbientChanged Event

If your control's appearance or behavior is affected by changes to any of the properties of the AmbientProperties object, you can place code to handle the change in the UserControl_AmbientChanged event procedure.

The argument of the AmbientChanged event procedure is a string containing the name of the property that changed.

Important If you're authoring controls for international use, you should always handle the AmbientChanged event for the LocaleID property. See "Localizing Controls," later in this chapter.

Note If an instance of your control is placed on a Visual Basic form, and the FontTransparent property of the form is changed, the AmbientChanged event will not be raised.


Visual Basic ActiveX Control Features

Visual Basic allows you to author full-featured ActiveX controls. The topics included in this section explain many of the features of the UserControl object that enable ActiveX control capabilities.

You can read about other potential ActiveX control capabilities in "Adding Properties to Controls," "Adding Methods to Controls," and "Raising Events from Controls."

You can also add property pages to your control, as discussed in "Creating Property Pages for ActiveX Controls."

  • How to Handle Focus in Your Control

  • Controls You Can Use As Constituent Controls

  • Object Models for Controls

  • Allowing Developers to Put Controls on Your Control

  • Allowing Your Control to be Enabled and Disabled

  • Adding a Font Object to Your User Control

  • Giving Your Control a Transparent Background

  • Creating Lightweight Controls

  • Allowing Developers to Set Access Keys for Your Control

  • Making Your Control Align to the Edges of Forms

  • Making Your Control Invisible at Run Time

  • Adding an AboutBox to Your Control

  • Providing a Toolbox Bitmap for Your Control

  • Allowing Your Control to be a Default or Cancel Button

  • Adding Internet Features to Controls

  • Designing Controls for Use With HTML

  • Binding a Control to a Data Source

  • Allowing Developers to Edit Your Control at Design Time

Many of the features described in this section are also demonstrated in the CtlPlus.vbg sample application which is listed in the Samples directory.


How to Handle Focus in Your Control

The way you handle focus for your control depends on which model you're using to develop your control. Models for building ActiveX controls are discussed in "Three Ways to Build ActiveX Controls," earlier in this chapter.

Important If you're authoring a control that can be a container for other controls, as described in "Allowing Developers to Put Controls on Your Control," note that the material in this topic does not apply to controls a developer places on an instance of your control. These contained controls will receive focus independent of your control and its constituent controls.

User-Drawn Controls

If you're authoring a user-drawn control, there won't be any constituent controls on your UserControl. If you don't want your control to be able to receive the focus, set the CanGetFocus property of the UserControl object to False. CanGetFocus is True by default.

If your user-drawn control can receive the focus, the UserControl object will receive GotFocus and LostFocus events when your control receives and loses the focus. A user-drawn control is responsible for drawing its own focus rectangle when it has the focus, as described in "User-Drawn Controls," in this chapter.

This is the only function your UserControl's GotFocus and LostFocus events need to fulfill for a user-drawn control. You don't need to raise GotFocus or LostFocus events for the user of your control, because the container's extender provides these events if the CanGetFocus property is True.

Note The UserControl object of a user-drawn control will also receive a EnterFocus event prior to GotFocus, and an ExitFocus event after LostFocus. You don't need to put any code in the event procedures of these event, and in fact it is recommended that you not do so.

User-drawn controls can respond to access keys, as described later in this topic.

Controls That Use Constituent Controls

If you're authoring a control that enhances a single constituent control, or is an assembly of constituent controls, your UserControl object will be unable to receive the focus, regardless of the setting of the CanGetFocus property, unless none of its constituent controls can receive the focus.

If no constituent controls can receive the focus, and CanGetFocus is True, then your UserControl object will receive the same events a user-drawn control receives. The only thing you need to do with these events is provide a visual indication that your control has the focus.

How Constituent Controls Are Affected by CanGetFocus

If your control contains at least one constituent control that can receive the focus, the CanGetFocus property of the UserControl object must be set to True. If you attempt to set CanGetFocus to False on a UserControl that has constituent controls that can receive focus, an error will occur.

Visual Basic will not allow a constituent control that can receive focus to be placed on a UserControl whose CanGetFocus property is False: Icons of controls that can receive focus are disabled in the Toolbox when the UserControl's design window is active.

EnterFocus and ExitFocus

When the focus moves from outside your control to any of your control's constituent controls, the UserControl object will receive an EnterFocus event. The GotFocus event for the constituent control that receives the focus will be raised after the UserControl_EnterFocus event procedure.

As long as the focus remains within your control, the UserControl object's focus-related events will not be raised. As the focus moves from one constituent control to another, however, the appropriate GotFocus and LostFocus events of the constituent controls will be raised.

When the focus moves back outside your control, the last constituent control that had the focus will receive its LostFocus event. When the event procedure returns, the UserControl object will receive its ExitFocus event.

You can use the EnterFocus event to change which constituent control receives the focus. You may wish to do this in order to restore the focus to the constituent control that last had it, rather than simply allowing the first constituent control in your UserControl's tab order to receive the focus, which is the default behavior.

Tip If your control is complex — as for example an Address control with multiple constituent controls — you may be tempted to validate the data in the ExitFocus event. Don't. The user of your control can put code in the Validate event of the user control to handle data validation as they see fit. If it's absolutely necessary to validate data inside the control, use the Validate events in combination with the CausesValidation properties of the constituent controls. Be aware that you can't always count on the Validate event for constituent controls, as is discussed in "Handling the Validate Event" below.

Tip Generally speaking, it's not a good idea to use MsgBox when you're debugging focus-related events, because the message box immediately grabs the focus. It's a very bad idea to use MsgBox in EnterFocus and ExitFocus events. Use Debug.Print instead.

Receiving Focus via Access Keys

Avoid hard coding access keys for your control's constituent controls, because access keys permanently assigned to your control in this fashion will limit a user's freedom to choose access keys for her form. In addition, two instances of your control on the same form will have access key conflicts.

"Allowing Developers to Set Access Keys for Your Control," later in this chapter, discusses how you can give the user of your control the ability to set access keys on instances of your control.

Forwarding Focus to the Next Control in the Tab Order

If your control cannot receive the focus itself, and has no constituent controls that can receive the focus, you can give your control the same behavior displayed by Label controls. That is, when the access key for your control is pressed, the focus is forwarded to the next control in the tab order.

To enable this behavior, set the ForwardFocus property of the UserControl object to True.

Handling the Validate Event

The Validate event and CausesValidation property for a user control behave exactly like they do for any other control, but the behavior of Validate and CausesValidation for constituent controls may not yield the expected results. Let's review the standard behavior. When a control loses focus, its Validation event is fired before its LostFocus event — but only if the control about to receive the focus has its CausesValidation property set to True. This allows you to handle data validation before the control loses focus.

A user control exposes a Validate event and, via the Extender object, exposes a CausesValidation property. Code in the Validate event is executed when the focus is shifted from the user control to any control that had its CausesValidation property set to True; setting the CausesValidation property of the user control to True will enable the Validation event for any control passing focus to the user control.

The Validate event and CausesValidation property for constituent controls work as expected as long as the focus remains inside the user control. When the focus is shifted outside of the user control, the Validate event for the constituent control isn't fired. For that reason, it's best to avoid trying to handle validation within a user control.


Controls You Can Use As Constituent Controls

You can place any of the controls supplied with Visual Basic on a UserControl, with the exception of the OLE container control.

Any ActiveX control you've purchased, or any control written to the older OLE specification, can be placed on a UserControl.

As long as you're authoring a control for your own use, that's all you need to know. However, if you're going to distribute your control to others, even if you're giving it away, you need to consider distribution and licensing issues.

Note Toolbox objects other than controls, such as insertable objects — for example, Microsoft Excel Charts — cannot be placed on UserControl objects.

The Easy Part — UserControl and Intrinsics

The UserControl object and the Visual Basic intrinsic controls are created by the Visual Basic run-time DLL. Anyone who installs your .ocx file will automatically get a copy of the run-time DLL and support files, so if you author your controls using just the UserControl and intrinsic controls, you have no further licensing or distribution issues to worry about.

The intrinsic controls include: PictureBox, Label, TextBox, Frame, CommandButton, CheckBox, OptionButton, ComboBox, ListBox, HScrollBar, VScrollBar, Timer, DriveListBox, DirListBox, FileListBox, Shape, Line, Image, and Data.

ActiveX controls included with the Professional Edition of Visual Basic are subject to licensing rules, as explained below.

Note The Microsoft User Forms in Microsoft Office applications include a set of controls similar to the intrinsic controls. These controls may be used on a UserControl; however, you must distribute the support DLL for Microsoft User Forms in your Setup program.

Distributing Constituent Controls

An instance of your control is composed of a UserControl object and its constituent controls, as explained in "The UserControl Object," earlier in this chapter. In order to add an instance of your control to a form, a developer must be able to create these objects.

SetupWizard makes this task easy. When you create a Setup program for your .ocx file, SetupWizard includes all the .ocx files for the constituent controls, along with the Visual Basic run-time DLL and any necessary support files.

When a developer runs your Setup program, the .ocx files that provide the constituent controls are installed on his computer. The only other thing he needs to worry about is whether he has the legal right to use them.

If none of the constituent controls require a license, the developer is set. However, if you used controls you purchased, or any of the ActiveX controls included with Visual Basic, Professional Edition, there are licensing requirements to be met.

Licensing Constituent Controls

When you purchase a control, you generally acquire the right to distribute instances of that control royalty-free as part any application you create. However, such license agreements do not give you the right to sell or give away the control to other developers — which is what you're doing when you use it as a constituent control.

So the rule is: In order to use your control, a developer must have licenses for all the licensed controls you've used as constituent controls.

Most ActiveX Controls Included with Visual Basic Are Exempted

The licensing rule stated above applies to the DBGrid control. Any of the other ActiveX controls included with the Professional Edition of Visual Basic can be used as constituent controls of a control you intend to distribute to other developers.

For example, if you use the TreeView control as a constituent control, anyone who is licensed to use your control is licensed to use TreeView as part of your control. (If you didn't include licensing support when you made your .ocx, this means that anyone can use your control. See "Licensing Issues for Controls," later in this chapter.)

Important Your license agreement for Visual Basic states that in order to distribute any redistributables included with Visual Basic, the software you author must add significant and primary functionality. This means that a control that uses TreeView as a constituent control must do significantly more than TreeView does by itself. Consult your Microsoft License Agreement for details.

For more information about how to use ActiveX controls included with the Professional and Enterprise Editions, see "Using ActiveX Controls."

Distribution and Licensing Examples

Applying the rule yields the following examples.

ActiveX Controls Included with Visual Basic

Suppose you author some controls using some of the ActiveX controls included with Visual Basic, Professional Edition. SetupWizard adds the necessary .ocx files to your Setup program.

As long as you didn't use the DBGrid control, anyone who runs SetupWizard can use your controls. That's not always the case if you used DBGrid as a constituent control, as these scenarios show:

  • A developer who has a copy of Visual Basic, Professional Edition buys your .ocx and installs it. She already has the supporting .ocx files for DBGrid, and the license for it, so she has everything she needs.

  • A student who has a copy of Visual Basic, Learning Edition buys your control and installs it. He now has the supporting .ocx file for DBGrid, but doesn't have the license to use it.

  • A stock market analyst who has a copy of Microsoft Excel buys your control and installs it. She now has the supporting .ocx files for DBGrid, but doesn't have the license to use it.

ActiveX Controls You've Purchased

Suppose you purchase MegaDino.ocx from Late Cretaceous Computing, and use the Tyrannosaur and Velociraptor controls from this .ocx to develop your own UltimatePredator control. You package this control in UPred.ocx, and you give it away.

Anyone to whom you give a copy of UPred.ocx must have purchased and installed MegaDino.ocx in order to use the UltimatePredator control legally. This is true regardless of the development software they're using.

In fact, if the creators of MegaDino.ocx and DinoRama.ocx used the standard registry key licensing scheme, people to whom you give UPred.ocx will be unable to use the UltimatePredator control unless they have MegaDino.ocx installed.

Shareware Controls

Suppose you author your control using a shareware control.

If you sell your control component (.ocx file), the purchaser must also pay the author of the shareware control the appropriate license fee.

If you distribute your control component as shareware, a person who wants to use it must pay the appropriate license fees to you and to the author of the shareware control you used.

Constituent Controls and the Internet

If you want people to be able to use your control on World Wide Web pages, remember that the rule for Web servers is exactly the same as the rule for developers. That is, in order to use your control, a Web server must have licenses for all the licensed controls you've used as constituent controls.

For More Information Licensing issues, including how to add licensing support for the controls you author, how licensing support works, and the mechanism for using licensed controls with the World Wide Web are discussed in "Licensing Issues for Controls," later in this chapter.


Object Models for Controls

Complex controls such as TreeView and Toolbar provide run-time access to their functionality through objects. For example, the TreeView control has a Nodes collection containing Node objects that represent the items in the hierarchy the TreeView control displays. Users can create new nodes using the Add method of the Nodes collection.

Objects like Node and Nodes are called dependent objects. Dependent objects exist only as a part of some other object, as Node objects are always part of a TreeView control. They cannot be created independently.

You can provide dependent objects like Node and Nodes by including class modules in your ActiveX control project and organizing them into an object model. Object models can be as simple as the Nodes collection with its Node objects, or arbitrarily complex.

Important Control components can only provide dependent objects. They cannot provide objects that can be independently created, using the New operator or the CreateObject function.

For More Information Dependent objects are discussed in "Instancing for Classes Provided by ActiveX Components," in "General Principles of Component Design." Also, you can read about object models in "Organizing Objects: The Object Model," which is also in "General Principles of Component Design."

Some design considerations for collections in controls are discussed in "Creating Robust Controls," later in this chapter. More information on robust techniques for using objects can be found in "Private Communications Between Your Objects," in "General Principles of Component Design."

Classes, class modules, and objects are discussed in "Programming with Objects," in the Visual Basic Programmer's Guide.


Allowing Developers to Put Controls on Your Control

Some controls can act as containers for other controls. For example, if you place controls on a Visual Basic PictureBox control, all of the controls move when you move the PictureBox. Visual Basic users take advantage of this capability to group controls, produce scrollable pictures, and so on.

You can allow developers to place controls on your ActiveX control by setting the ControlContainer property of the UserControl object to True.

Note Lightweight user controls cannot act as control containers. If the Windowless property of a UserControl object is set to True, the ControlContainer property will be ignored. Lightweight user controls are discussed in detail in "Creating Lightweight Controls" later in this chapter.

Controls a developer places on an instance of your ActiveX control can be accessed using the ContainedControls collection of the UserControl object. You can use this collection at either design time or run time.

The ContainedControls Collection vs. the Controls Collection

The ContainedControls collection is different from the Controls collection, which contains only the constituent controls you have used in designing your ActiveX control. This is illustrated in Figure 9.7, which supposes that the ShapeLabel control's ControlContainer property is True.

Figure 9.7 The Controls and ContainedControls collections

Availability of the ContainedControls Collection

You cannot access the ContainedControls collection in the Initialize event of your UserControl object. Support for the ControlContainer feature is provided by the object your control is placed on, so your control must be sited on the container object before ContainedControls is available. When the UserControl object receives its ReadProperties event, siting has occurred.

Once your control is sited, and support for the ControlContainer feature is present, the ContainedControls collection may not immediately contain references to the controls a developer has placed on your control. For example, if your control is on a Visual Basic form, the Count property of the ContainedControls collection will be zero until after the UserControl_ReadProperties event procedure has executed.

Performance Impact of ControlContainer

There is extra overhead required to allow a developer to place controls on instances of your ActiveX control. Clipping must be done for the contained controls, which must appear on top of all the constituent controls in your UserControl, and of course the ContainedControls collection must be maintained.

In other words, controls that serve as containers for other controls are heavyweight controls.

For best performance of your controls, you should set ContainedControls to True only if it makes sense for a particular control. For example, it doesn't make much sense for a control assembly like an Address Control to be a container for other controls.

Support for ControlContainer

ControlContainer support will not work for every container your control may be placed on. Visual Basic forms support the ISimpleFrame interface that enables the ControlContainer feature, so your control can always support this capability on a Visual Basic form.

If an instance of your control is placed on a container that is not aware of ISimpleFrame, ControlContainer support will be disabled. Your control will continue to work correctly in all other ways, but developers will be unable to place other controls on an instance of your control.

In order for the ContainedControls collection to be available, an ISimpleFrame-aware container must implement the IVBGetControls interface. Calls to the collection will cause errors if the container does not implement this interface, so it's a good idea to use error handling when you access the collection.

Note Controls a developer places on a container with a transparent background are not visible. If you want your control to be used as a control container by developers, don't give it a transparent background.


Allowing Your Control to be Enabled and Disabled

The Enabled property is an odd beast. It's an extender property, but the Extender object doesn't provide it unless your control has an Enabled property of its own, with the correct procedure ID. If the extender's Enabled property isn't present, your control will not display the same enabled/disabled behavior as other controls.

Your property should delegate to the Enabled property of the UserControl object, as shown in the following code sample:

Public Property Get Enabled() As Boolean
   Enabled = UserControl.Enabled
End Property

Public Property Let Enabled(ByVal NewValue As Boolean)
   UserControl.Enabled = NewValue
   PropertyChanged "Enabled"
End Property

Add this code to the code window of the UserControl your ActiveX control is based on, as discussed in "Adding Properties to Controls," later in this chapter.

Note You can easily add the Enabled property by using the ActiveX Control Interface Wizard to create the interface for your control. The wizard includes the Enabled property in its list of recommended properties.

Notice that the Enabled property of the UserControl object is qualified by the object's class name (UserControl). The class name can be used to distinguish properties and methods of the UserControl object from members of your ActiveX control which have the same names.

The Enabled property of the UserControl object acts much like the Enabled property of a form, enabling and disabling the UserControl and all of its constituent controls.

Note The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," later in this chapter.

Assigning the Procedure ID for the Enabled Property

In order for your Enabled property to work correctly, you need to assign it the Enabled procedure ID. Procedure IDs are discussed in "Properties You Should Provide," later in this chapter.

To assign the procedure ID for the Enabled property

  1. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  2. In the Name box, select your Enabled procedure.

  3. Click Advanced to expand the Procedure Attributes dialog box.

  4. In the Procedure ID box, select Enabled to give the property the correct identifier.

When you give your Enabled property the correct procedure ID, the container's Extender object shadows it with its own Enabled property; when the user sets the extender property, the container sets your Enabled property.

The reason for this odd arrangement is to ensure consistent Windows behavior. When a form is disabled, it's supposed to disable all of its controls, but the controls are supposed to continue to paint themselves as if they were enabled.

A Visual Basic form conforms to this behavior by tricking its controls. It sets the Extender object's Enabled property to False for all of its controls, without calling the Enabled properties of the controls. The controls think they're enabled, and paint themselves so, but in code they appear to be disabled.

If your control has an Enabled property without the Enabled procedure ID, it will remain enabled in code while all the controls around it are disabled. You can see this by putting a command button and a control of your own on a form, and adding the following code:

Private Sub Command1_Click()
   Form1.Enabled = False
   Debug.Print Command1.Enabled
   Debug.Print MyControl1.Enabled
End Sub

Run the program before and after assigning the Enabled procedure ID to your control's Enabled property. In the first case, you'll see that the command button's Enabled property returns False, while your control's Enabled property returns True. After the procedure ID is assigned, both controls will return False.

Correct Behavior for the Enabled Property

You should avoid setting the UserControl's Enabled property, except in your control's Enabled property. The reason for this is that the container is responsible for enabling and disabling the controls it contains. It's rude for a control to tamper with properties the user is supposedly in control of.

Painting a User-Drawn Control's Disabled State

When you author a user-drawn control, you have to provide your own representation of your control's disabled state. If you have implemented an Enabled property as shown above, you can determine when you need to do this by testing the value of UserControl.Enabled in the UserControl_Paint event procedure.

For More Information See "User-Drawn Controls," later in this chapter.


Adding a Font Object to Your User Control

Although a UserControl object has a Font property, there are situations where you will need to implement a Font object of your own. For example, if you want to allow the end user of your control to select a font at run time, the standard Font property won't work.

Even though the Font property is exposed and can be set in the Properties window at design time or via code at run time, the font displayed on the user control doesn't change. This is because the control never receives notification that the property has changed; consequently, it never fires its Paint event.

To resolve this problem, you need to add your own StdFont object to the control. The StdFont object is exactly the same as the UserControl Font with one exception — it provides a FontChanged event. You need to declare it using the WithEvents keyword in order to expose the FontChanged event:

Private WithEvents mFont as StdFont

In the Initialize event of your user control, the following code creates an instance of the StdFont object and assigns it to the User Control's Font property:

Private Sub UserControl_Initialize()
   Set mFont = New StdFont
   Set UserControl.Font = mFont
End Sub

To expose your Font to the outside world, you'll need a Property Let / Property Set pair:

Public Property Get Font() as StdFont
   Set Font = mFont
End Property

Public Property Set Font (mnewFont as stdFont)
   With mFont
      .Bold = mnewFont.Bold
      .Italic = mnewFont.Italic
      .Name = mnewFont.Name
      .Size = mnewFont.Size
   End With
   PropertyChanged "Font"
End Property

Notice that this code uses With to set each property of the StdFont object individually: Simply assigning mnewFont to mFont would only change the default Name property.

You'll also need to add code in the FontChanged event to reassign the StdFont object and to force a Paint event:

Private Sub mFont_FontChanged(ByVal PropertyName As String)
   Set UserControl.Font = mFont
   Refresh
End Sub

Finally, in the Paint event you can add code to display text. The text will be displayed in the newly selected font.

Private Sub UserControl_Paint()
   Cls      ' Clear the display
   Print "Hello"
End Sub

Now the Font property of your user control can be set at either design time or at run time and the changes will immediately be visible.


Giving Your Control a Transparent Background

Setting the BackStyle property of the UserControl object to Transparent allows whatever is behind your control to be seen, in between the constituent controls on your UserControl's surface. When the BackStyle property is set to Transparent, the BackColor and Picture properties of the UserControl are ignored.

Bitmaps with Transparent Backgrounds

If you assign a bitmap to the UserControl's MaskPicture property, and set the MaskColor property of the UserControl equal to the background color of the bitmap, Visual Basic will clip around the parts of the bitmap that are not equal to the MaskColor — even if those parts are not contiguous.

You can draw on the visible surface of the Bitmap using the graphics methods of the UserControl object, such as Line, Circle, and PaintPicture. Visual Basic will clip your drawing to the visible parts of the bitmap.

Note This feature is supported for image-type bitmaps, such as BMP, GIF, JPEG, and DIB. It is not supported for Windows metafiles, icons, or cursors.

Labels with Transparent BackStyle

If one of the constituent controls on the UserControl is a Label whose BackStyle property has also been set to Transparent, and whose Font property specifies a TrueType font, Visual Basic will clip around the font. In addition, mouse clicks that fall in the spaces between letters will be passed through to the container.

Performance Considerations

Setting BackStyle to Transparent may affect the performance of your control. Visual Basic must do a great deal of clipping to make the background show through correctly if your control uses:

  • A large number of constituent controls.

  • A complex bitmap.

  • A Label control with a transparent background, a TrueType font, and a large amount of text.

To improve the performance of your control, you may be able to create a lightweight user control. The rules for creating controls that are both lightweight and transparent differ somewhat from regular controls. Lightweight user controls are discussed in depth in "Creating Lightweight Controls," later in this chapter.

Note Controls a developer places on a container with a transparent background are not visible. If you want your control to be used as a control container by developers, don't give it a transparent background.


Creating Lightweight Controls

Lightweight controls, sometimes referred to as windowless controls, differ from regular controls in one significant way: They don't have a window handle (hWnd property). Because of this they use fewer system resources, making them ideal for Internet applications, distributed applications, or any application where system resources may be a limiting factor. Examples of lightweight controls include the Label control and the Image control.

When designing a user control, you should consider creating a lightweight control unless your requirements meet one or more of the following criteria:

  • Your control will contain constituent controls for which lightweight versions aren't available. A lightweight control can only contain other lightweight controls.

  • Your control will act as a design-time container for other controls. Because you can't predict what kind of controls a developer may place on your control, it can't be a lightweight control.

  • Your control needs to make calls to the Windows API that require a window handle (hWnd) as an argument. A lightweight control doesn't have an hWnd property; it returns the hWnd of its container, which may not produce the desired results.

  • Your control needs to implement the BorderStyle property or the EditAtDesignTime property. These properties are disabled when the Windowless property is set to True.

You create a lightweight user control by setting the Windowless property to True at design time. Lightweight user controls can only contain other lightweight controls: If you attempt to place a windowed control on a lightweight user control, you will get a design-time error. The same is true if you attempt to set the Windowless property to True on a user control that already contains windowed controls.

Not all containers support lightweight controls. When your lightweight user control runs in a host that doesn't support it, it will automatically run in windowed mode. The system will assign it an hWnd on the fly. Hosts that are known to support lightweight controls include Visual Basic 4.0 or later, Internet Explorer 4.0 or later, and Visual Basic for Applications.

Creating Lightweight Controls with Transparent Backgrounds

Lightweight controls expose several new properties and a new event that are useful in creating controls with transparent backgrounds. With a windowed control, the MaskPicture and MaskColor properties are used to determine if a mouse click occurs within a valid area of the control's window; if not, the event is routed to the form. Because a lightweight control doesn't have a window, it's up to the control to determine what should receive the click event. The HitTest event provides notification when the cursor is over your control; you control the behavior of the HitTest event by setting a combination of several properties.

Control Regions

To understand how a lightweight control manages hit testing, you must first be familiar with the possible regions of a user control. A control has four possible regions:

  • Mask region – the painted (nontransparent) area on the control, including the union of all contained controls or any areas on a user-drawn control that have been drawn upon.

  • Outside region – the area outside of the mask area.

  • Transparent region – the area inside a mask region that isn't painted — for example, the area inside a circle. This area is determined by the developer of the control.

  • Close region – the area around the edges of the mask region. The width of the close region is determined by the control developer.

Figure 9.8

The actual composition of the mask region is dependent upon the setting of the BackStyle property:

  • If the BackStyle is Transparent, the MaskPicture and MaskColor properties plus any constituent controls make up the mask region. The transparent region consists of any areas that match the MaskColor.

  • If the BackStyle is Opaque, the entire surface of the control is the mask region; there is no transparent region.

Visual Basic doesn't know about the Transparent and Close regions: You have to define these regions yourself and test for them in the HitTest event.

The HitTest Event

The HitTest event is fired whenever the cursor is over a lightweight user control when the BackStyle is set to Transparent. You can add code to the HitTest event to determine whether mouse messages will be received by your control or passed on to the next object beneath it in the ZOrder. The HitTest event fires before any other mouse messages.

Because it's possible to layer several controls at design time, portions of other controls may be visible beneath the transparent areas of your control. In some cases, you might want to test for the existence of another control under your control. If one exists, you might allow it to receive the mouse events; if not, you might want to handle the mouse events yourself. The HitTest event gives you a second (and third) chance to handle the events if there is nothing underneath or if the controls underneath decline the events.

The HitTest event takes three arguments: the x and y coordinates specifying the location of the cursor, and a HitResult argument that corresponds to a control region. There are four possible settings for HitResult:

  • 3 (constant vbHitResultHit) – the cursor is over the mask region of the control; the control receives all mouse messages.

  • 2 (constant vbHitResultClose) – the cursor is over the close region of the control; mouse messages are ignored until the second pass.

  • 1 (constant vbHitResultTransparent) – the cursor is over the transparent region of the control; mouse messages are ignored until the third pass.

  • 0 (constant vbHitResultOutside) – the cursor is over the outside region the control; mouse messages are always ignored.

Hit testing is performed in the following order for layered controls:

  • A first pass is made through the ZOrder; the topmost control returning a HitResult of 3 (Hit) receives mouse messages.

  • If no control returns a HitResult of 3, a second pass is made; the topmost control returning a HitResult of 2 (Close) receives the mouse messages.

  • If no control returns a HitResult of 2, a third pass is made; the topmost control returning a HitResult of 1 (Transparent) receives the mouse messages.

  • If no control returns a HitResult of 1, the underlying container receives the mouse messages.

There's just one problem with this — the HitTest event will never return a HitResult of 1 or 2 because Visual Basic doesn't know about the close or transparent regions. You need to define these regions yourself in code; then if the X and Y coordinates fall within your defined region, you change the HitResult accordingly. The following code shows a HitTest event for a control with a square "hole" in its center:

Private Sub MyControl_HitTest(X As Single, _
   Y As Single, HitResult As Integer)

   ' Determine if the X,Y coordinates fall
   ' within the Close region.
   If (X > 200 And X < 210) Or (X > 390 And X < 400) Then
      If (Y > 200 And Y < 210) Or (Y > 390 And Y < 400) Then
         ' Coordinates are within the Close region.
         HitResult = vbHitResultClose
         ' We got a hit, so we can exit.
         Exit Sub
      End If
   End If

   ' Now check for the Transparent region.
   If (X > 210 And X < 390) Then
      If (Y > 210 And Y < 390) Then
         ' Coordinates are within the Transparent region.
         HitResult = vbHitResultTransparent
      End If
   End If
End Sub

In the above example, we first test to see if the coordinates fall within our Close region, in this case within 10 twips of the edge of our square hole. If so, we change the HitResult to 2 (vbHitResultClose) and exit. If not, we test to see if the coordinates are within the Transparent region, and if so we change the HitResult to 1 (vbHitResultTransparent). If neither test returns a hit, we go with the HitResult that was passed in.

Of course, in the case of a square region, it's fairly easy to determine the regions in code. As you might imagine, this procedure would be much more complex for a circular region, an irregularly shaped region, or for multiple regions within a control.

There's one more aspect to consider: With complex code being executed in the HitTest event, and with the HitTest event firing repeatedly as the cursor moves over your control, you might suspect that performance might be less than optimal. Your suspicion would be correct.

Performance Considerations for Hit Testing

Hit testing can be a pretty expensive operation in terms of performance. This isn't surprising when you consider what's going on behind the scenes. First of all, Visual Basic must perform calculations to clip your control to the mask defined by MaskPicture and MaskColor. Next, if your control contains other controls or user-drawn graphics, it must also clip around them. Finally, if you've defined Close and Transparent regions in your control, it needs to execute the code to check for those regions. With the HitTest event being fired repeatedly, this process could cause your control to perform very slowly.

Fortunately user controls have two additional properties, ClipBehavior and HitBehavior, that can be set in combination to improve performance. The following table shows the results of different pairs of settings:

 | 0 ClipBehavior None | 1 ClipBehavior Use Region
0 HitBehavior None | Hit test always returns 0 - no clipping. | Hit test always returns 0 - clipped to mask region.
1 HitBehavior Use Region | Hit test returns 0 or 3 - no clipping. | (Default) Hit test returns 0 or 3 - clipped to mask region.
2 HitBehavior Use Paint | Hit test returns 0 or 3 - clipped to paint. | Hit test returns 0 or 3 - clipped to paint inside mask region.

When HitBehavior is set to 0 (vbHitBehaviorNone), the HitTest event will always return a HitResult of 0 (Outside). You can add code to determine where the hit occurred and change the HitResult accordingly. This may improve performance if there isn't a lot of code in the HitTest event procedure.

Setting ClipBehavior to 0 (vbClipBehaviorNone) can often improve performance because Visual Basic doesn't need to determine the boundaries between the Mask and Outside regions. This is especially true if the Mask region is complex — for instance, when it includes TrueType® fonts or irregular shapes.

If your control will use hot spots (in other words, only portions of the visible area will accept clicks), setting ClipBehavior to 0 and HitBehavior to 1 (vbUseRegion) can improve performance.

Setting HitBehavior to 2 (vbUsePaint) may be necessary for user-drawn controls where the visible portion of the control isn't the same as the Mask region. This is the most expensive operation because the control needs to repaint each time the HitTest event is executed.

Important When a lightweight user control is sited on a container that doesn't support the Windowless property, some HitBehavior and ClipBehavior settings may cause unpredictable results. If your control will be used in containers that don't support lightweight controls, you should use the default settings.


Allowing Developers to Set Access Keys for Your Control

Placing ampersand characters (&) in the captions of Label controls and CommandButton controls creates access keys with which the end user of an application can shift focus to the control. You can create a similar effect in your ActiveX controls.

For example, suppose you have created a user-drawn button. In the Property Let for your button's Caption property, you can examine the text of the caption the user has entered. If there's an ampersand in front of a letter, you can assign that letter to the AccessKeys property of the UserControl object.

When the end user presses one of the access keys enabled in this fashion, your UserControl object receives an AccessKeyPress event. The argument of this event contains the access key that was pressed, allowing you to support multiple access keys on a control.

Access Keys for Control Assemblies

Control assemblies may contain constituent controls that can get the focus, and that support access keys of their own. You can use this fact to provide access key functionality.

Suppose you've authored a general-purpose control that consists of a text box and a label; you want the user to be able to set an access key in the label's caption, and forward the focus to the text box. You can accomplish this by giving the label and text box TabIndex values of zero and one (the TabIndex values on the UserControl are not visible outside your control), and delegating the Caption property of your control to the label, thus:

Property Get Caption() As String
   Caption = Label1.Caption
End Property

Property Let Caption(NewCaption As String)
   Label1.Caption = NewCaption
   PropertyChanged "Caption"
End Property

When a developer assigns the text "&Marsupial" to your Caption property, the label control will do all the access key work for you.

Note When the end user presses an access key on one of your constituent controls, the UserControl does not receive an AccessKeyPress event.

Control Assemblies with Fixed Text

For fixed-purpose control assemblies, such as an Address control, you can put ampersand characters (&) in the captions of constituent controls. Unfortunately, these hard-coded access keys may conflict with other access key choices the user wishes to make on a form.

In a more sophisticated variation of this scheme, you might add an AccessKeyXxxx property to your control for the appropriate constituent controls. That is, if the caption of the label next to the txtLastname control was "Last Name," you would add an AccessKeyLastName property. The developer using your control could assign any character from the label's caption to this property, and in the Property Let code you could change the caption to contain the ampersand.


Making Your Control Align to the Edges of Forms

When creating controls like toolbars and status bars, it's useful to be able to make the control align to one of the borders of the form it's placed on. You can give your ActiveX control this capability by setting the Alignable property of the UserControl object to True.

If the container your control is placed on supports aligned controls, it will add an Align property to the Extender object. When the user chooses an alignment, your control will automatically be aligned appropriately.

In the Resize event of your UserControl, you will have to redraw your user-drawn control or rearrange the constituent controls of your control assembly. You can use the Align property of the Extender object to determine which container edge the control instance has been aligned to.

Note Not all containers support alignable controls. If you attempt to test the value of the Align property, make sure you use error trapping.


Making Your Control Invisible at Run Time

To author a control that's invisible at run time, like the Timer control, set the InvisibleAtRuntime property of the UserControl object to True.

Important Don't use the Visible property of the Extender object to make your control invisible at run time. If you do, your control will still have all the overhead of a visible control at run time. Furthermore, the extender properties are available to the user, who may make your control visible.

Invisible Controls vs. Ordinary Objects

Before you create a control that's invisible at run time, consider creating an ordinary object provided by an in-process code component (ActiveX DLL) instead.

Objects provided by in-process code components require fewer resources than controls, even invisible controls. The only reason to implement an invisible control is to take advantage of a feature that's only available to ActiveX controls.

Setting a Fixed Size for Your Control

Controls that are invisible at run time typically maintain a fixed size. You can duplicate this behavior using the Size method of the UserControl object, as shown here:

Private Sub UserControl_Resize()
   Size 420, 420
End Sub

The Width and Height properties of a UserControl object are always given in Twips, regardless of ScaleMode.


Adding an AboutBox to Your Control

ActiveX controls typically have an About "property" at the top of the Properties window, with an ellipsis button. Clicking the button shows an About box identifying the control and the software vendor that created it.

Visual Basic makes it easy to provide such About boxes. You can have separate About boxes for each control in your control component (.ocx file), or one About box that all the controls in the component share.

To add an About box to a control component

  1. Create an About box by adding a form to your ActiveX control project, and giving it appropriate text and controls. Name the form dlgAbout.

  2. In the code window for any control in the project, add the following Sub procedure:
    Public Sub ShowAboutBox()
dlgAbout.Show vbModal
Unload dlgAbout
Set dlgAbout = Nothing
End Sub

    Important Unloading the About box and setting it to Nothing frees the memory it was using. This is a courtesy to the user of your controls.

  3. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box. If the ShowAboutBox procedure is not selected in the Name box, click the drop down and select it.

  4. Click Advanced to expand the Procedure Attributes dialog box.

  5. In the Procedure ID box, select AboutBox to give the ShowAboutBox procedure the correct identifier.

  6. Repeat steps 2 through 5 for each control in the project.

Note The name of the About box form and the method that shows it can be anything you like. The procedure above used dlgAbout and ShowAboutBox for purposes of illustration only.

If you wish to have separate About boxes for each control, simply create additional forms, and show a different form in each control's ShowAboutBox method.

Of course, each form you add to the project increases its size. You can get the same effect with the single dlgAbout form by giving it a property named, let us say, ControlID. This property identifies which control dlgAbout is being shown for. In each control's ShowAboutBox method, set the ControlID property before showing dlgAbout. Place code in the Load event of dlgAbout to change the text and bitmaps on the About box appropriately.

For More Information Adding properties and methods to forms is discussed in "Programming with Objects," in the Visual Basic Programmer's Guide.


Providing a Toolbox Bitmap for Your Control

The Toolbox bitmap size is 16 pixels wide by 15 pixels high, as specified by the ActiveX control specification. You can create a bitmap this size, and assign it to the ToolboxBitmap property of your UserControl object.

Important Do not assign an icon to the ToolboxBitmap property. Icons do not scale very well to Toolbox bitmap size.

Visual Basic automatically uses the class name of your control as the tool tip text when users hover the mouse pointer over your icon in the Toolbox.

Tip When creating bitmaps, remember that for many forms of color-blindness, colors with the same overall level of brightness will appear to be the same. You can avoid this by restricting your bitmap to white, black, and shades of gray, or by careful color selection.


Allowing Your Control to be a Default or Cancel Button

Default and cancel buttons are controlled by the container. To notify the container that your control is capable of being a default or cancel button, set the DefaultCancel property of the UserControl to True.

If you have set the Default or Cancel property to True for one of the constituent controls on your UserControl, you must set the DefaultCancel property of the UserControl to True or the constituent control property will be ignored.

User-drawn controls can examine the DisplayAsDefault property of the AmbientProperties object to determine whether they should draw the extra black border that visually identifies a default button.

If DefaultCancel is True, and the developer makes an instance of your control the default button, the UserControl's AccessKeyPressed event will occur when the user presses the Enter key. The argument of the event will contain the ASCII value 13.

If an instance of your control is the cancel button, the argument of the AccessKeyPress event will contain the ASCII value 27.

Important The status of a default or cancel button can change at any time. You must place code in the UserControl's AmbientChanged event to detect changes in the DisplayAsDefault property, and adjust your control's appearance accordingly.


Adding Internet Features to Controls

ActiveX controls created with Visual Basic can support asynchronous downloading of property values, such as Picture properties that may contain bitmaps. Through the Hyperlink property of the UserControl object, they can also request that a browser jump to a URL, or navigate through the history list.

These features are available when the control is placed on a container that provides the necessary support functions, such as Microsoft Internet Explorer.

You may wish to design your control to support both normal loading of property values from a PropertyBag, which is not supported by browsers, and asynchronous downloading of property values.

Note Asynchronous downloading of local files is available in any application. An absolute file path must be used (that is, a path starting from the root directory). Relative file locations will not work.

Asynchronous Downloading

A control requests asynchronous downloading of a property by calling the AsyncRead method of the UserControl object. This method can be called from any event, method, or property procedure of your control, as long as the control has already been sited on the container.

The call returns immediately, and downloading proceeds in the background, as shown in Figure 9.8a.

Figure 9.8a Starting asynchronous download of a bitmap property

When the container has retrieved the entire property value, the control receives an AsyncReadComplete event that identifies the property whose value has been retrieved. The control can then access the retrieved data and set the property value, as shown in Figure 9.8b.

Figure 9.8b Asynchronous download completes

The example code in this topic follows this scheme to create a simple control that displays a bitmap.

To work through the example, open a new Standard EXE project. Use the Project menu to add a UserControl to the project. Place a PictureBox control on the UserControl, and set the properties as shown in the following table:

Object | Property | Setting
UserControl | Name | AsyncBitmap
PictureBox | Name | picBitmap
 | AutoSize | True

The control will have two properties, an ordinary Picture property and a related PictureFromURL property. The Picture property is implemented with all three property procedures, because a Picture object can be assigned with or without the Set statement.

Public Property Get Picture() As Picture
   Set Picture = picBitmap.Picture
End Property

Public Property Let Picture(ByVal NewPicture _
      As Picture)
   Set picBitmap.Picture = NewPicture
   PropertyChanged "Picture"
End Property

Public Property Set Picture(ByVal NewPicture _
      As Picture)
   Set picBitmap.Picture = NewPicture
   PropertyChanged "Picture"
End Property

The Picture property of the ActiveX control simply delegates to the Picture property of the picture box, picBitmap. The picture box does all the work of displaying the bitmap.

The new property overrides the Picture property of the UserControl object. From now on, if you type Picture without qualifying it, you will get the ActiveX control's Picture property, as defined here. To access the Picture property of the UserControl, you must now qualify it by typing UserControl.Picture.

Note The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," later in this chapter.

The PictureFromURL Property

When a URL string is assigned to the ActiveX control's PictureFromURL property, the Property Let begins a download of the bitmap. When the download is complete, the bitmap is assigned to the Picture property. PictureFromURL is thus an alternate way of assigning a value to the Picture property.

The PictureFromURL property stores the URL string in a private data member, in the Declarations section of the UserControl:

Option Explicit
Private mstrPictureFromURL As String

The Property Get simply returns this string, so the program can discover where the picture was downloaded from. The Property Let does all the work:

Public Property Get PictureFromURL() As String
   PictureFromURL = mstrPictureFromURL
End Property

Public Property Let PictureFromURL(ByVal NewString _
      As String)
   ' (Code to validate path or URL omitted.)
   mstrPictureFromURL = NewString
   If (Ambient.UserMode = True) _
         And (NewString <> "") Then
      ' If program is in run mode, and the URL string
      ' is not empty, begin the download.
      AsyncRead NewString, vbAsyncTypePicture, _
         "PictureFromURL"
   End If
   PropertyChanged "PictureFromURL"
End Property

When a URL string is assigned to the PictureFromURL property, the string is saved in the private variable. If the project the control has been added to is in run mode (which will always be true for a control on an HTML page) and if the URL string is not empty, the Property Let starts an asynchronous download, and then exits.

The asynchronous download is begun by calling the UserControl's AsyncRead method, which has the following syntax:

AsyncRead Target, AsyncType [, Property]

The Target argument specifies the location of the data. This can be a path or a URL. The host determines the correct method to retrieve the data.

The AsyncType argument specifies the form in which the retrieved data will be provided. It has the following possible values:

Constant | Description
VbAsyncTypePicture | The retrieved data is provided as a Picture object.
VbAsyncTypeFile | The retrieved data is placed in a file created by Visual Basic. A string containing the path to the file is provided. This is useful when the data contains a large AVI file to be played. The control author can assign the string to the file name property of the appropriate constituent control.
VbAsyncTypeByteArray | The retrieved data is provided as a byte array. It is assumed that the control author will know how to handle this data.

The Property argument is a string containing the name of the property whose value is to be downloaded. You can use this to enable multiple simultaneous downloads, because this same string is returned in the AsyncReadComplete event, and can be used in a Select statement.

The value of the Property argument can also be used as the argument of the CancelAsyncRead method, described below.

Completing the Download

As each requested download completes, the control receives an AsyncReadComplete event. The argument of the AsyncReadComplete event is a reference to an AsyncProperty object, which can be used to identify the downloaded property and retrieve the downloaded data.

Private Sub UserControl_AsyncReadComplete( _
      AsyncProp As VB.AsyncProperty)
   On Error Resume Next
   Select Case AsyncProp.PropertyName
      Case "PictureFromURL"
         Set Picture = AsyncProp.Value
         Debug.Print "Download complete"
   End Select
End Sub

You should place error handling code in this event procedure, because an error condition may have stopped the download. If this was the case, that error will be raised when you access the Value property of the AsyncProperty object.

When the downloaded bitmap is assigned to the Picture property, the control repaints.

Note In addition to the Value property that contains the downloaded data and the PropertyName property that contains the name of the property being downloaded, the AsyncProperty object has an AsyncType property. This contains the same value as the AsyncType argument of the AsyncRead method that started the download.

A Bit More Code

So far, the example has no way to save a bitmap assigned at design time. That is, there is no code in the InitProperties, ReadProperties, and WriteProperties events. That code is omitted here, because the purpose of the example is to show asynchronous downloading of a local file.

One more bit of code will prove useful, however. The reason for using a picture box to display the bitmap, instead of simply using the Picture property of the UserControl, is to take advantage of the AutoSize property of the picture box. The following code resizes the entire control whenever the picture box is resized by the arrival of a new bitmap.

Private Sub picBitmap_Resize()
   ' If there's a Picture assigned, resize.
   If picBitmap.Picture <> 0 Then
      UserControl.Size picBitmap.Width, _
         picBitmap.Height
   End If
End Sub

The resizing only happens if the picture box actually contains a Picture object. This allows the user to specify the size of the empty picture box while the download is pending. The code is as follows:

Private Sub UserControl_Resize()
   If picBitmap.Picture = 0 Then
      picBitmap.Move 0, 0, ScaleWidth, ScaleHeight
   Else
      If (Width <> picBitmap.Width) _
            Or (Height <> picBitmap.Height) Then
         Size picBitmap.Width, picBitmap.Height
      End If
   End If
End Sub

If there is no Picture object, the picture box is sized to fill the visible area of the UserControl. If there is a Picture object, and the UserControl is resized, it will snap back to the size of the picture box.

Starting the Download

Close the UserControl designer. The control is now running, even though the rest of the project is in design mode. The default control icon on the Toolbox is enabled, so you can add an instance of the control to Form1.

Locate a large bitmap on your computer — the larger the better. Note the file name and path to the file. Add the following code to the Declarations section of Form1, substituting the name and path of your bitmap for the one shown here:

Option Explicit
Const DOWNLOADFILE = "file:\windows\forest.bmp"

Make sure the string begins with file:\ so that it's a valid URL for a local file.

Note The URL you pass to the AsyncRead method cannot be a relative URL. That is, you cannot specify relative pathnames for local files.

Place the following code in the form's Load event:

Private Sub Form_Load()
   AsyncBitmap1.PictureFromURL = DOWNLOADFILE
   DEBUG.PRINT "Load event complete"
End Sub

When the form loads, the URL for the local bitmap file will be assigned to the PictureFromURL property, starting the download.

Running the Sample

Press F5 to run the project. In the Immediate window, notice that the first message is "Load event complete," followed by the "Download complete" message from the AsyncReadComplete event.

If the bitmap was large enough, you may also have noticed that Form1 painted while the bitmap was still downloading. Close Form1, to return to design mode.

Run the project again, and this time click the Close button on Form1 as soon as you see it. Because the download is proceeding asynchronously in the background, the form becomes active and can respond to user input before the bitmap has been loaded.

Using the Control

Up to this point, the example only demonstrates downloading of a local file. It can't be used on a Web page, because controls in Standard EXE projects cannot be used by other applications. To use the control in other projects or on Web pages, add the source file to an ActiveX control project and compile it as an .ocx file. You'll need some additional code —for example, to save the Picture property, as shown here.

Private Sub UserControl_InitProperties()
   ' Use Nothing as the default when initializing,
   '   reading, and writing the Picture property,
   '   so than an .frx file won't be needed if
   '   there's no picture.
   Set Picture = Nothing
End Sub

Private Sub UserControl_ReadProperties( _
         PropBag As PropertyBag)
   Set Picture = _
      PropBag.ReadProperty("Picture", Nothing)
End Sub

Private Sub UserControl_WriteProperties( _
         PropBag As PropertyBag)
   PropBag.WriteProperty "Picture", Picture, Nothing
End Sub

You can use the ActiveX Control Interface Wizard to add standard properties, methods, and events — such as MouseMove — to the control.

Canceling Asynchronous Downloads

You can call the CancelAsyncRead method to cancel an asynchronous data load. CancelAsyncRead takes the property name as an argument; this must match the value of the PropertyName argument in a prior AsyncRead method call.

Only the specified data load is canceled. All others continue normally.

For More Information You can also use the AsyncReadProgress event to monitor the progress of an asynchronous download. The AsynchReadProgress event is discussed in "Downloading Data Asynchronously" in "Building ActiveX Documents."

Navigating with the Hyperlink Object

The Hyperlink object gives your control access to ActiveX hyperlinking functionality. Using the properties and methods of the Hyperlink object, your control can request a hyperlink-aware container, such as Microsoft Internet Explorer, to jump to a given URL or to navigate through the history list.

You can access the Hyperlink object through the Hyperlink property of the UserControl object. The following code fragment assumes that the control's URLText property contains a URL string. Clicking on the control causes it to request that its container navigate to that URL.

Private Sub UserControl_Click()
   HyperLink.NavigateTo Target:=URLText
End Sub

If the target is not a valid URL or document, an error occurs. If the control is sited on a container that does not support hyperlinking, an application that is registered as supporting hyperlinking is started to handle the request.

The NavigateTo method accepts an optional Location argument, which specifies a location within the target URL. If a location is not specified, the server will jump to the top of the URL or document.

The NavigateTo method also accepts an optional FrameName argument, which specifies a frame within the target URL.

Note If your control is placed on a container that does not support the IHLink interface, the Hyperlink property of the UserControl object will return Nothing. You should test the property for this value before attempting to use the Hyperlink object.

Moving Through the History List

You can call the GoForward and GoBack methods to navigate through the History list. For example:

   Hyperlink.GoForward

If GoForward and GoBack are called when there are no entries in the History list to move forward to, an error occurs. GoForward and GoBack will also raise errors if the container is not hyperlink-aware.

For More Information Details of Internet support for ActiveX controls authored in Visual Basic can be found on the Microsoft Visual Basic Web site.

Information on designing Internet and intranet applications with Visual Basic can be found in Building Internet Applications.


Designing Controls for Use With HTML

A control on an HTML page is specified using the HTML <OBJECT> and </OBJECT> tags. When the HTML is processed, the control is created and positioned. If the <OBJECT> tag includes any <PARAM NAME> attributes, the property values supplied with those attributes are passed to the control's ReadProperties event using the standard PropertyBag object, as discussed in "Understanding Control Lifetime and Key Events."

Once the HTML page is active, the control's property values may also be set by scripts attached to events that occur on the page.

Note If there are no <PARAM NAME> attributes other than those that set extender properties (such as Top and Left), the control may receive an InitProperties event rather than a ReadProperties event. This behavior is dependent on browser implementation, and should not be relied on.

The Package and Deployment Wizard makes it easy to create an Internet setup for your control, with cabinet (.cab) files that can be automatically downloaded when a user opens an HTML page containing an instance of your control. Support files, such as MSVBVM60.DLL, can be downloaded separately. P-code .ocx files are very compact, so if support files already exist on a user's computer, downloading can be very fast.

Visual Basic controls can support digital signatures, safe initialization, and safe scripting.

Important In order to use a control that includes licensing support on an HTML page, a licensed copy of the control component must be installed on the Web server that provides the page. This is discussed in "Licensing Issues for Controls," later in this chapter.

Making Your Control Safe for Scripting and Initialization on HTML Pages

Code that's downloaded as a result of opening a page on the World Wide Web doesn't come shrink-wrapped, blazoned with a company name to vouch for its reliability. Users may be understandably skeptical when they're asked to okay the download. If you intend for your control to be used on HTML pages, there are several things you can do to reassure users.

  • Digital signatures create a path to you (through the company that authorized your certificate), in the event that your control causes harm on a user's system. You can incorporate your signature when you use Package and Deployment Wizard to create an Internet setup for your control component.

  • Marking your control safe for scripting tells users that there's no way a script on an HTML page can use your control to cause harm to their computers, or to obtain information they haven't supplied willingly.

  • Marking your control safe for initialization lets users know there's no way an HTML author can harm their computers by feeding your control invalid data when the page initializes it.

This topic explains how to design your control so that when you create your Internet setup, you'll be able to mark your control as safe for scripting and safe for initialization.

Note The default setting for Internet Explorer is to display a warning and to refuse to download a component that has not been marked safe for scripting and initialization.

For More Information The latest information on digital signatures, cabinet files, and Internet setup can be found on the Microsoft Visual Basic Web site.

Safe for Scripting

When a Web designer places your control on an HTML page, he uses a scripting language such as JavaScript or Visual Basic, Scripting Edition to access the control's properties, invoke its methods, and handle its events. By marking your control as safe for scripting, you're providing an implicit warrantee: "No matter what VBScript or JavaScript code is used, this control cannot be made to harm a user's computer, or to take information the user hasn't volunteered."

As the author of your control, you can be reasonably sure that in normal use it won't destroy data or compromise the security of a user's computer. Once your control is in the hands of a Web designer, however, you have no guarantee that it will be used in the ways you intended.

Keys to Scripting Safety

As an example of a control that's not safe for scripting, consider the rich text box. The RichTextBox control has a SaveFile method that can be used to write the contents of the control to a file. A malicious person could write a script that would cause this control to over-write an operating system file, so that the user's computer would malfunction.

What makes the control unsafe is not that it can save information to a file — it's the fact that the script can specify the filename. This observation provides the key to creating controls that are safe for scripting. As long as your control doesn't allow a script to specify the source or target for file or registry operations, or make API calls that can be directly controlled by a script, it is probably safe for scripting.

Thus, a control that permits a Web page designer to do any of the following is probably not safe for scripting:

  • Create a file with a name supplied by a script.

  • Read a file from the user's hard drive with a name supplied by a script.

  • Insert information into the Windows Registry (or into an .ini file), using a key (or filename) supplied by a script.

  • Retrieve information from the Windows Registry (or from an .ini file), using a key (or filename) supplied by a script.

  • Execute a Windows API function using information supplied by a script.

  • Create or manipulate external objects using programmatic IDs (for example, "Excel.Application") that the script supplies.

The line between safe and unsafe can be a fine one. For example, a control that uses the SaveSetting method to write information to its own registry key doesn't disqualify itself for safe scripting by doing so. On the other hand, a control that allows the registry key to be specified (by setting a property or invoking a method) is not safe.

A control that uses a temporary file may be safe for scripting. If the name of that temporary file can be controlled by a script, then the control is not safe for scripting. Even allowing a script to control the amount of information that goes into the temporary file will make the control unsafe for scripting, because a script could continue dumping information into the file until the user's hard disk was full.

As a final example, a control that uses API calls is not necessarily unsafe for scripting. Suppose, however, that the control allows a script to supply data that will be passed to an API, and doesn't guard against oversize data overwriting memory, or invalid data corrupting memory. Such a control is not safe for scripting.

As an indication of the seriousness of scripting safety, note that VBScript itself does not include methods to access the registry, save files, or create objects.

Choosing Constituent Controls

You might think that using a constituent control that's not safe for scripting would automatically make your ActiveX control unsafe for scripting. This is not necessarily true.

As explained in "Adding Properties to Controls," later in this chapter, the properties and methods of constituent controls do not automatically become part of your control's interface. As long as you avoid exposing the properties and methods that make a constituent control unsafe, you can use it without making your own control unsafe.

For example, if you use the RichTextBox as a constituent control, you should not expose its SaveFile method.

Important Do not provide a property that returns a reference to an unsafe constituent control. A script could use this reference to access the properties and methods that make the control unsafe.

Documenting Scripting Safety

Determining whether a control is safe is not a trivial exercise. You may find it helpful to record your design decisions that affect safe scripting. A useful exercise is to construct tables containing the following:

  • All of your control's public properties, methods, and events.

  • All of the files and registry keys accessed, and all API calls used.

If there are any dependencies or data transfer between the elements of these two tables, then the control is probably not safe for scripting.

You may wish to have this documentation reviewed by an experienced programmer who understands both ActiveX controls and scripting.

Safe for Initialization

A control marked as safe for initialization carries an implicit guarantee that it will cause no harm no matter how its properties are initialized.

On an HTML page, your control's initial state is set using PARAM NAME attributes with the OBJECT tag that embeds your control on the page. If a malicious Web designer can make your control steal information or otherwise cause harm by placing invalid data in a PARAM NAME attribute, then your control is not safe for initialization.

The best defense against malicious data is to validate each property value that's obtained in your control's ReadProperties event. All the data a Web designer places in PARAM NAME attributes is supplied to your control through the PropertyBag object in the ReadProperties event. (A well-written control should perform this kind of validation anyway, to prevent problems caused by developers who manually edit .frm files.)

For More Information The most up-to-date information on authoring controls for the Internet can be found on the Microsoft Visual Basic Web site.

Using Show and Hide Events

The Show and Hide events can be very useful on Web pages. If your control is performing a resource-intensive task, such as showing a video clip or repeatedly downloading and displaying a stock value, you may want to pause this activity when the Hide event occurs.

The Hide event means that the user has moved on to another page, relegating the page your control is on to the History list. The Show event means that the user has returned to your page, and can thus be the signal for resuming resource-intensive display tasks.

For More Information The Show and Hide events are discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter.

Using the Parent and ParentControls Properties

You can use the Parent property of the UserControl object to access the container. For example, in Internet Explorer, the following code will change the background color of the HTML page on which your control is located:

Parent.Script.get_document.bgColor = "Blue"

More information on the Internet Explorer Scripting Object Model can be found on Microsoft's Web site.

Important Always use late binding for calls to the Internet Explorer Scripting Object Model. Using early binding will almost certainly cause compatibility problems in the future, while late binding will always work.

The ParentControls Collection

The ParentControls collection allows you to access the other controls on a container where your control has been sited. On some containers — Internet Explorer is one of them — ParentControls returns an extender object whose properties and methods are not merged with those of the controls. This prevents you from accessing the controls themselves.

To access the other controls on an HTML page, you can set the ParentControlsType property of the ParentControls collection to vbNoExtender. The ParentControls collection will thereafter return the interfaces of the controls themselves, without the extender.

ParentControls.ParentControlsType = vbNoExtender

Note The Parent property and ParentControls collection can be used with many other containers. They are not limited to HTML browsers.


Binding a Control to a Data Source

Visual Basic allows you to mark properties of your control as bindable, allowing you to create data-aware controls. A developer can associate bindable properties with fields in any data source, making it easier to use your control in database applications.

Use the Procedure Attributes dialog box, accessed from the Tools menu, to mark properties of your control as bindable. Figure 9.9 shows the data binding options made available by clicking the dialog's Advanced button.

Figure 9.9 Data binding options for ActiveX control properties

The controls supplied with Visual Basic can be bound to data source fields using their DataSource and DataField properties. You can select one property of your control to be bound to the DataField property. Typically, this will be the most important piece of data your control holds.

Although you can mark only one field as bound to the field specified in the DataField property, you can mark additional properties of your ActiveX control as bindable. Developers can use the DataBindings collection to bind these additional bindable properties to data fields.

Note Some development tools and control containers do not support data binding. This topic describes the support for data-bound controls provided by Visual Basic.

The DataBindings Collection

The DataBindings collection is an extender property that Visual Basic provides to users of your control. It allows the developer to access the list of bindable properties on your control.

Note All bindable properties appear in the DataBindings collection at run time. At design time, only properties marked "Show in DataBindings collection at design time" will appear when the DataBindings property is accessed in the Properties window.

For example, you might create an Address control assembly, using labels and text boxes as constituent controls. The bindable properties of your control would correspond to the text boxes on your control, as shown in Figure 9.10.

Figure 9.10 An Address control assembly with multiple fields

Note If your control has multiple bindable properties, you must mark one as binding to the Extender object's DataField property. Otherwise, the Extender object will not provide a DataSource property for your control. You can mark a property as binding to DataField by selecting "This property binds to DataField" in the Procedure Attributes dialog box. The property you mark as binding to DataField can also be bound using the DataBindings collection.

The mapping between properties of the control and contents of the constituent controls is accomplished by delegation, as in this code fragment:

Public Property Get AddressLine1() As String
   AddressLine1 = txtAddressLine1.Text
End Property

Public Property Let AddressLine1(NewValue As String)
   If CanPropertyChange("AddressLine1")
      txtAddressLine1.Text = NewValue
      ' The following line tells Visual Basic the
      ' property has changed--if you omit this line,
      ' the data source will not be updated!
      PropertyChanged "AddressLine1"
   End If
End Property

Delegating to the text box control means that the text box does all the work of displaying the value and accepting user changes. Because the user can change the value of the property while the text box is displaying it, you must also mark the property as changed in the text box's Change event, as shown below.

Private Sub txtAddressLine1_Change()
   PropertyChanged "AddressLine1"
End Sub

Important In order for the new value to be written back to the data source, you must call PropertyChanged. If you don't call the PropertyChanged method, your control will not be bound for update.

For More Information The PropertyChanged method has another important purpose, as discussed in "Adding Properties to Controls," later in this chapter.

Calling CanPropertyChange

Your control should always call CanPropertyChange before changing the value of a property that can be data-bound. Do not set the property value if CanPropertyChange returns False. Doing so may cause errors in some control containers.

If your control always calls CanPropertyChange, you can check "Property will call CanPropertyChange before changing" on the Procedure Attributes dialog box.

Note At present, CanPropertyChange always returns True in Visual Basic, even if the bound field is read-only in the data source. This does not cause a problem with the code shown above, because Visual Basic doesn't raise an error when your program attempts to change a read-only field; it just doesn't update the data source.

Discovering and Setting Bindable Properties at Run Time

If a developer placed an instance of the AddressBox control on a form, she could execute the following code to list the bindable properties:

   Dim dtb As DataBinding
   For Each dtb In AddressBox1.DataBindings
      Debug.Print dtb.PropertyName
   Next

At run time, the developer could use the following code to bind the AddressLine1 property to the AddrLine1 field, assuming that field was available on the data source specified by the DataSource extender property:

   AddressBox1.DataBindings( _
      "AddressLine1").DataField = "AddrLine1"

Finding Out Whether a Field has Changed

You can test the DataChanged property of a DataBinding object to find out if the value of a field has changed. This property functions in the same way as the DataChanged extender property of bound controls.

Setting Multiple Data Bindings at Design Time

Bindable properties always appear in the DataBindings collection at run time. By default, they do not appear in the Data Bindings dialog box at design time.

If you want a bindable property to appear in the Data Bindings dialog box, select that property in the Procedure Attributes dialog box and check "Show This Property in the Bindings Collection."

Figure 9.11 Using the Data Bindings dialog box

The Data Field box shows all fields available on the data source specified by the current value of the DataSource extender property on the control instance.

Attributes and Flags

If you have developed OLE controls in the past, you can use the following table to see what flags are set by the Procedure Attributes dialog box. The table also shows how these attributes are accessed through the Member object in the Visual Basic Extensibility Model.

Attribute | Flag | Member Object
Property is data bound. | BINDABLE | Bindable
This property binds to DataField. | DEFAULTBIND | DefaultBind
Show in DataBindings collection at design time. | DISPLAYBIND | DisplayBind
Property will call CanPropertyChange before changing. | REQUESTEDIT | RequestEdit


Allowing Developers to Edit Your Control at Design Time

Some controls, like the OLE control supplied with Visual Basic, allow you to edit the control's contents at design time. You can enable this behavior for your control by setting the EditAtDesignTime property of your UserControl object to True.

If a container supports this feature, it will add an Edit menu item to the context menu that appears when a developer right-clicks on an instance of your control at design-time.

The developer using the control can activate a control instance by right-clicking the control to get the Context menu, then clicking Edit. The control will be activated, and will behave as it does at run time.

This allows you to author controls with visual design features, such as letting the user size rows and columns, or set property values by typing directly into constituent controls. You can detect whether your control is running at design time by testing the UserMode property of the AmbientProperties object. This property is False at design time.

The control only remains active while it is selected. When the developer clicks on another control, the control will deactivate. To reactivate the control, the developer must select Edit from the context menu.

Note When your control is activated in this fashion, the events of the UserControl object will occur, so that your control can operate normally, but your control will be unable to raise any events. The RaiseEvent method will simply be ignored; it will not cause an error.


Drawing Your Control

The way you draw your control's appearance depends on the control creation model you're using.

If you're creating a user-drawn control, you have to do all the drawing yourself. You have to know when to draw your control, what state it's in (for example, clicked or unclicked), and whether you should draw a focus rectangle.

If you're enhancing an existing control or creating a control assembly, on the other hand, your UserControl's constituent controls provide your control's appearance. The constituent controls draw themselves automatically, and all you have to worry about is whether they're positioned correctly on the UserControl.

The following topics outline the basic techniques required for each model.

  • User-Drawn Controls Describes where to put your drawing code, and visual behavior like button clicks, default buttons, and focus rectangles.

  • Providing Appearance Using Constituent Controls Outlines the use of the Resize event, including the handling of minimum control size.

For More Information Control creation models are discussed in "Control Creation Basics," earlier in this chapter.


User-Drawn Controls

When you're doing your own drawing, the only place you need to put drawing code is in the UserControl_Paint event procedure. When the container repaints the area your control is located on, the UserControl object will receive a Paint event.

If the built-in graphics methods of the UserControl object, such as Line and Circle, don't meet your drawing needs, you can use Windows API calls to draw your control. You can also "draw" text on your control using the Print method. Regardless of the drawing technique, the code goes in UserControl_Paint.

If your control has to change its appearance based on user actions, such as when the user clicks on the control, you can raise the Paint event by calling the UserControl's Refresh method.

Note While you cannot draw on the transparent parts of a UserControl with BackStyle = Transparent, it is possible to create controls with transparent backgrounds that are mostly user-drawn. See "Giving Your Control a Transparent Background," earlier in this chapter.

Important In Paint events, do not use DoEvents, or any other code that yields to other programs. Doing so will cause errors.

The following example demonstrates the basic principle of a three-state button, each of the three states being represented by a different bitmap. To work through the example, open a new Standard EXE project and use the Project menu to add a UserControl to the project. Place a PictureBox on the UserControl, and set object properties as follows:

Object | Property | Setting
UserControl | Name | TripleState
PictureBox | AutoRedraw | True
 | Name | picStates
 | Picture | (Any bitmap)
 | Visible | False

Note The example works best if the bitmap chosen for the Picture property changes color dramatically from left to right.

Add the following code to the Declarations section of the UserControl's code window.

Option Explicit
' Private variable keeps track of the current state.
Private mintState As Integer

A simple mechanism is used to provide the three states: Clicking on the control rotates through the states by incrementing the value of a private state variable each time the control is clicked. If the value becomes too large, it is reset to zero. Add this code to the UserControl's Click event.

Private Sub UserControl_Click()
   mintState = mintState + 1
   If mintState > 2 Then mintState = 0
   ' The following line causes Paint event to occur.
   Refresh
End Sub

Add the following code to the UserControl's Paint event. When the Paint event occurs, the code copies one-third of the bitmap in the invisible PictureBox to the UserControl. Which third is copied depends on the current value of the private state variable mintState.

Private Sub UserControl_Paint()
   PaintPicture picStates.Picture, 0, 0, ScaleWidth, _
      ScaleHeight, mintState * picStates.Width / 3, _
      0, picStates.Width / 3
End Sub

Note Another way to provide a different appearance for the each of your control's states is to use the Select…Case statement.

When mintState is zero, its initial value, the first third of the hidden bitmap will be copied onto the UserControl, as shown in Figure 9.12.

Figure 9.12 Copying the first third of the hidden bitmap to the UserControl

Click the Close box on the UserControl designer, to enable its icon in the Toolbox. Double-click the icon to place a copy of the control on a form, and press F5 to run the project. Click on the control to change the state.

You can hide the form behind another window, or minimize and restore the form, to verify that the control correctly retains its state, and repaints accordingly.

Tip For better performance when drawing your own control, make sure the AutoRedraw property is set to False.

For More Information See "Drawing the ShapeLabel Control," in "Creating an ActiveX Control."

Using Windows API calls is discussed in "Accessing the Microsoft Windows API," in the Component Tools Guide.The PaintPicture method is discussed in "Working with Text and Graphics," in the Visual Basic Programmer's Guide.

Working with Other Events

In similar fashion, you can simulate other event-driven appearance changes, such as button presses. To animate button presses, put state-changing code in the MouseUp and MouseDown events. Regardless of the events being used, the principle is the same: change the state, and call the Refresh method.

Showing That a User-Drawn Control Has the Focus

If your control can get the focus, you will need a second state variable to keep track of whether your control currently has the focus, so that each time your control redraws itself it will show (or not show ) an appropriate indication that it has the focus.

The Windows API DrawFocusRect can be used to draw the type of single-pixel dotted line used to show focus in the CommandButton control. There is no comparable API for non-rectangular shapes.

For More Information See "How to Handle Focus in your Control," earlier in this chapter.

Showing a User-Drawn Control as Disabled

If you implement an Enabled property, you will need to keep track of whether your control is enabled, so you can provide a visual cue to the user when your control is disabled.

If you implement your control's Enabled property as discussed in "Allowing Your Control to be Enabled and Disabled," earlier in this chapter, you can simply test UserControl.Enabled to determine whether to draw your control as enabled or disabled.

The way you draw your control to indicate that it's disabled is entirely up to you.

User-Drawn Controls That Can Be Default Buttons

Setting the DefaultCancel property of your UserControl object to True tells the container your control can be a default or cancel button, so the Extender object can show Boolean Default and Cancel properties.

You can examine the value of the DisplayAsDefault property of the AmbientProperties object to determine whether your control should show the extra black border that tells the end user your control is the default button. Show the border only when the DisplayAsDefault property is True.

Important Correct behavior for a button in Windows is to show the default border only if your control has been designated as the default button, and no other button has the focus. DisplayAsDefault is True only if both of these conditions are met. Other methods of determining when to display the border may result in incorrect behavior.

For More Information See "Understanding the Container's Extender Object," and "Using the AmbientProperties Object to Stay Consistent with the Container," earlier in this chapter.

Handling the FontChanged Event

If you want to display text on your user-drawn control, you can do so using the Print method in the UserControl_Paint event. For example, the following code would display "Push Me" on the TripleState control from the previous example:

Private Sub UserControl_Paint()
   Cls      ' Clear any previous text
   Print Spc(3); "Push Me"
End Sub

There is just one problem with the above example — the text will always be displayed in the default font, even if you have exposed the Font property for your control. Your control has no idea that the font has changed, so the Paint event isn't fired.

To circumvent this problem, you need to add a Font object to your user control and call the User Control's Refresh method in the FontChanged event of the Font object. The Font object must be declared using the WithEvents keyword in order to expose this event:

Option Explicit
Private WithEvents mFont as stdFont

For a complete example of using a Font object and the FontChanged event, see "Adding a Font Object to Your User Control" in "ActiveX Control Features."


Providing Appearance Using Constituent Controls

If you're enhancing an existing control, that single constituent control will typically occupy the entire visible surface of your UserControl object. You can accomplish this by using the Move method of the constituent control in the Resize event of the UserControl, as shown below.

Private Sub UserControl_Resize()
   picBase.Move 0, 0, ScaleWidth, ScaleHeight
End Property

The code above assumes that an enhanced picture control is being authored. A PictureBox control has been placed on the UserControl, and named picBase.

If the control you're enhancing has a minimum size, or a dimension that increases in large increments — such as the height of a ListBox control, which changes by the height of a line of text — you will have to add code to determine whether the Move method has produced the desired result.

You can rely on the control you're enhancing to handle painting, including (where appropriate) default button highlighting.

Tip You may also have to add code to resize your UserControl object, to accommodate a constituent control that can't be sized arbitrarily — such as a text box or list box. To avoid exhausting stack space with recursive calls to the Resize event, use static variables to determine when the UserControl_Resize event procedure is making recursive calls.

Resizing a Control Assembly

If you're authoring a control assembly, the Resize event will be more complex, because you have to adjust both size and relative location of multiple constituent controls.

Enforcing a Minimum Control Size

If you author a control with a large number of constituent controls, there may be a minimum size below which resizing the constituent controls is futile, because too little of each control is visible to be of any use, or because the enforced minimum sizes of some constituent controls has been reached.

There is no real harm in allowing the user to reduce the size of your control to absurdity. Most controls allow this, because preventing it is a lot of work, and because at some point you have to rely on your user's desire to produce an application that works and is usable.

In the event that resizing below some threshold causes your control to malfunction, you might make all of your constituent controls invisible, as an alternative to enforcing a minimum size.

The following code fragment provides a simple example of enforcing a minimum size.

Private Sub UserControl_Resize()
   Dim sngMinH As Single
   Dim sngMinW As Single

   ' Code to resize constituent controls. It is
   ' assumed that each of these will have some minimum
   ' size, which will go into the calculation of the
   ' UserControl's minimum size.

   sngMinW = <<Width calculation>>
   sngMinH = <<Height calculation>>

   If Width < sngMinW Then Width = sngMinW
   If Height < sngMinH Then Height = sngMinH
End Sub

Notice the <<pseudo-code placeholders>> for the calculation of your control's minimum width and height. These calculations will be in the ScaleMode units of your UserControl. They may be very complicated, involving the widths and heights of several of the constituent controls.

The Width and Height properties of the UserControl are then set, if necessary.

Important The Width and Height properties of the UserControl include the thickness of the border, if any. If BorderStyle = 1 (Fixed Single), the area available for constituent controls will be reduced by two pixels (not Twips) in both width and height. If you have exposed the BorderStyle property for your user to set, include code to test the current value.

As an alternative, you could use the Size method:

   If Width > sngMinW Then sngMinW = Width
   If Height > sngMinH Then sngMinH = Height
   If (sngMinW <> Width) Or (sngMinH <> Height) Then
      ' (Optionally, set recursion flag.)
      Size sngMinW, sngMinH
      ' (Clear recursion flag, if set.)
   End If

This code is more slightly more complicated, but it simplifies things if you need to avoid recursion when resizing your control, as discussed below.

Important The Width and Height properties of the UserControl are always expressed in Twips, regardless of the ScaleMode setting. If you have set ScaleMode to something other than Twips, use the ScaleX and ScaleY methods to convert your minimum size calculations to Twips.

Dealing with Recursion

No code for recursion is included in the example above, and recursion is virtually guaranteed. For example, if you attempt to resize the control so that both width and height are below the minimum values, the Resize event will reset the Width property, which will cause a second Resize to be raised immediately.

This second Resize event will test and reset the height, and then return — so that by the time the first Resize event tests the height, it will already have been reset to the minimum. Clearly, this can lead to confusing debugging situations.

Even if you use the Size method, a second Resize event will occur, repeating all your calculations. This can be avoided by setting a flag when you deliberately resize the control. The Resize event should check this flag, and skip all processing when it is True.

A recursion flag is not necessary for simple minimum size situations, but is virtually required for more complicated scenarios.

For example, if you use the code above in a control whose Align property is True, so that it aligns to the form it's placed on, as described in "Making Your Control Align to the Edges of Forms," infinite recursion errors are guaranteed, until stack space is exhausted and an error occurs.

Important Always use error handling in Resize event procedures. Errors here cannot be handled by the container, and your control component will therefore fail, causing the application using your control to fail, as well.

For More Information The models for creating controls are discussed in "Three Ways to Build ActiveX Controls" earlier in this chapter.


Adding Properties to Controls

You implement properties of your ActiveX control by adding property procedures to the code module of the UserControl that forms the basis of your control class.

By default, the only properties your control will have are the extender properties provided by the container. You must decide what additional properties your control needs, and add code to save and retrieve the settings of those properties.

Properties for controls differ in two main ways from properties of other objects you create with Visual Basic.

  • Property values are displayed in the Properties window and Properties Pages dialog box at design time.

  • Property values are saved to and retrieved from the container's source file, so that they persist from one programming session to the next.

As a result of these differences, implementing properties for controls has more requirements and options than for other kinds of objects.

Implement Control Properties Using Property Procedures

The most important consequence of the differences listed above is that control properties should always be implemented using property procedures instead of public data members. Otherwise, your control will not work correctly in Visual Basic.

Property procedures are required because you must notify Visual Basic whenever a property value changes. You do this by invoking the PropertyChanged method of the UserControl object at the end of every successful Property Let or Property Set, as shown in the following code fragment.

Private mblnMasked As Boolean

Public Property Get Masked() As Boolean
   Masked = mblnMasked
End Property

Public Property Let Masked(ByVal NewValue As Boolean)
   mblnMasked = NewValue
   PropertyChanged "Masked"
End Property

There are two reasons for notifying Visual Basic that a property value has changed:

  • If you don't call PropertyChanged, Visual Basic cannot mark control instances as needing to be saved. They will not get WriteProperties events, and developers who use your control will lose any property values they set at design time.

  • Because property values may be displayed in more than one place, the development environment must be informed when a property value changes, so it can synchronize the values shown in the Properties window, the Property Pages dialog box, and so forth.

Run-Time Properties

Properties that are available only at run time don't need to call the PropertyChanged method, unless they can be data-bound. However, they still need to be implemented using property procedures, as explained in the related topic "Creating Design-Time-Only or Run-Time-Only Properties."

For More Information Authoring controls that can be bound to data sources is discussed in "Binding Your Control to a Data Source," earlier in this chapter.

Showing Properties in the Properties Window

If you create both a Property Get and a Property Let, your property will automatically be displayed in the Properties window. In some cases, you may not want this.

For example, if displaying a property value requires a time-consuming calculation, the developer who uses your control may be annoyed at the length of time it takes to access the Properties window.

On the Property Attributes dialog, accessible from the Tools menu, select the property you wish to suppress. Click the Advanced button, check "Don't show in Property Browser," and then click Apply.

Properties You Don't Need to Implement

Right away, you can avoid a lot of work. The Extender object, which is provided by the container your control is placed on, will supply a number of properties for you. DragIcon, HelpContextID, TabIndex, Top, and Visible are a few of the extender properties supplied by Visual Basic forms.

For More Information To see all the properties Visual Basic's Extender object provides, search the Language Reference in the index for Extender Object. The Extender object is discussed in "Understanding the Container's Extender Object," earlier in this chapter. An odd exception is the Enabled property, which you must implement so that the Extender object can mask it. See "Allowing Your Control to be Enabled and Disabled," earlier in this chapter.

Other Consequences of the Special Nature of Control Properties

Other property creation requirements and options for controls are discussed in the following related topics.

  • Saving the Properties of Your Control Discusses the mechanism for saving and retrieving property values.

  • Exposing Properties of Constituent Controls You can save time and code by delegating to the constituent controls you place on the UserControl object.

  • Using Standard Control Property Types How to use the same property value enumerations other controls use.

  • Creating Design-Time-Only or Run-Time-Only Properties Making a property unavailable or read-only in different modes.

  • Marking a Property as the Properties Window Default How to identify property that will be highlighted first for a new control.

  • Grouping Properties by Category Creating and using property groups in the Properties window.

  • Properties You Should Provide Explains how to decide what properties your control should provide, and how to use the ActiveX Control Interface Wizard to make it easy.

For More Information The UserControl object is discussed in "The UserControl Object," earlier in this chapter. General information on creating properties for objects, such as making a property the default for an object, is provided in "Adding Properties and Methods to Classes," in "General Principles of Component Design."


Saving the Properties of Your Control

As discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter, instances of controls are continually being created and destroyed — when form designers are opened and closed, when projects are opened and closed, when projects are put into run mode, and so on.

How does a property of a control instance — for example, the Caption property of a Label control — get preserved through all this destruction and re-creation? Visual Basic stores the property values of a control instance in the file belonging to the container the control instance is placed on; .frm/.frx files for forms, .dob/.dox files for UserDocument objects, .ctl/.ctx files for UserControls, and .pag/.pgx files for property pages. Figure 9.13 illustrates this.

Figure 9.13 An .frm file contains saved control properties

When you author a control, you must include code to save your property values before a control instance is destroyed, and read them back in when the control instance is re-created. This is illustrated in Figure 9.14.

Figure 9.14 Saving and retrieving property values

Figure 9.14 is slightly oversimplified. You don't actually have to close a form to cause the WriteProperties event procedures of control instances to be executed. Saving a form file to disk causes WriteProperties events to be raised for all controls on the form.

Tip This topic explains the mechanism for saving and retrieving property values in code, but you won't normally have to write all the code described here. The ActiveX Control Interface Wizard can generate most of the code to save and retrieve your property values.

Saving Property Values

Use the PropertyBag object to save and retrieve property values. The PropertyBag is provided as a standard interface for saving property values, independent of the data format the container uses to save its source data.

The following code example uses the Masked property, a Boolean property described in the related topic "Adding Properties to Controls."

Private Sub UserControl_WriteProperties(PropBag As _
      PropertyBag)
   ' Save the value of the Boolean Masked property.
   PropBag.WriteProperty "Masked", Masked, False
   ' . . . more properties . . .
End Sub

The WriteProperty method of the PropertyBag object takes three arguments. First is a string that identifies the property being saved, followed by value to be saved — usually supplied by accessing the property, as shown above.

Note If the value you're saving is contained in the default property of a constituent control, and that property is of type Variant — for example, the Text property of a text box — you must specify the property name. That is, you must use Text1.Text instead of just Text1.

The last parameter is the default value for the property. In this case, the keyword False is supplied. Typically, you would create a global constant, such as PROPDEFAULT_MASKED, to contain this value, because you need to supply it in three different places, in the WriteProperties, ReadProperties, and InitProperties event procedures.

The Importance of Supplying Defaults

It may seem strange, at first, to be supplying a default property value when you're saving the value of a property. This is a courtesy to the user of your control, because it reduces the size of the .frm, .dob, .pag, or .ctl file belonging to the container of the control.

Visual Basic achieves this economy by writing out a line for the property only if the value is different from the default. Assuming that the default value of the Masked property is False, the WriteProperty method will write a line for the property only if the user has set it to True.

You can easily see how this technique reduces the size of .frm files by opening a new Standard EXE project, adding a CommandButton to Form1, and saving Form1.frm. Use a text editor such as Notepad or Wordpad to open Form1.frm, and compare the number of properties that were written to the file for Command1 to the number of properties in the Properties window for Command1.

Wherever possible, you should specify default values for the properties of your control when initializing, saving, and retrieving property values.

Retrieving Property Values

Property values are retrieved in the ReadProperties event of the UserControl object, as shown below.

Private Sub UserControl_ReadProperties(PropBag As _
      PropertyBag)
   On Error Resume Next
   ' Retrieve the value of the Masked property.
   Masked = PropBag.ReadProperty("Masked", False)
   ' . . . more properties . . .
End Sub

The ReadProperty method of the PropertyBag object takes two arguments: a string containing the name of the property, and a default value.

The ReadProperty method returns the saved property value, if there is one, or the default value if there is not. Assign the return value of the ReadProperty method to the property, as shown above, so that validation code in the Property Let statement is executed.

If you bypass the Property Let by assigning the property value directly to the private data member or constituent control property that stores the property value while your control is running, you will have to duplicate that validation code in the ReadProperties event.

Tip Always include error trapping in the UserControl_ReadProperties event procedure, to protect your control from invalid property values that may have been entered by users editing the .frm file with text editors.

Properties that are Read-Only at Run Time

If you create a property the user can set at design time, but which is read-only at run-time, you have a small problem in the ReadProperties event. You have to set the property value once at run time, to the value the user selected at design time.

An obvious way to solve this is to bypass the Property Let, but then you have no protection against invalid property values loaded from source files at design time. The correct solution to this problem is discussed in "Creating Design-Time-Only, Run-Time-Only, or Read-Only Run-Time Properties."

Initializing Property Values

You can assign the initial value of a property in the InitProperties event of the UserControl object. InitProperties occurs only once for each control instance, when the instance is first placed on a container.

Thereafter, as the control instance is destroyed and re-created for form closing and opening, project unloading and loading, running the project, and so on, the control instance will only receive ReadProperties events. This is discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter.

Be sure to initialize each property with the same default value you use when you save and retrieve the property value. Otherwise you will lose the benefits that defaults provide to your user, described in "The Importance of Supplying Defaults," earlier in this topic.

Tip The easiest way to ensure consistent use of default property values is to create global constants for them.

Saving and Retrieving Binary Data

Except for standard objects, such as Picture, the ReadProperty and WriteProperty methods have only limited ability to store and retrieve binary data. Storing binary data in strings is problematic because of Unicode conversion and line length limitations in the .frm file, but the two methods will accept arrays of type Byte.

If you're already keeping the data for a property in a Byte array, you can save it by passing the Byte array to the Value argument of the WriteProperty method:

' Private storage for the Blob property.
Private mbytBlob(0 To 1023) As Byte

Private Sub UserControl_WriteProperties(PropBag As _
      PropertyBag)
   ' Save the binary data for the Blob property.
   PropBag.WriteProperty "Blob", mbytBlob
   ' . . . more properties . . .
End Sub

The data will be saved in the .frx file. When retrieving the data, assign the return value from the ReadProperty method to mbytBlob. If mbytBlob is of variable length, you must save its size as a separate entry, using a name such as "BlobSize," so that you can retrieve the size and ReDim the array before retrieving the data. (You do not need to have a BlobSize property in order to do this.)

You can also use this technique to store any binary property data that you can manage to copy to a Byte array.


Exposing Properties of Constituent Controls

By default, the properties of the UserControl object — and the constituent controls you add to it — are not visible to the end user of your control. This gives you total freedom to determine your control's interface.

Frequently, however, you will want to implement properties of your control by simply delegating to existing properties of the UserControl object, or of the constituent controls you've placed on it. This topic explains the manual technique of exposing properties of the UserControl object or its constituent controls.

Understanding delegation and property mapping will help you get the most out of the ActiveX Control Interface Wizard, which is designed to automate as much of the process as possible. It will also enable you to deal with cases that are too complicated for the wizard to handle.

Exposing Properties by Delegating

Suppose you want to create a control that allows the end user to edit a field with a special format, such as a Driver's License Number. You start by placing a single text box on a UserControl, and naming it something catchy like txtBase.

Because your new control is an enhancement of a single Visual Basic control, you also resize txtBase to fit the UserControl. You do this in the UserControl's Resize event, as discussed in "Providing Appearance Using Constituent Controls," earlier in this chapter.

To create the BackColor property of your control, you can simply expose the BackColor property of the text box, as shown in this code fragment from the UserControl's code module.

Public Property Get BackColor() As OLE_COLOR
   BackColor = txtBase.BackColor
End Property

Public Property Let BackColor(ByVal NewColor _
      As OLE_COLOR)
   ' . . . property validation code . . .
   txtBase.BackColor = NewColor
   PropertyChanged "BackColor"
End Property

The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," earlier in this chapter.

The BackColor property you create for your control simply saves its value in the BackColor property of the text box control. Methods are exposed in similar fashion, by delegating the work to the corresponding method of the control you're enhancing. Delegation is discussed in "Composing Objects," in "Programming with Objects," in the Visual Basic Programmer's Guide.

Tip When you use the OLE_COLOR data type for color properties, the Properties window will automatically show the ellipsis button that brings up the standard color selection dialog box. Standard property types are discussed in the related topic "Using Standard Control Property Types."

Note User controls don't support Dynamic Data Exchange (DDE). If you delegate DDE-related properties such as LinkMode or events such as LinkOpen, they will be displayed at design time but will cause an error at run time.

Important Because properties of the UserControl object and constituent controls are exposed by delegation, you cannot expose design-time-only properties such as Appearance and ClipControls. The settings you choose for such properties will be fixed for your ActiveX control.

Mapping Properties to Multiple Controls

Frequently you will want to map more than one constituent control property to a property of your control. Delegation gives you the flexibility to handle this situation.

Suppose, for example, that you've created your control's appearance by placing several check boxes, option buttons, and labels on a UserControl. It makes sense for the BackColor of your UserControl to be the background color of your control. However, it also makes sense for the BackColor properties of the constituent controls to match this color.

The following code fragment illustrates such an implementation:

Public Property Get BackColor() As OLE_COLOR
   BackColor = UserControl.BackColor
End Property

Public Property Let BackColor(ByVal NewColor _
      As OLE_COLOR)
   Dim objCtl As Object
   ' . . . property validation code . . .
   UserControl.BackColor = NewColor
   For Each objCtl In Controls
      If (TypeOf objCtl Is OptionButton) _
            Or (TypeOf objCtl Is CheckBox) _
            Or (TypeOf objCtl Is Label) _
         Then objCtl.BackColor = NewColor
   Next
   PropertyChanged "BackColor"
End Property

When the property value is read, the value is always supplied from the BackColor property of the UserControl. Always choose a single source to be used for the Property Get.

Note When you give your control a property which the underlying UserControl object already possesses, using that property name in your code will refer to the new property you have defined, unless you qualify the property name with UserControl, as shown above.

For More Information Using the Controls collection to iterate over all constituent controls is discussed in "Control Creation Terminology," earlier in this chapter. The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," earlier in this chapter.

Multiple BackColor Properties

The implementation above raises an interesting question. What if you want to provide the user of your control with a way to set the background color of all the text boxes on your control? You've already mapped BackColor to its most natural use, but you can always get creative with property names.

For example, you might add a TextBackColor property, modeled on the example above, that would set the BackColor properties of all the text boxes on your control. Choose one text box as the source of the TextBackColor, for the Property Get, and you're in business. (It doesn't make much sense to use the UserControl's BackColor for this purpose.)

Mapping to Multiple Object Properties

As another example of multiple property mapping, you might implement TextFont and LabelFont properties for the control described above. One property would control the font for all the labels, and the other for all the text boxes.

When implementing multiple mapped object properties, you can take advantage of multiple object references. Thus you might implement the LabelFont property as shown in the following code fragment:

Public Property Get LabelFont() As Font
   Set LabelFont = UserControl.Font
End Property

' Use Property Set for object properties.
Public Property Set LabelFont(ByVal NewFont As Font)
   Set UserControl.Font = NewFont
   SyncLabelFonts
   PropertyChanged "LabelFont"
End Property

Private Sub SyncLabelFonts()
   Dim objCtl As Object
   For Each objCtl In Controls
      If TypeOf objCtl Is Label Then
         Set objCtl.Font = UserControl.Font
      End If
   Next
End Sub

The code in the SyncLabelFonts helper function assigns to each Label control's Font property a reference to the UserControl object's Font object. Because all of the controls have references to the same Font object, changes to that font will be reflected in all the labels.

A helper function is used because the same code must be executed when your control is initialized, and when saved properties are read.

Note The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," earlier in this chapter.

The code to initialize, save, and retrieve the LabelFont property is shown below. Optionally, you can set the characteristics of the UserControl's font to match those of the container's font, as discussed in "Using the AmbientProperties Object to Stay Consistent with the Container."

Private Sub UserControl_InitProperties()
   SyncLabelFonts
End Sub

Private Sub UserControl_ReadProperties(PropBag As _
      VB.PropertyBag)
   On Error Resume Next
   Set LabelFont = PropBag.ReadProperty("LabelFont")
End Sub

Private Sub UserControl_WriteProperties(PropBag As _
      VB.PropertyBag)
   PropBag.WriteProperty "LabelFont", LabelFont
End Sub

Because the Font object is a standard object, it can be saved and retrieved using PropertyBags.

The developer using your control can now use the Property window to set a font for the LabelFont property. Supposing that the name you give your control is MultiEdit, she can also use code like the following at run time:

Private Sub Command1_Click()
   YourControl1.LabelFont.Bold = True
   YourControl1.LabelFont.Name = "Monotype Sorts"
End Sub

The beauty of this code is that it never calls the Property Let for the LabelFont property, as you can verify by adding the code above to a UserControl that has several constituent Label controls, and putting breakpoints in the Property Get and Property Let.

When Visual Basic executes the first line above, the Property Get is called, returning a reference to the UserControl's Font object. The Bold property of the Font object is set, using this reference. Because the constituent Label controls all have references to the UserControl's Font object, the change is reflected immediately.

Don't Expose Constituent Controls as Properties

You might wonder why you shouldn't simply expose constituent controls whole. For example, if your UserControl has a text box control named Text1 on it, you might try to write the following:

' Kids, don't try this at home.
Property Get Text1() As TextBox
   Set Text1 = Text1
End Property

The user of your control could then access all the properties and methods of Text1, and you've only written one line of code.

The code above will not compile, because TextBox is not a public data type. But that's not the real reason this is a bad idea.

It might simplify your life to expose all the properties and methods of a constituent control, rather than selectively exposing them, but consider the experience that awaits the user of your control. He now has direct access to the Text property of Text1, bypassing any validation code you might have written in a Property Let. He can also change the height and width of the text box, which may completely wreck the code you've written in your UserControl_Resize event procedure.

All in all, the developer is likely to conclude that your control is too buggy to be worth using. But wait, there's more. If the developer uses your control with other development tools, such as Microsoft Excel or Microsoft Access, type library information for the constituent controls will not be available. All references to Text1 will be late bound, so your control will be not only buggy, but slow.

Exposing constituent controls also limits your ability to change your control's implementation in the future. For example, you might want to base a future version of your control on a different constituent control than the intrinsic TextBox control. Unless the properties and methods of this SuperTextBox exactly matched those of the intrinsic TextBox, your users would be unable to upgrade without rewriting their code.

It's good programming practice to expose only those constituent control properties required for the operation of your control. For example, if the text box mentioned above holds a user name, you might expose the value of Text1.Text through a UserName property.

Using the ActiveX Control Interface Wizard

When you have a large number of constituent controls, or even one constituent control with many properties you wish to expose, the ActiveX Control Interface Wizard can significantly reduce the amount of work required to expose constituent control properties.

Using the ActiveX Control Interface Wizard is discussed in the related topic "Properties You Should Provide."


Using Standard Control Property Types

The code examples in the related topic "Exposing Properties of Constituent Controls" show the use of the standard types Font and OLE_COLOR to create properties that use standard, system-supplied property pages, which the user can access with the ellipsis button in the Properties window.

Whenever possible, use the standard data types and enumerations provided by Visual Basic as data types of your control's properties. This makes life easy for your users, by presenting consistent property value choices in the Properties window.

Standard Enumerations

The following code uses the standard enumeration for the MousePointer property.

Public Property Get MousePointer() As _
      MousePointerConstants
   MousePointer = UserControl.MousePointer
End Property

Public Property Let MousePointer(ByVal NewPointer As _
      MousePointerConstants)
   UserControl.MousePointer = NewPointer
   PropertyChanged "MousePointer"
End Property

When the MousePointer property appears in the Properties window, it will have the same enumeration as the MousePointer properties of other controls.

You can use the Object Browser to determine what enumerations are available in the Visual Basic type libraries.

Note The purpose and importance of PropertyChanged are discussed in "Adding Properties to Controls," earlier in this chapter.

Standard Data Types

Visual Basic provides four standard data types of special interest to control authors.

OLE_COLOR

The OLE_COLOR data type is used for properties that return colors. When a property is declared as OLE_COLOR, the Properties window will display a color-picker dialog that allows the user to select the color for the property visually, rather than having to remember the numeric equivalent.

An example of the use of OLE_COLOR can be found in "Exposing Properties of Constituent Controls."

OLE_COLOR is treated internally as a Long.

OLE_TRISTATE

The OLE_TRISTATE data type is used for three-state check boxes. If you're authoring a control with check box functionality, declare its Value property as OLE_TRISTATE.

OLE_TRISTATE is an enumeration with the following values:

  • 0 - Unchecked

  • 1 - Checked

  • 2 - Gray

OLE_OPTEXCLUSIVE

If you're developing a control with option-button functionality, use the OLE_OPTEXCLUSIVE data type for the Value property of your control. This will cause your control's Value property to behave like that of the intrinsic OptionButton control. That is, when instances of your control are grouped, and the user clicks an unselected control instance, the currently selected instance's Value is automatically set to 0 (thus unselecting the button), and the Value of the clicked instance is set to 1.

This behavior is handled by the container. The container checks the Value property for each control it contains, and groups those that are of type OLE_OPTEXCLUSIVE.

Note You must use the Procedure Attributes dialog box to make the Value property the default property, in order for the control host to enable the behavior described.

OLE_OPTEXCLUSIVE is handled as a Boolean type internally.

OLE_CANCELBOOL

Use this data type for an event argument that allows the user to cancel the event. For example, the standard KeyPress event passes a Cancel parameter as its last parameter. If the user sets this parameter to False, the event is canceled.

OLE_CANCELBOOL is handled as a Boolean type internally.


Creating Design-Time-Only, Run-Time-Only, or Read-Only Run-Time Properties

To create a property that can be read at run time, but can be set only at design time, implement the property using property procedures. In the Property Let or Property Set procedure, test the UserMode property of the AmbientProperties object. If UserMode is True, raise an error, as shown in the following code fragment:

Private mdblSerendipity As Double

Property Get Serendipity() As Double
   Serendipity = mdblSerendipity
End Property

Property Let Serendipity() As Double
   ' (Code to validate property values omitted.)
   If Ambient.UserMode Then
      Err.Raise Number:=382 _
         Description:= _
         "Let/Set not supported at run time."
   End If
   Serendipity = mdblSerendipity
   PropertyChanged "Serendipity"
End Property

To suppress a property completely at run time, you can also raise a "Property is not available at run time" error in Property Get.

Note Implementing properties of the Variant data type requires all three property procedures, Property Get, Property Let, and Property Set, because the user can assign any data type, including object references, to the property. In that case, the error raised in Property Let must also be raised in Property Set.

Error Values to Use for Property Errors

The following error values are provided by Visual Basic and should be used when raising errors for read-only or write-only properties:

Err.Number | Err.Description
382 | Let/Set not supported at run time.
383 | Let/Set not supported at design time.
393 | Get not supported at run time.
394 | Get not supported at design time.

If a property is read-only at run time and UserMode is True, raise error 382 in the Property Let or Property Set procedure. If a property is not available at run time, raise error 382 in the Let or Set procedure and error 383 in the Get procedure. Likewise, if a property is not available at design time, raise error 393 in the Let or Set procedure and error 394 in the Get procedure.

Handling Read-Only Run-Time Properties in the ReadProperties Event

The recommended practice for the ReadProperties event is to assign the retrieved value to the property, so that the Property Let is invoked. This allows the validation code in the Property Let to handle invalid values the user has manually entered into the container's source file, as described in "Saving the Properties of Your Control."

Clearly, this is problematic for read-only run-time properties. The solution is to bypass the Property Let, and assign the retrieved value directly to the private member or constituent control property. If the property accepts only certain values, you can use a helper function that can be called from both Property Let and ReadProperties.

The following code fragment illustrates these two solutions:

Private Sub UserControl_ReadProperties(PropBag As _
      PropertyBag)
   ' Always use error trapping in ReadProperties!
   On Error Resume Next
   ' Retrieve the value of the HasWidgets property,
   ' which is read-only at run time.
   mblnHasWidgets = _
      PropBag.ReadProperty("HasWidgets", False)
   If Err.Number <> 0 Then
      ' If the .frm file contained a value that caused
      '   a type mismatch (error 13), substitute the
      '   default value for the property.
      mblnHasWidgets = False
      ' When using On Error Resume Next, always
      '   reset Err.Number after an error.
      Err.Number = 0
   End If

   ' Retrieve the value of the Appearance property,
   ' which can be set at design time
   ' and has two valid values, Appears3D
   ' and AppearsFlat.  (These constants should be
   ' defined in a Public Enum.)
   mintAppearance = _
      PropBag.ReadProperty("Appearance", Appears3D)
   ' Validate the value retrieved from the .frm
   '   file.
   Call ValidateAppearance(mintAppearance)
   If Err.Number <> 0 Then
      ' If the .frm file contained an invalid integer
      '   value (error 380) or a value that caused a
      '   type mismatch (error 13), substitute the
      '   default value for the property.
      mintAppearance = Appears3D
      ' When using On Error Resume Next, always
      '   reset Err.Number after an error.
      Err.Number = 0
   End If

   ' . . . more properties . . .
End Sub

The Property Let for the Appearance property would call the same ValidateAppearance helper function used in the example above, but would not trap errors — thus the error would be raised at the line of code that assigned the invalid value. The helper function might look something like this:

Private Sub ValidateAppearance(ByVal Test As Integer)
   Select Case Test
      Case Appears3D
      Case AppearsFlat
      Case Else
         ' Error 380 is the standard "Invalid property
         '   value" error.
         Err.Raise 380
   End Select
End Sub

Important If the wrong data type is entered in the source file, a type mismatch error will occur. Thus, errors can occur even for a Boolean or numeric property. (This is why you should always use error trapping in ReadProperties.) You can trap the error with On Error Resume Next, as above, and substitute the default value for the property.

Creating Run-Time-Only Properties

You can create a property that is available only at run time by causing property procedures to fail during design time (that is, when the UserMode property of the AmbientProperties object is False).

Visual Basic's Properties window does not display properties that fail during design-time.

Tip You can open the Procedure Attributes dialog box, select your run-time-only property, click the Advanced button, and check "Don't show in Property Browser" to prevent the Properties window from interrogating the property. This keeps the Properties window from putting you in break mode every time it queries the property, which is a nuisance when you're debugging design-time behavior.


Marking a Property as the Properties Window Default

When you first place a new control instance on a form or other container, Visual Basic chooses a property to highlight the Properties window. This will be the same as the last property that was highlighted, if the new control has that property. Otherwise, Visual Basic uses the property marked as the user interface default.

If you don't specify this user interface default, Visual Basic highlights a property according to various internal criteria, such as the order in which you added properties to the type library.

To specify the user interface default for your control

  1. In the UserControl code window, place the cursor in one of the property procedures for the property.

  2. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  3. The property should be shown in the Name box. If not, use the Name box to select it.

  4. Click the Advanced button to show advanced features. Check User Interface Default in the Attributes box, then click OK or Apply.

Tip The best candidate for the user interface default is the property users will most often set. For example, the Interval property is the user interface default for the Timer control.


Grouping Properties by Category

The Properties window has two tabs, one showing all of a control's properties in alphabetical order, and one which organizes the properties into categories.

You can assign each of your control's properties to one of the existing categories, or create new categories. Assigning categories is highly recommended, because Visual Basic places all unassigned properties in the Misc category.

To assign a property to a property category

  1. In the UserControl code window, place the cursor in one of the property procedures for the property you want to assign to a category.

  2. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  3. The property should be shown in the Name box. If not, use the Name box to select it.

  4. Click the Advanced button to show advanced features. Select the desired category in the Property Category box, then click OK or Apply.

You can create a new category by typing a category name in the Property Category box. The category will be created only for this control. You can use the same category name for other controls, but it must be entered separately for each control.

Tip To reduce user confusion, assign properties to the same categories they appear in for other controls. If possible, use existing categories. Create new categories only when you have a group of related properties that will clearly be easier to use if grouped in a new category.


Properties You Should Provide

Recommended properties include Appearance, BackColor, BackStyle, BorderStyle, Enabled, Font, and ForeColor. It's also a good idea to implement properties commonly found on controls that provide functionality similar to yours.

In addition, you may wish to selectively implement properties of any constituent controls on your UserControl object, as discussed in "Exposing Properties of Constituent Controls," earlier in this chapter.

All of the above properties should use the appropriate data types or enumerations, as discussed in the related topics "Using Standard Control Property Types" and "Exposing Properties of Constituent Controls."

Note If you're authoring a control that provides its appearance using constituent controls, implementing the Appearance property is problematic. For most controls, the Appearance property is available only at design time — but you can only delegate to run-time properties of constituent controls.

Procedure IDs for Standard Properties

Every property or method in your type library has an identification number, called a procedure ID or DISPID. The property or method can be accessed either by name (late binding) or by DISPID (early binding).

Some properties and methods are important enough to have special DISPIDs, defined by the ActiveX specification. These standard procedure IDs are used by some programs and system functions to access standard properties of your control.

For example, there's a procedure ID for the method that displays an About Box for a control. Rather than rummaging through your type library for a method named AboutBox, Visual Basic calls this procedure ID. Your method can have any name at all, as long as it has the right procedure ID.

To assign a standard procedure ID to a property

  1. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  2. In the Name box, select the property.

  3. Click Advanced to expand the Procedure Attributes dialog box.

  4. In the Procedure ID box, select the procedure ID you want to assign to the property. If the procedure ID you need is not in the list, enter the number in the Procedure ID box.

Important Selecting (None) in the procedure ID box does not mean that the property or method will not have a procedure ID. It only means that you have not selected a particular procedure ID. Visual Basic assigns procedure IDs automatically to members marked (None).

One Procedure ID to a Customer

A property or method of a control can have only one procedure ID, and no other property or method of the control can have the same procedure ID.

That is, every control in your control component can have a default property (procedure ID = 0), but only one property on each control can have that procedure ID.

If you assign the same procedure ID to two different members, the one that comes first in the type library is the only one that can be accessed. The other might as well not exist.

Procedure IDs of Interest

It's always a good idea to assign the standard procedure ID to a property, if there is one. The Procedure Attributes dialog box lists procedure IDs by the property name they are usually associated with. You may find the following IDs of particular interest.

AboutBox

Allows you to specify a method that shows an About Box for your control, as discussed in "Adding an About Box to Your Control," earlier in this chapter. There is no particular method name associated with this ID.

Caption, Text

Either of these procedure IDs will give a property the Properties window behavior demonstrated by the Caption and Text properties of Visual Basic intrinsic controls. That is, when a user types a value into the Properties window, the new value will be reflected immediately in the control.

This means that your Property Let procedure will be called for each keystroke the user enters, receiving a complete new value each time.

The property you assign these to need not be called Caption or Text, although those properties represent the kind of functionality these procedure IDs were designed to support.

(Default)

The default property of a control is the one that will be accessed when no property has been specified. For example, the following assigns the string "Struthiomimus" to the (default) Caption property of Label1:

   Label1 = "Struthiomimus"

Enabled

This procedure ID must be assigned to the Enabled property of your control, in order for its enabled/disabled behavior to match that of other controls. This is discussed in "Allowing Your Control to be Enabled or Disabled," earlier in this chapter.

For More Information The procedure IDs of interest to control authors are listed in the Procedure ID box of the Procedure Attributes dialog box. For a complete list of DISPIDs, consult the ActiveX specification.

Providing Useful Defaults

Whenever you implement a property with the same name as one of the standard ambient properties, such as BackColor, Font, and ForeColor, you should consider whether the value of the corresponding property of the AmbientProperties object would be a useful default.

You can see an example of this behavior by changing the size of the font on a Visual Basic form, and then adding a label or command button. The new control uses the form's current font settings as its default font settings. Most of the intrinsic controls follow this example.

If you're authoring a check box, option button, or label, setting the control's default BackColor to match AmbientProperties.BackColor might be a useful service to your users.

Clearly, this requires some thought about how controls are used. For example, on a text box the Font property would be a good candidate for ambient matching, while the BackColor property would not.

For More Information See "Using the AmbientProperties Object to Stay Consistent with the Container," earlier in this chapter.

Using the ActiveX Control Interface Wizard

The ActiveX Control Interface Wizard can assist you in determining what properties to provide, and in delegating to the appropriate constituent controls.

After you have placed all the constituent controls you're going to use on your UserControl, start the wizard and select your control. The wizard will examine your constituent controls, and produce a list of all the properties, methods, and events that appear in all their interfaces, plus those in the UserControl object's interface, and the standard properties listed above. You can select from this list those properties, methods, and events you want in your control's interface.

The wizard will produce default mappings of your control's properties to properties of the UserControl object or of constituent controls. In subsequent steps, you can modify these mappings.

When you have finished determining your control's interface and delegating to existing properties, the wizard will generate property procedure code to implement the properties, using the correct data types for standard properties, and including delegation code for all your property mappings, enormously reducing the amount of work required to generate a full-featured control.


Adding Methods to Controls

You implement methods of your ActiveX control by adding Public Sub and Function procedures to the code module of the UserControl that forms the basis of your control class.

By default, the only methods your control will have are the extender methods provided by the container, such as the Move method. You can decide what additional methods your control needs, and add code to implement them.

Standard Methods

If your control is not invisible at run time, you should provide a Refresh method. This method should simply call UserControl.Refresh. For user-drawn controls, this will raise the Paint event; for controls built using constituent controls, it will force a refresh of the constituent controls.

It's also a good idea to implement methods commonly found on controls that provide functionality similar to yours. In addition, you may wish to selectively implement methods of the UserControl object, or of its constituent controls.

Using the ActiveX Control Interface Wizard

The ActiveX Control Interface Wizard can assist you in determining what methods to provide, and in delegating to the appropriate constituent controls.

After you have placed all the constituent controls you're going to use on your UserControl, start the wizard and select your control. The wizard will examine your constituent controls, and produce a list of all the properties, methods, and events that appear in all their interfaces, plus those in the UserControl object's interface. You can select from this list those properties, methods, and events you want in your control's interface.

The wizard will produce default mappings of your control's methods to methods of the UserControl object or of constituent controls. In subsequent steps, you can modify these mappings.

When you have finished with determining your control's interface, and delegating to existing methods, the wizard will generate Sub and Function procedures to implement the properties, including delegation code for all your mappings. This greatly reduces the amount of work required to generate a full-featured control.


Raising Events from Controls

It's important to distinguish between the events received by your UserControl object (or by the controls it contains) and the events your control raises. Events your control receives are opportunities for you to do something interesting; events your control raises provide opportunities for the developer who uses your control to do something interesting.

This principle is demonstrated, with illustrations, in the step-by-step procedure "Adding an Event to the ShapeLabel Control" in "Creating an ActiveX Control."

The following related topics explain control events in detail.

  • Exposing Events of Constituent Controls Describes how to use the events of the UserControl object and its constituent controls to generate events for your ActiveX control.

  • Events the Container Provides for Your Control Discuses events provided for the developer by the container, independent of the control author.

  • Specifying a Default Event for the Code Window Describes how to specify which of your control's events is selected by default in the code window.

  • Events Your Control Should Raise Explains how to determine what events your control should raise, and how to use the ActiveX Control Interface Wizard to simplify the creation of events.

For More Information General information on using events in components, including restrictions on arguments, can be found in "Adding Events to Classes" in "General Principles of Component Design."


Exposing Events of Constituent Controls

The mechanism for exposing events is different from the delegation used to expose properties and methods. You expose an event in a constituent control by raising your own event, as in the following code fragment from a UserControl code module:

' Declaration of your control's Click event.
Public Event Click()

' When the txtBase text box control raises a Click,
'   your control forwards it by raising the Click
'   event you declared.
Private Sub txtBase_Click()
   RaiseEvent Click
End Sub

' You may also want to raise your control's Click event
'   when a user clicks on the UserControl object.
Private Sub UserControl_Click()
   RaiseEvent Click
End Sub

Notice that your Click event may be raised from multiple locations in your code. You can add your own code before and after raising the Click event.

The Difference Between Events and Properties or Methods

It may help to think of properties and methods as incoming, and events as outgoing. That is, methods are invoked from outside your control, by the developer who's using your control. Thus, the developer invokes a method of your UserControl object, and you respond by delegating to the method of your constituent control.

By contrast, events originate in your control and are propagated outward to the developer, so that she can execute code in her event procedures. Thus, your UserControl object responds to a click event from one of its constituent controls by raising its own Click event, thus forwarding the event outward to the developer.

Mouse Events and Translating Coordinates

The MouseDown, MouseMove, and MouseUp event procedures for the UserControl object have arguments giving the event location in the ScaleMode of the UserControl. Before raising your control's MouseDown, MouseMove, and MouseUp events, you must translate the event location to the coordinates of the container.

How do you know what the container's ScaleMode is? You don't. You don't even know if the container has a ScaleMode property. Fortunately, you don't have to worry about that, because Visual Basic provides the ScaleX and ScaleY methods for translating coordinates.

The following code fragment shows how to expose the MouseMove event from a UserControl whose ScaleMode is Twips. It assumes that the MouseMove event has been declared with the correct parameters (you can use the ActiveX Control Interface Wizard to generate event declarations).

Private Sub UserControl_MouseMove(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)    RaiseEvent MouseMove(Button, Shift, _
      ScaleX(X, vbTwips, vbContainerPosition), _
      ScaleY(Y, vbTwips, vbContainerPosition))
End Sub

The ScaleX method translates from vbTwips to whatever the coordinate system of the container is; this information is obtained by Visual Basic using low-level ActiveX calls. The return value from ScaleX (the new X coordinate) is passed as the X argument of the MouseMove event.

If you change the ScaleMode of your UserControl at run time, you should use UserControl.ScaleMode instead of vbTwips in the code above, so that the correct translation is done regardless of current ScaleMode setting.

Importance of Testing

The code above, with its explicit vbTwips, will have to be changed if the ScaleMode of the UserControl is permanently changed at design time. It's important to test your control's response to mouse moves on forms with a variety of ScaleMode settings, to ensure that ScaleX and ScaleY have the correct arguments.

Constituent Controls

If your control includes constituent controls that have mouse events, you'll have to raise your control's mouse events there, too. Otherwise there will appear to be dead spots on your control, where the mouse events don't occur.

Constituent control mouse events are slightly more complicated, because they provide X and Y in the coordinates of the constituent control rather than the UserControl. The following code fragment shows how this is handled when the constituent control has the same ScaleMode as the UserControl:

Private Sub Label1_MouseMove(Button As Integer, _
      Shift As Integer, X As Single, Y As Single)    RaiseEvent MouseMove(Button, Shift, _
      ScaleX(X + Label1.Left, _
         vbTwips, vbContainerPosition), _
      ScaleY(Y + Label1.Top, _
         vbTwips, vbContainerPosition))
End Sub

Adding the Left and Top properties of Label1 to the X and Y coordinates translates them to UserControl coordinates; ScaleX and ScaleY then transform the results to container coordinates.

If the UserControl's ScaleMode were set to Pixels, you would first have to call ScaleX and ScaleY to translate X and Y to pixels, before adding the results to Label1.Top and Label1.Left. The results of the addition would then be passed to ScaleX and ScaleY, as above.

Other Events that Provide Position

If you create events of your own that pass location information to the container, you must use the same technique to transform the locations into the container's coordinates. If you want to pass width and height information, use vbContainerSize instead of vbContainerPosition when calling ScaleX and ScaleY.

Using the ActiveX Control Interface Wizard

The ActiveX Control Interface Wizard can greatly simplify the task of forwarding events. This is discussed in the related topic "Events Your Control Should Raise."


Events the Container Provides for Your Control

The container's extender object may provide events for the benefit of developers using your control. If your control is used with Visual Basic, the user of your control gets four such events without any work on your part: GotFocus, LostFocus, DragOver, and DragDrop.

You don't need to be concerned with extender events. They are invisible to your control, and you cannot put code in their event procedures.


Specifying a Default Event for the Code Window

The first time you select a control instance in the Object box of the container's code window, Visual Basic selects an event to highlight in the Procedure box, and inserts into the code window an event procedure for that event.

If a control doesn't specify this user interface default, Visual Basic selects the first event alphabetically.

Note While you're working with a new control in the development environment, before you've specified the user interface default, Visual Basic may sometimes select the default event based on internal considerations, such as which event went into the type library first.

To specify the user interface default for your control's events

  1. In the UserControl code window, place the cursor on the declaration of the event you want to specify as the user interface default.

  2. On the Tools menu, click Procedure Attributes to open the Procedure Attributes dialog box.

  3. The property should be shown in the Name box. If not, use the Name box to select it.

  4. Click the Advanced button to show advanced features. Check User Interface Default in the Attributes box, then click OK or Apply.

Tip You can only mark one event as the user interface default. Choose the event users will most frequently place code in.


Events Your Control Should Raise

Recommended events include Click, DblClick, KeyDown, KeyPress, KeyUp, MouseDown, MouseMove, and MouseUp. It's also a good idea to implement events commonly found on controls that provide functionality similar to yours.

In addition, you may wish to selectively implement events of the constituent controls on your UserControl object, or of the UserControl object itself.

It's important to use the same arguments, with the same data types, as these standard events, as discussed in "Exposing Events of Constituent Controls." Data types are discussed in "Using Standard Control Property Types."

Using the ActiveX Control Interface Wizard

The ActiveX Control Interface Wizard can assist you in determining what events to provide, and in forwarding the appropriate constituent control events.

After you have placed all the constituent controls you're going to use on your UserControl, start the wizard and select your control. The wizard will examine your constituent controls, and produce a list of all the properties, methods, and events that appear in all their interfaces, plus those in the UserControl object's interface, and the standard events listed above. You can select from this list those properties, methods, and events you want in your control's interface.

The wizard will produce default mappings of your control's events to events of the UserControl object or of constituent controls. In subsequent steps, you can modify these mappings.

When you have finished determining your control's interface, the wizard will generate code to raise the events you've selected, using the correct arguments and data types for standard events, and including event forwarding code for all your event mappings. This enormously reduces the amount of work required to generate a full-featured control.


Providing Named Constants for Your Control

As with other component types, public enumerations can be shared by all of the controls in a control component (.ocx file). Place public Enums for your component in any UserControl code module.

"Providing Named Constants for Your Component," in "General Principles of Component Design," discusses techniques for providing constants, validating constants in properties, and so forth. See that topic for general information on the subject.

There are two additional factors specific to control components:

  • Enum member names are used in the Properties window.

  • Global objects cannot be used to simulate string constants.

Enum Member Names in the Properties Window

As an example of the first factor, consider the following Enum and property:

Public Enum DINOSAUR
   dnoTyrannosaur
   dnoVelociraptor
   dnoTriceratops
End Enum

Private mdnoFavoriteDinosaur As DINOSAUR

Public Property Get FavoriteDinosaur() As DINOSAUR
   FavoriteDinosaur = mdnoFavoriteDinosaur
End Property

Public Property Let FavoriteDinosaur(ByVal NewDino _
      As DINOSAUR
   mdnoFavoriteDinosaur = NewDino
   PropertyChanged "FavoriteDinosaur"
End Property

When you set the FavoriteDinosaur property in the Properties window, the drop down list will contain dnoTyrannosaur, dnoVelociraptor, and dnoTriceratops.

As you can see, there's a fine tradeoff here between names that will look good in the drop down, and names that will avoid collisions with names used in Enums for other components.

As a rule of thumb, don't abandon the prefix ("dno" in the example above) that groups constants in global lists. The prefix provides at least some protection from name conflicts. On the other hand, don't make your prefixes so long that they obscure the names.

Cannot Simulate String Constants Using Global Objects

Class modules in control components can have one of two values of the Instancing property, Private or PublicNotCreatable. The Instancing values that enable global objects are not available in control components, so it is not possible to simulate string constants using properties of global objects, as described in "Providing Named Constants for Your Component" in "General Principles of Component Design."


Setting Up a New Control Project and Test Project

As discussed in "Two Ways to Package ActiveX Controls," earlier in this chapter, Visual Basic enables you to author shareable control components (.ocx files), or to include private controls as .ctl files in your component project. These two scenarios have different testing requirements.

Testing Private Controls

The only way to test a private control is to place it on a form within the project. Of course, there may be several forms in the project that use the control, but it is recommended that you create a separate form for testing your private controls.

The reason for this is that simply using the control is not likely to test it exhaustively. Once your application or component is compiled, user actions you have not anticipated may cause unexpected results in your control's code.

By including a test form that exercises all of your control's interface members, you can test your control more thoroughly.

Testing Controls in Control Components

When you're developing a control component, you need thorough test coverage of all aspects of your control. This coverage is best provided using a separate test project. Visual Basic allows you to run multiple projects, so you can load your test project and ActiveX control project and run them together for debugging purposes.

Once you have compiled your control component, the test project can be used as a test harness for quality assurance test suites.

If you use the ActiveX Control Interface Wizard to build the interface and generate code for your control, you can get a test project created by simply checking an option on the wizard's final screen.

Examples of creating a new ActiveX control project and a test project can be found in the step-by-step procedures "Creating the ControlDemo Project" and "Adding the TestCtlDemo Project," in "Creating an ActiveX Control."

Tip You may prefer to author your controls as private controls in a Standard EXE project, and to test them by placing them on forms within the project. When you're ready to compile an .ocx file, you can remove the .ctl files from the Standard EXE project and add them to an ActiveX control project.

You can then set up the Standard EXE project as a test harness, using the Controls tab of the Components dialog box to add your controls to the Toolbox, as described in "Compiling the ControlDemo Component," in "Creating an ActiveX Control."

For More Information "Two Ways to Package ActiveX Controls" lists several reasons why you might want to create a control component, even if you're just distributing controls as part of your own applications.


Creating Robust Controls

For your user, the three most important things about an ActiveX control are robustness, robustness, and robustness. Because a control component runs in the process space of an application that uses it, fatal errors in your controls are also fatal errors for the application.

The following lists of DOs and DON'Ts are by no means inclusive. They only provide a starting point for producing robust controls.

Error Handling

DO | DON'T
Provide thorough error handling in every event procedure you put code in, whether the event belongs to the UserControl or to a constituent control. | Raise errors in any event procedures.
In particular, provide thorough error handling in the UserControl's Paint, Resize, and Terminate events. |

Unhandled errors in event procedures will be fatal to your control component, and the application using it, because there will never be a procedure on the call stack that can handle the errors.

It's perfectly safe to raise errors in property procedures and methods, because properties and methods are always invoked by other procedures, and errors you raise can be handled by the user in those procedures.

Object Models

If your control component includes dependent objects, such as a collection of ToolbarButton objects for a Toolbar control:

DO | DON'T
Create wrapper classes for collections of such objects, as described in "General Principles of Component Design" and "Standards and Guidelines." | Use the Collection object without a wrapper class. The Collection object accepts any variant, meaning your users could accidentally insert objects that might cause errors in your control's code.
Use property procedures for collection properties. | Implement such properties as simple data members.

For example, if you create a ToolbarButtons class as the wrapper class for a collection of ToolbarButton objects, add the property to your UserControl object as a read-only property procedure:

Private mToolbarButtons As ToolbarButtons

Property Get ToolbarButtons() As ToolbarButtons
   Set ToolbarButtons = mToolbarButtons
End Property

Private Sub UserControl_Initialize()
   Set mToolbarButtons = New ToolbarButtons
End Sub

By contrast, the following implementation allows your user to accidentally set ToolbarButtons to Nothing, destroying the collection:

Public ToolbarButtons As New ToolbarButtons

Implementing Properties

DO implement properties using property procedures, instead of public data members.

You can use Property Let to validate property values. If you use public data members, you'll have to check the data every time you use it; and if that happens in an event procedure, you won't be able to raise an error without bringing down the application that's using your control component.

In addition, your properties will not work correctly in the Properties window and Property Pages dialog box, as discussed in "Adding Properties to Your Control," earlier in this chapter.


Debugging Controls

The most important difference between debugging controls and debugging other objects is that some of the code in your control must execute while the form a control instance is placed on is in design mode.

For example, the code in the property procedures you use to implement your control's properties must execute when the developer using your control sets its properties using the Properties window.

Code that saves and retrieves your control's property values must also run at design time, whenever the user loads a form containing an instance of the control, puts the project in Run mode, or saves the project to disk.

Code in the Resize event (or the Paint event, for user-drawn controls) must run at design time to provide the design-time appearance of your control.

You can see this feature in action in the step-by-step procedure "Running the ShapeLabel Control at Design Time," in "Creating an ActiveX Control."

For More Information General information on debugging components can be found in "Debugging, Testing, and Deploying Components."

Running Code at Design Time

To put a control you're authoring into a state such that its code can execute at design time, you must close the control's visual designer, by clicking the Close box or pressing Ctrl+F4.

When the designer is closed, Visual Basic enables the control's icon in the Toolbox, so that you can add instances of the control to forms for testing.

If code in your control hits a break point at design time, for example during a Property Let invoked by the Properties window, Visual Basic enters break mode, just as it would if your project were running. When you press F5 to continue execution, the code in your control resumes execution. Visual Basic remains in design mode.

You can see this by setting a break point in a property procedure, and then placing an instance of your control on a test form.

Making Changes to Existing Controls

When you open the UserControl designer for a control, Visual Basic disables all instances of the control and grays the control's icon in the Toolbox. If you have a test form open with instances of the control on it, Visual Basic covers the disabled control instances with cross-hatching.

You can also disable control instances by certain changes to code in the control's code window, such as adding a new property or method, or adding code to a previously unused event procedure.

Once a control instance has been disabled in this fashion, it cannot execute code. It will not even receive a Terminate event.

Refreshing Control Instances

When you close the UserControl designer, the disabled instances of the control are quietly destroyed (they get no more events) and replaced with fresh instances. You can see this by putting Debug.Print statements in the Initialize and Terminate events of a control.

You can also refresh the control instances by right-clicking the test form and selecting Update UserControls from the context menu. If there are any control designers open, they will be closed before the control instances are refreshed.

For More Information General information on debugging components can be found in "Debugging, Testing, and Distributing Components."


Distributing Controls

As discussed in "Two Ways to Package ActiveX Controls," earlier in this chapter, Visual Basic lets you author shareable control components (.ocx files), or simply include private controls as .ctl files in the project for your application or component.

This topic and its related topics focus on distribution, versioning, and licensing issues for control components. Private controls are compiled directly into an executable or component, and are distributed along with it. Being private, they also have no versioning or licensing issues.

Distributing Control Components

When you distribute a control component, you're providing a tool other developers can use in their applications. Versioning issues address the question of how you update that tool without breaking your customers' code.

Because you're providing a tool, instead of a finished application, you have licensing issues to consider. You have to decide whether to include licensing support for your control. If you plan on building your controls using licensed controls from other authors, you need to consider how that affects your distribution plans.

Because the tool you're creating is an-process component ("ocx" is really just another way to spell DLL), you have to select a base address that will minimize memory conflicts, and thus avoid performance problems.

Finally, because the tool you're creating uses the Visual Basic run-time DLL, and possibly other support files, you have to create a Setup program.

Setup is covered in the remainder of this topic. The important subject of base addresses is discussed in "Setting Base Addresses for In-Process Components" in "Debugging, Testing, and Deploying Components."

Licensing and versioning issues are covered in the following related topics:

  • Licensing Issues for Controls Explains how to add licensing to your control project, and discusses the implications of using licensed controls as constituent controls.

  • Versioning Issues for Controls Explains the four areas of compatibility you need to consider when you create a new version of your control.

Important Be sure to set the description for each control in your control component. Some clients, such as Visual Basic, represent the entire .ocx file using the string you enter in the Project Description box of the Project Properties dialog box, but others display the browser strings for the individual controls. See "Providing Help and Browser Strings for Objects," in "Debugging, Testing, and Deploying Components."

Creating Setup for ActiveX Control Components

ActiveX controls created with Visual Basic require the Visual Basic run-time DLL. Depending on what constituent controls you use, you may require additional support files. To ensure that you distribute all the necessary support files, using SetupWizard is recommended.

For the most part, using SetupWizard for control components is no different from using it for any other component created using Visual Basic. This subject is thoroughly covered in "Debugging, Testing, and Deploying Components."

If you plan to use your control component for Internet or intranet development, you can obtain the most up-to-date information on setup options from the Microsoft Visual Basic Web site.

For More Information The SetupWizard is introduced in "Distributing Your Application," in the Visual Basic Programmer's Guide.


Licensing Issues for Controls

Licensing for controls is a sensitive issue. After you've spent hundreds of hours developing a control, what if somebody else puts an instance of it on a UserControl, exposes all the properties, methods, and events, adds one or two trivial properties, then compiles and sells it as a new control?

Visual Basic's licensing support protects your investment. When you add licensing support to your control component, a license key is compiled into it. This key covers all the controls in the component.

Running your Setup program transfers the license key to another computer's registry, allowing your controls to be used for development. Simply copying your .ocx file to another computer and registering it does not transfer the license key, so the controls cannot be used.

To add licensing support to your control project

  • On the Project menu, click <MyProject> Properties to open the Project Properties dialog box. Select the General tab, check Require License Key, then click OK.

When you make the .ocx file, Visual Basic will create a .vbl file containing the registry key for licensing your control component. When you use the Package and Deployment wizard to create a setup for your .ocx, the .vbl file is automatically included in the setup procedure.

How Licensing Works

When a developer purchases your control component and runs your Setup program, the license key is added to the registry on her computer.

Thereafter, whenever the developer puts an instance of your control on a form, Visual Basic (or any other developer's tool) tells the control to create itself using the registry key.

If a developer has obtained a copy of your control component, but not the registry key, the control cannot create instances of itself in the development environment.

When a Developer Distributes Applications

When the developer compiles a program that uses one of your controls, the license key for your component is compiled in. When she creates a Setup for the program, your .ocx is included. Users can then purchase the compiled program and run Setup. Your control is installed on each user's machine — but your license key is not added to the registry.

Each time a user runs the program, the Visual Basic run-time DLL asks your control to create a run-time instance of itself, and passes it the key that was compiled into the program. Your control doesn't have to check the registry, because Visual Basic passed it the key.

Thus the user can run a compiled application without having to have the control component's license key in the registry.

Licensing and the User

Suppose the user later obtains a copy of Visual Basic. Noticing that your control component is installed on his computer, he adds your .ocx file to a project.

The first time he tries to put an instance of one of your controls on a form, Visual Basic tells the control to create itself using the registry key. The key is not there, so the control component can't be used in the development environment.

Licensing and General-Purpose User Applications

When desktop applications such as Microsoft Word and Microsoft Excel create control instances on documents or user forms, they tell the control to create an instance of itself using the license key in the registry. This means that a licensed control cannot be used by an end user unless the user has purchased your control component and installed it.

User documents cannot have the license key compiled into them. Suppose the user of a desktop application gives a coworker a copy of your control component along with a document that contains one of your controls. When the document is opened, the control will be asked to create its run-time instance — using the registry key.

In other words, the coworker must also have purchased and installed your control component. Otherwise, when the document is opened, the control instance cannot be created.

Corporate developers who author ActiveX controls for use by end users within their companies may find it more convenient to omit licensing support. This will make it easier for end users to distribute documents containing controls.

Licensing and the Control Author

Now suppose that someone who purchased your control component decides to use one of your controls to author a new control of her own. As with any other program, when she compiles her control component, your license key is compiled in. SetupWizard creates a license key for the new component, but does not add your license key to the setup program.

When a developer installs this new code component, its license key is placed in the registry. The developer then runs Visual Basic, and attempts to put an instance of the control on a form.

The control is asked to create itself using the registry key. In turn, it asks its constituent controls to create themselves using their registry keys. Your control doesn't find its license key in the registry, so control creation fails.

Distributing Controls That Use Licensed Controls

If the control author wishes to distribute a new control that uses a control you authored, she must inform purchasers that in order to use her control, they must have your control component installed on their computers.

Alternatively, the control author might negotiate with you for the right to distribute your license key along with her own, in the setup program for her control.

In either case, both license keys will be installed on a developer's machine, so the developer can create design-time instances of the second author's controls. When those controls are compiled into an executable program, both license keys are compiled in.

When the program is subsequently installed by a user and run, the second author's control is asked to create itself. Its constituent controls are also asked to create themselves, and passed their license keys. (And so on, if your control uses constituent controls with license keys.)

Note Of the ActiveX controls included with the Professional Edition of Visual Basic, only the DBGrid control requires a license key on a developer's computer. For example, if you use the TreeView control as a constituent control, anyone who is licensed to use your control is licensed to use TreeView as part of your control. However, remember that your Microsoft License Agreement requires that you add significant and primary functionality to any redistributable, such as TreeView, in order to legally distribute it as part of software you develop.

For More Information Licensing and distribution of constituent controls, including those supplied with the Professional Edition of Visual Basic, is discussed in "Controls You Can Use As Constituent Controls," earlier in this chapter.

Licensing and the Internet

Licensed controls can be used on World Wide Web pages, in conjunction with browsers that support control licensing. Both the control component and the license key must be available to be downloaded to the computer of the person accessing a Web page.

The downloaded license key is not added to the registry. Instead, browser asks the control to create a run-time instance of itself, and passes it the downloaded license key.

The owner of the Web server that uses your control must have purchased and installed your control, just as a developer would, in order to supply both control and license.

If the license is not available, control creation will fail, and the browser will receive a standard control creation error. Whether the browser passes this message along to the person accessing the Web page, or simply ignores it, depends on the person who developed the browser.

Note Of the ActiveX controls included with the Professional Edition of Visual Basic, only the DBGrid control requires a license key on a Web server. For example, if you use the TreeView control as a constituent control, anyone who is licensed to use your control is licensed to use TreeView as part of your control. However, remember that your Microsoft License Agreement requires that you add significant and primary functionality to any redistributable, such as TreeView, in order to legally distribute it as part of software you develop.

For More Information See "Controls You Can Use As Constituent Controls," earlier in this chapter.


Versioning Issues for Controls

When you create a new version of a control, there are several areas of backward compatibility you must address:

  • Your controls interface; that is, its properties, methods, and events.

  • UserControl properties that affect control behavior.

  • Whether property values are saved and retrieved.

  • Procedure attribute settings.

Interface Compatibility

You can add new properties, methods, and events without breaking applications compiled using earlier versions of your control component. However, you cannot remove members from the interface, or change the arguments of members.

You can use Visual Basic's Version Compatibility feature to avoid creating incompatible interfaces. On the Component tab of the Project Properties dialog box, click Binary Compatibility in the Version Compatibility box. This enables a text box in which you can enter the path and file name of the previous version of your component.

The default value in this text box is the last location where you built the component. If you are going to continue using that location to build the new version of your control component, it's a good idea to place a copy of your previous version in another location, and then enter that location in the text box.

For More Information Interface compatibility and the use of the Version Compatible Component box are discussed in "Version Compatibility," in "General Principles of Component Design."

UserControl Properties

Be careful when changing properties of the UserControl object, such as ControlContainer. If a previous version of your control had this property set to True, so that developers could use the control to contain other controls, and you change the property to False in a subsequent version, existing applications may no longer work correctly if the new version is installed on the same computer.

Saving and Retrieving Property Values

You may retain a property for backward compatibility, but stop mentioning it in your Help file, and mark it as Hidden using the Procedure Attributes dialog box.

You can stop saving the value of such obsolete properties in the WriteProperties event, but you should continue to load their values in the ReadProperties event. If you stop loading a property value, you will break any previously compiled application that used the property.

Procedure Attribute Settings

Changing attributes of a procedure may break applications that were compiled using previous versions of your control. For example, it you use the Procedure Attributes dialog to change the default property or method of a control, code that relied on the default will no longer work.

Compiled Applications

In some cases, changing properties in a new version of your control will have no effect on applications that were compiled with an earlier version. This is because some services are provided by the container and are set when the application is compiled. Even though the settings have been changed in the new version of your control, the container won't initialize these properties.

Properties that are provided by the container may include Alignable, CanGetFocus, ControlContainer, DefaultCancel, ForwardFocus, Public, ToolboxBitmap, and Visible. If you change any of these properties in a later version of your control, you will need to recompile any applications that use the control.


Localizing Controls

You can increase the market for your control component by localizing it. A localized control displays text — captions, titles, and error messages — using the language of the locale in which the control is used for application development, rather than the language of the locale in which it was authored.

This topic examines localization issues specific to ActiveX controls. General localization issues are discussed in "International Issues," in the Visual Basic Programmer's Guide.

Using the LocaleID

When you compile an executable with Visual Basic, the LocaleID (also referred to as the LCID) of the Visual Basic version is compiled in. Thus an application compiled with the German version of Visual Basic will contain &H0407, the LocaleID for Germany.

In the same way, the LocaleID is compiled into an ActiveX control component created with Visual Basic. This becomes the default LocaleID for your controls. If this were the end of the story, you would have to compile a new version of your component, using the correct version of Visual Basic, for every locale in which you wanted to distribute it.

Fortunately, control components are more flexible than compiled applications. Your component can be used with versions of Visual Basic for any locale, and even with development tools that support other locales, because they can determine the correct LocaleID at run time.

Discovering the LocaleID

The LocaleID property of the AmbientProperties object returns the LocaleID of the program your control was used in. The AmbientProperties object is available as a property of the UserControl object, as described in "Using the AmbientProperties Object to Stay Consistent with the Container," earlier in this chapter.

You can test the Ambient property as soon as an instance of your control is sited on the container; that is, in the InitProperties or ReadProperties events. Once you know the LocaleID, you can call code to load locale-specific captions, error message text, and so forth from a resource file or satellite DLL, as described later in this topic.

You need to call this code in both events, because the InitProperties event occurs only when a control instance is first placed on a container. Thereafter the control instance receives the ReadProperties event instead, as discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter.

You should also call your locale code in the AmbientChanged event, because your control could be used in an application that resets its locale according to Windows Control Panel settings, which can be changed by the user at any time. Your control could also receive AmbientChanged events if it's used as a constituent control, as described later in this topic.

Avoid Accessing Constituent Controls in the Initialize Event

The constituent controls on your UserControl discover the LocaleID by checking the AmbientProperties object which the UserControl, like any good container, makes available to them. This happens automatically, with no effort on your part.

When the Initialize event occurs, your control has been created, and all the constituent controls have been created and sited on your control's UserControl object. However, your control has not yet been sited on the container, so the UserControl cannot supply the correct LocaleID to the constituent controls.

If code in the Initialize event accesses the properties and methods of the constituent controls, their responses will reflect the LocaleID of the version of Visual Basic you used to compile your component, rather than the LocaleID of the application in which your control is compiled. For example, a method call might return a string in the wrong language.

To avoid this, you should not access constituent controls in the Initialize event.

Responding to the AmbientChanged Event

The AmbientChanged event occurs whenever an Ambient property changes on the container your control has been placed on, as discussed in "Using the AmbientProperties Object to Stay Consistent with the Container," earlier in this chapter.

Applications compiled with Visual Basic use the LocaleID of the version of Visual Basic that compiled them. However, your control could be used in an application written using a development tool such as Microsoft Visual C++, in which it is possible to change an application's LocaleID in response to system messages.

For example, if a user opens the Control Panel and changes the locale, an application would receive a notification of the change, and reset itself accordingly. Your controls can handle this situation by including code to change locale dynamically, as in the following example.

Private Sub UserControl_AmbientChanged( _
      PropertyName As String)
   Select Case PropertyName
      Case "LocaleID"
         ' Code to load localized captions,
         ' messages, and so forth from a resource
         ' file or satellite DLL, as described below.

      ' Case statements for other properties.

   End Select
End Sub

A change in the locale can also occur if you use your control as a constituent of another control. As described above, constituent controls don't get the correct LocaleID when they're first sited on a UserControl object. When the outermost control has been sited on the application's form, all the constituent controls will receive AmbientChanged events with the correct LocaleID.

Base Language and Satellite DLLs

The most flexible localization technique is to compile your control component with the default text strings and error messages in the language of the locale you expect to be your largest market. Place text strings and error messages for other locales in satellite ActiveX DLLs, one for each locale.

This scheme makes your component very attractive to developers who create versions of their programs for multiple languages, because they can work with multiple locales on one development machine.

Satellite DLLs are also attractive to users in multilingual countries. Such users may have programs compiled by programmers in different locales; if two such programs use your control component, satellite DLLs allow both programs to coexist on a user's computer.

Important Your control should not raise an error if the requested satellite DLL is not found, as this could cause the entire application to fail. In the event the satellite DLL is not available, simply use the default locale in which your control component was built.

Naming Satellite DLLs

If you use an open-ended naming convention for your satellite DLLs, you can supply additional DLLs later without recompiling your program. An example of such a naming convention would be to include the LocaleID in the DLL names. Using this convention, your satellite DLLs for the Belgian French, German, and US English locales might be named MyControls20C.dll, MyControls407.dll and MyControls409.dll.

If you use Windows API calls to load and extract resources from your satellite DLLs, you can create the name of the DLL to be loaded by converting the LocaleID to a string, and appending it to a base name. (Note that the examples above use the hexadecimal representation of the LocaleID.)

As an alternative to using API calls, you can build your satellite DLLs as Visual Basic ActiveX DLL projects. To do this, create a class module with methods for retrieving resources. Give the class a name such as Localizer. Add this class module to each DLL project.

Use your open-ended naming scheme for the Project Name, so that each DLL has a unique programmatic ID, or ProgID, in the Windows registry. Each time you compile a new satellite DLL, you create a new Localizer class, whose full programmatic ID includes the Project Name of the DLL.

In your ActiveX control project, you can then create an instance of the appropriate Localizer class using code such as the following:

Dim strProgID As String
Dim objLoc As Object
' Generate the ProgID of the Localizer object
' for the appropriate satellite DLL.
strProgID = "MyControls" & Hex$(AmbientProperties.LocaleID) _
   & ".Localizer"
Set objLoc = CreateObject(strProgID)
If objLoc Is Nothing Then
   ' Satellite DLL not found; use default locale.
Else
   ' Call methods of Localizer object to retrieve
   ' localized string and bitmap resources.
End If

The code above uses late binding (that is, the variable objLoc is declared As Object). You can get better performance with early binding, by using the Implements feature of Visual Basic. Instead of making the resource retrieval methods members of the Localizer class, you can define them in an abstract class named IResources.

In your Localizer class, use the Implements statement to implement IResources as a second interface. You can call the methods of this interface with early binding, as shown below.

' Early-bound variable for IResources interface.
Dim ires As IResources
' Get the IResources interface of the Localizer
' object obtained from the satellite DLL.
Set ires = objLoc
' Call the methods of your IResources interface
' to retrieve localized resources.
Set cmdOK.Caption = ires.GetString(ID_CMDOK)

As with the late-bound Localizer object, you can simply add the Localizer class module, with its second interface, to each satellite DLL project. The ability to add the same interface to several different classes is called polymorphism.

For More Information The Implements feature is discussed in "Providing Polymorphism by Implementing Interfaces," in "General Principles of Component Design."

Accessing satellite DLLs is discussed in "International Issues," in the Visual Basic Programmer's Guide.

Adding resource files to Visual Basic projects is discussed in "More About Programming," in the Visual Basic Programmer's Guide.

Resource Files

An alternative to satellite DLLs is to place text strings and error messages in a resource file, and compile the file into your control component. There are disadvantages to this technique.

  • If you use one resource file for each locale, you must compile a separate .ocx file for each locale. To avoid file name conflicts, you can put a locale indicator in the name of each .ocx file, as for example MyControlsDE.ocx for German, or MyControlsFR.ocx for French.

  • Unfortunately, you cannot avoid type library name conflicts so easily. A developer can have only one locale version of your control installed at a time. This may be a drawback in multilingual markets.

  • Although you can avoid the problem of compiling multiple .ocx files by putting the text and error message strings for all locales into a single resource file, the result will be a much larger .ocx file, and you will have to recompile the component to add support for new locales.

Localizing Interfaces

If you localize property, method, and event names, you must compile a separate version of your control for each locale. To allow multiple versions of your control to coexist on one computer, you must use a different name for your control in each version.

As a result of the different interface and control names, multilingual developers will have to rewrite their code for each language version of a program that uses your control. This will make your control component less attractive to such developers.

Microsoft applications, such as Visual Basic, do not localize interface member names.

Localizing Property Pages

Microsoft applications localize property pages, but not property names. If you use this scheme, the caption that shows up on a property page may not match the name of the property in the Properties window.

When localizing captions for properties on property pages, take care to select captions that make it obvious what the property is. Alternatively, you may wish to include the property name in parentheses.

For principles of form layout that simplify localization, see "International Issues," in the Visual Basic Programmer's Guide.

Localizing Type Library Information

There is no way to retrieve a browser string from a localized DLL or resource file, so browser strings must be compiled into your type library. To produce localized type libraries, you must use the Procedure Attributes dialog box to change the browser strings for all your properties, methods, and events. You must then re-compile your executable.

Localizing type library information thus limits your ability to localize using satellite DLLs. You may wish to leave your browser strings in the default language of your control.

For More Information See "International Issues," in the Visual Basic Programmer's Guide.


Creating Property Pages for ActiveX Controls

Property pages offer an alternative to the Properties window for viewing ActiveX control properties. You can group several related properties on a page, or use a page to provide a dialog-like interface for a property that's too complex for the Properties window.

In Visual Basic, property pages are displayed in the Property Pages tabbed dialog box, as shown in Figure 10.1.

Figure 10.1 The Properties window and the Property Pages dialog box

Each tab in the Property Pages dialog box represents one PropertyPage object. For example, the PropertyPage object that provides the General tab shown in Figure 10.1 was created using the PropertyPage designer shown in Figure 10.2.

Figure 10.2 The PropertyPage designer for the General tab

In the designer, the PropertyPage object doesn’t display a tab. Nor does it display the OK, Cancel, and Apply buttons. These are provided automatically by the Property Pages dialog, and are not part of any individual PropertyPage object. The Property Pages dialog uses the Caption property of the PropertyPage object as the text for the tab.

In this example, the General page groups two properties, DinoName and Age. The page is laid out to simplify localization; the length of the captions can change without affecting the layout. This is discussed in "Property Page Design Guidelines."

The following topics explain how property pages work, and the different ways you can use them in your ActiveX controls.

Topics

How Property Pages Work

Property pages look a lot like forms, but they have very different behavior. This topic explains the events you need to handle.
Connecting a Property Page to an ActiveX Control

Once you’ve added a property page to your project, you need to connect it to any control that’s going to use it.
Associating a Property Page with a Property

Explains how to designate a property page to be opened when the user edits a property in the Properties window.
Using Standard Property Pages

Visual Basic provides standard pages for fonts, colors, and pictures.
Property Page Design Guidelines

Some guidelines to help you create easy-to-use, professional-looking property pages.

For More Information A simple property page example can be found in the step by step procedures in "Creating an ActiveX Control." Control creation is covered in depth in "Building ActiveX Controls." The Property Page Wizard can automate much of the work of creating property pages. Search for Property Page Wizard.


How Property Pages Work

Property pages look a lot like forms, and designing them is somewhat similar to designing forms. The way property pages work, however, is quite different from the way forms work.

For example, when the Property Pages dialog box creates an instance of a property page, the Initialize event is the first event the PropertyPage object gets — just as it would be for a form. However, unlike a form, the PropertyPage object doesn’t get a Load event. The key event for PropertyPage objects is the SelectionChanged event.

This topic examines the three things your PropertyPage object must do:

  • In the SelectionChanged event, obtain the property values to be edited.

  • Set the PropertyPage object’s Changed property whenever the user edits a property value.

  • In the ApplyChanges event, copy the edited property values back to the selected control (or controls).

The SelectionChanged Event

The SelectionChanged event occurs when the property page is displayed, and when the list of currently selected controls changes.

For example, after selecting an instance of your control and opening the Property Pages dialog box, a developer might realize that she needed to change the properties of two instances of your control. By clicking the second instance while holding down the CTRL key, she could add the second instance to the list of selected controls. Each of your property pages would then receive a SelectionChanged event.

Important You should always treat the SelectionChanged event as if your property page is being loaded for the first time. As you’ll see, changing the selection fundamentally changes the state of the property page.

Coding the SelectionChanged Event for a Single Control

The most important thing you need to do in the SelectionChanged event is to set the values of the controls that display property values for editing. For example, consider the General page for the VirtualVelociraptor control (originally shown in Figure 10.1):

Suppose that the Age property of the VirtualVelociraptor control uses the following public Enum:

Public Enum DinoAge
   vvHatchling
   vvJuvenile
   vvAdult
End Enum

The SelectionChanged event of the property page might look like this:

Private Sub PropertyPage_SelectionChanged()
   ' Place the value of the DinoName property for the
   '   first selected control in the txtDinoName text
   '   box for display and editing.
   txtDinoName = SelectedControls(0).DinoName
   ' Use the value of the Age property of the first
   '   selected control to select the appropriate
   '   option button in the Age frame.
   optAge(SelectedControls(0).Age).Value = True
   ' (The code above depends on the fact that the
   '   elements of the DinoAge Enum have the values
   '   0, 1, and 2.)
End Sub

Tip The Property Page Wizard will populate your property page with text box controls and check boxes (for Boolean properties), and generate default code for the SelectionChanged event.

The SelectedControls Collection

The SelectedControls collection contains all the controls currently selected in the container the developer is working on. The collection may contain several instances of your control; if the property page is shared by more than one control in your control component, the collection may contain controls of multiple types.

Note You don’t need to worry about the collection containing controls other than your own — text boxes, for example — because the Property Pages dialog box only displays those pages that are used by all of the currently selected controls.

For the moment, ignore the possibility that multiple controls might be selected. What the code in the SelectionChanged event shown above is doing is taking the value of each property for the first control in the collection and assigning it to the appropriate control on the property page.

In the case of a single selected control, this places all of the control’s property values in fields where the user can edit them.

Different Ways to Edit Properties

Instead of showing the property value of the Age property as a set of option buttons, you could use a drop-down list showing the elements of the enumeration:

The drop-down list takes up less space than the option buttons did (an advantage that grows larger as the number of possible values increases), and it shows the names of the constants that would be used in code.

The following code fragment shows how you might set up such a list.

Private Sub PropertyPage_SelectionChanged()
   txtDinoName = SelectedControls(0).DinoName
   ' Create a drop-down list containing the values and
   '   names of the Enum elements for the Age
   '   property, and select the one that corresponds
   '   to the current value of the Age property.
   cboAge.AddItem vvHatchling & " - vvHatchling"
   cboAge.AddItem vvJuvenile & " - vvJuvenile"
   cboAge.AddItem vvAdult & " - vvAdult"
   cboAge.ListIndex = SelectedControls(0).Age
   ' (The index of each Enum element in the drop-down
   '   list is the same as the element's value.)
End Sub

Tip While you can choose any editable representation that makes sense for a property, remember that the more space each property takes up, the more tabs you’ll need. Minimizing the number of tabs makes the property pages for your control easier to use. For most enumerations, a drop-down list will make the most efficient use of space.

Coding the SelectionChanged Event for Multiple Controls

To determine whether multiple controls are selected, you can test the Count property of the SelectedControls collection to see whether it’s greater than one.

In order to deal with multiple selected control instances, it’s useful to divide the properties of your control into two groups:

  • Properties that can sensibly be set to the same value for multiple controls. For example, it’s very convenient to be able to set the BackColor property of several Label controls to the same value.

  • Properties that do not make sense to set to the same value for multiple controls. For example, it’s not particularly helpful to set the Caption property of several Label controls to the same value. In fact, it might be quite annoying to the user to do so by accident.

One approach you might take in your SelectionChanged event is to disable the edit fields for properties of the second sort whenever multiple controls are selected. In the discussion of the ApplyChanges event, an alternate technique will be shown.

Shared Property Pages

If you have multiple controls in your project, and two such controls share a property page, make sure that you provide error trapping for the code that reads the property values. If the first control selected doesn’t include all of the properties shown on the page, an error will occur when you try to read that property value.

Enabling the Apply Button by Setting Changed = True

In order to tell Visual Basic that the user has edited one or more properties on a property page, you must set the PropertyPage object’s Changed property to True. Because there’s no way to know which property a user might decide to change, you must do this for every property displayed on the page.

For example, to notify the PropertyPage of changes in the DinoName or Age properties from the previous example, you would use the following code:

Private Sub txtDinoName_Changed()
   Changed = True
End

Private Sub cboAge_Change()
   Changed = True
End

Note that this is exactly the same as coding PropertyPage.Changed = True.

Notifying the PropertyPage object that values have changed enables the Apply button on the Property Pages dialog box, and causes the ApplyChanges event to occur when the Apply button is pressed, when the user changes tabs, or when the dialog box is dismissed.

Note You may wish to keep track of which properties have changed, so that you don’t have to write them all out.

The ApplyChanges Event

The second most important event in a PropertyPage object is the ApplyChanges event. In this event you copy the edited property values back to the currently selected controls.

The ApplyChanges event occurs when the user:

  • Clicks the OK button to dismiss the dialog.

  • Clicks the Apply button.

  • Selects another tab in the Property Pages dialog box.

The following code for the ApplyChanges event assumes that the SelectionChanged event was coded using a drop-down list for the Age property, as shown earlier.

Private Sub PropertyPage_ApplyChanges()
   Dim vv As VirtualVelociraptor
   ' Set the DinoName property of the FIRST selected
   '   control only.
   SelectedControls(0).DinoName = txtDinoName

   For Each vv In SelectedControls
      ' Transfer the value currently selected in the
      '   drop-down list for the DinoAge property to
      '   all of the selected controls.
      vv.DinoAge = cboAge.ListIndex
      ' (The code above works because the value of
      '   each of the three elements of the Enum is
      '   the same as its index number in cboAge.)
   Next
End Sub

Because it generally doesn’t make sense to give all of the virtual velociraptors the same name, the DinoName property is applied only to the first selected control. The Age property, on the other hand, is applied to all the selected controls.

Note As the control author, it’s up to you to decide which properties make sense to set for multiple selected controls.

Dealing with Errors in ApplyChanges

In the case shown above, there’s no chance of error in the ApplyChanges event. The text property is a simple string, and the drop-down list limits user input for the Age property to only those values that are valid.

If your property page allows the user to enter values that may be rejected by the Property Let (or Property Set) procedure, you should use error trapping in the ApplyChanges event. The simplest scheme is to use On Error Resume Next, and test Err.Number after each property that may raise an error.

When an error occurs:

  • Stop processing the ApplyChanges event.

  • Display an error message, so the user understands what went wrong.

  • Set the focus to the property that caused the error.

  • Set the Changed property of the PropertyPage object to True.

    Important Setting Changed = True performs two functions. First, it re-enables the Apply button. Second, it prevents the Property Pages dialog box from being dismissed if the user clicked OK. This is the only way to prevent the dialog box from closing.


Connecting a Property Page to an ActiveX Control

Once you’ve added property pages to your ActiveX control project, you can use the Connect Property Pages dialog box to establish a connection between a control and the property pages you want it to use.

When a user shows the Property Pages dialog box for an instance of one of your controls, each of the pages you connected to the control will appear as one tab in the dialog box.

To connect property pages to a control

  1. In the Project Explorer window, double-click the control to open its designer.

  2. Press F4 to open the Properties window.

  3. Double-click the PropertyPages property (or select the property and then click the ellipsis button) to open the Connect Property Pages dialog box.

  4. Place a check mark beside each property page you want to appear when the developer who uses your control opens the Property Pages dialog box.

  5. Use the Page Order buttons to put the pages in the order you want them to appear in the Property Pages dialog box, then click OK.

For More Information The standard property pages for fonts, colors, and pictures are discussed in "Using Standard Property Pages."


Associating a Property Page with a Property

Sometimes a property is too complicated to set from the Properties window. A property might be an object, such as the Font object, with properties of its own. A property might consist of an array of values, or even a collection of objects — such as a collection of Toolbar buttons.

For example, Figure 10.3 shows how a property page might look for the Behavior property of a hypothetical VirtualVelociraptor control. This page allows cells to be added to and deleted from animation sequences for the control’s various behaviors.

Figure 10.3 A property too complex for the Properties window

Notice that only the tab associated with the property is displayed. If you compare this picture with Figure 10.1, you’ll notice that the page size used by the Property Pages dialog box is smaller in this picture. When multiple pages are displayed, the size of the largest page is used.

Note If you declare a property of type Font, OLE_COLOR, or Picture, Visual Basic will automatically associate the property with a StandardFont, StandardColor, or StandardPicture page, as described in "Standard Property Pages."

To associate a property page with an individual property

  1. In the Project Explorer window, right-click the UserControl to open the context menu, and select View Code to open the code window.

  2. On the Tools menu, select Procedure Attributes to open the Procedure Attributes dialog box, and click Advanced to expand the dialog box.

  3. In the Name box, select the property you wish to associate with a property page.

  4. Select the desired property page in the Use this Page in Property Browser list, as shown below, then click Apply or OK.

The EditProperty Event

Visual Basic allows you to associate multiple properties with the same property page. You might want to do this if your control has more than one property that uses the same property page layout, or if one property uses part of another property’s layout. In the latter case, you can use the EditProperty event to enable only the necessary parts of the property page.

When the user clicks the ellipsis button for a property that’s associated with a property page, the page receives the EditProperties event in addition to the events it normally receives. You can use the PropertyName argument of the EditProperties event to identify the property whose ellipsis button was clicked.

The EditProperties event gives you the opportunity to do a number of things to the property page, depending on the nature of your control and the complexity of the property being edited. For example, you might:

  • If the property being edited is displayed in a single control on the PropertyPage, set the focus to that control.

  • Enable and disable controls on the property page so that only those fields applicable to the specified property are enabled.

Not Showing Properties in the Properties Window

In some cases, you may not want a property displayed in the Properties window. For example, if displaying a property value requires a time-consuming calculation, the developer who uses your control may be annoyed at the length of time it takes to access the Properties window.

On the Property Attributes dialog, accessible from the Tools menu, select the property you wish to suppress. Click the Advanced button, check "Don’t show in Property Browser," and then click Apply.


Using Standard Property Pages

Visual Basic provides three standard property pages: StandardFont, StandardColor, and StandardPicture. If you declare properties of type Font, OLE_COLOR, or Picture, Visual Basic’s Properties window will automatically associate these properties with the appropriate standard property page.

Visual Basic will not automatically add these pages to the Property Pages dialog box, however. Use the procedure shown in "Connecting a Property Page to an ActiveX Control" to add standard property pages to the list of pages that will be displayed in the Property Pages dialog box.

Note The format used to display standard property pages in the Property Pages dialog box differs from the format used by the Properties window. For example, the Color page is in an entirely different format, and the Font page does not display all Font object properties.

Tip If you use the Property Page Wizard to create property pages for a control with properties declared Font, OLE_COLOR, or Picture, the wizard will automatically add the appropriate standard property pages to the list of pages to be displayed.

Standard Property Pages and Multiple Properties

If your control has more than one property that uses a standard property page, and you add the page to your control’s PropertyPages property, the standard page will include a list of properties the user can select from.

For example, Figure 10.4 shows the Properties window and Property Pages dialog box for the hypothetical VirtualVelociraptor control, which has several properties of type OLE_COLOR:

Figure 10.4 The Properties window for a control with multiple color properties

As Figure 10.5 shows, the Color page displayed by the Property Pages dialog box for the VirtualVelociraptor control has a list box containing the four color properties of the control.

Figure 10.5 A standard property page showing multiple properties

Figure 10.5 also shows that the Property Pages dialog box uses a very different format than that used by the Properties window.

Note When a standard property page displays multiple properties, the ApplyChanges event is raised each time the user selects a different property from the list.

The following code fragment shows how the StripeColor property of the hypothetical VirtualVelociraptor control should be declared in order to work with the Properties window and Property Pages dialog box:

Private mStripeColor As OLE_COLOR

Public Property Get StripeColor() As OLE_COLOR
   StripeColor = mStripeColor
End Property

Public Property Let StripeColor( _
      ByVal NewColor As OLE_COLOR)
   mStripeColor = NewColor
End Property

For More Information See "Adding Properties to Controls" in "Building ActiveX Controls."


Property Page Design Guidelines

You may find the following guidelines helpful in designing property pages that are professional-looking and easy to use.

  • When the property pages for your control are shown, focus should be on the first field of the first tab. To do this, give the first control on each PropertyPage the lowest TabIndex number for the page.

  • If properties are arranged in multiple columns on a page, tab order should usually be top to bottom in the first column, then move to the top of the next column.

  • Provide access keys (keyboard shortcuts) for all the fields on a property page.

    Note The letter ‘A’ is not available as an access key, because the Property Pages dialog box uses it for the Apply button.

  • Place standard property pages for fonts, colors, and pictures at the end of the list of property pages for a control.

  • Group similar properties together — for example, all properties that affect a control’s appearance should go on the same page.

  • Use a uniform size for your property pages. When the Property Pages dialog box displays multiple pages, it uses the same width and height for all of them.

    Tip The standard pages for fonts, colors, and pictures are all the same size. If you use these pages, set your other property pages to this standard size. A quick way to set the standard size is to set the StandardSize property of the PropertyPage to Large at design time.

  • For ease of use, keep the number of tabs to a minimum. For example, don’t add a tab for your control’s About box.

  • Most control components (.ocx files) that provide more than one control have a page with the caption "General" which contains items that are common to many controls.

  • Keep your pages simple and fast. Your users will be generally be developers employing your control to get a job done — animation and fancy graphics will just get in their way.

  • The Property Pages dialog box will show a Help button if you’ve used the General tab of the Project Options dialog box to add a Help file to your project. You can set the HelpContextID properties of your property pages and of the controls they contain.

  • When you add labels to your property pages, make sure you include the property name in the label. You may want to localize your control for international use, or add user-friendly captions for the benefit of desktop tools such as Microsoft Office, but remember that the user must be able to identify the actual property names in order to write code for them, or look them up in your Help file.

  • When you create strings for the named constants in an enumeration, always include the actual constant name, because that’s what the person using your control will have to use when programming.

  • If you plan to localize your control component for international use, make sure that combo boxes, text boxes, and so on are wide enough to accommodate translated strings. If you plan to localize for locales — such as Japan — that use DBCS characters, make sure the font you’re using is available on DBCS systems, and that the height of your text boxes will accommodate DBCS characters.

  • Avoid showing dialog boxes from property pages. (The File Open dialog is an exception to this rule.)

  • Cross-tab property dependencies. If two properties interact — for example, if setting the value of one property limits the valid values for another property — place the properties on the same page.

For More Information Rules for layout that simplifies localization can be found in "International Issues," in the Visual Basic Programmer’s Guide.


Building ActiveX Documents

This chapter covers the issues involved in building an ActiveX document-based solution. Some of the topics correspond to the sequence of development tasks presented in "Creating an ActiveX Document."

Topics

What is an ActiveX Document?

Describes what an ActiveX document is. Also includes an outline of the creation process, the UserDocument object, key events, and how to convert existing applications in to an ActiveX document.
Design Considerations for ActiveX Documents

Discusses the factors that will affect the design of an ActiveX document.
Targeting an ActiveX Document Container

Begins with a discussion of why you will want to target a container, and continues with details about Microsoft Office Binder and the Visual Basic IDE tool window.
Designing the User Interface for ActiveX Documents

Details about designing menus and adding forms, as well as how the Viewport works.
Adding Internet Features to ActiveX Documents

Asynchronous operations are explained as well as use of the Hyperlink object to add Internet features to ActiveX documents.
Persisting ActiveX Document Data
Ensuring Communication Among ActiveX Documents

Details a strategy for tracking global object variables that share data between ActiveX documents.
Debugging ActiveX Documents in Internet Explorer

Details the use of Internet Explorer as a debugging tool.


What Is an ActiveX Document?

The word "document" in "ActiveX document" is somewhat misleading. While the genesis of ActiveX documents reveals that a Microsoft Visual Basic ActiveX document is analogous to a Word document, when you create a Visual Basic ActiveX document, the distinction between a "document" and an application becomes genuinely blurred. While a traditional document (such as a Word document) is static, ActiveX documents need not be. Using Visual Basic, you can create a complete application with the semantics of a traditional document. In other words, you have the functionality of the application, but the flexibility of a document's behavior — when a user opens an ActiveX document, she will not only have the full functionality of an application, but the ability to persist and distribute "copies" of the data intrinsic to the application. Thus, the "document" is truly active.

An Analogy: Word Document = ActiveX Document

ActiveX documents are not an entirely new concept. You are probably already familiar with Word documents. As you know, a Word document is not the same as the Word application — the Word document (with the extension .doc) contains the actual content, whereas the Word application (Winword.exe) is used to create the document.

You may also know that a Word document can be viewed in other containers. In that case, the Word application supplies the objects that enable another ActiveX container (such as Internet Explorer) to view and activate the document. And this same mechanism works for ActiveX documents created with Visual Basic.

As Figure 11.1 shows, when you create an ActiveX document-based project, you are creating a Visual Basic "document" that can be contained in an ActiveX container (such as Internet Explorer). Compiling the ActiveX document creates both a Visual Basic Document file (with the extension .vbd) and its corresponding server — which can be an ActiveX .dll, or ActiveX .exe file. In other words, the .vbd file is to the .exe or .dll file what the .doc file is to the Winword.exe file.

Figure 11.1 Word Documents and ActiveX Documents Compared

  • ActiveX Document Advantages

  • ActiveX Document Creation Basics

  • Parts of an ActiveX Document

  • Converting Existing Applications into ActiveX Documents

  • ActiveX Document Similarities to ActiveX Controls

  • Key Events in the Life of an ActiveX Document


ActiveX Document Advantages

The advantages of creating and deploying ActiveX documents are:

  • You can leverage your knowledge of Visual Basic. If you are skilled in programming Visual Basic, you already have the knowledge to reach your goals — you do not have to learn another programming language (as you do to create an HTML page).

  • You can leverage the Visual Basic programming environment. The full development environment is available to you, including the Visual Basic code window, debugger, and compiler.

  • Ability to create ActiveX documents that run in Internet Explorer. For all intents and purposes, an ActiveX document is a Visual Basic application that is now accessible in a widely-used container.

  • Execution on a local machine. The ActiveX component can contain all the necessary code to deliver the results of complex calculations.

  • Immediate visual feedback about layout of elements. Placing TextBox and CommandButton controls on an ActiveX document gives you immediate feedback about how the document will look. In contrast, coding an HTML document takes practice and guesswork.

  • Support for the Hyperlink object, which allows an ActiveX document to request that the container (if it is Hyperlink-aware) to navigate to another ActiveX document, or a Web site.

  • Support for the AsyncRead method. The method begins an asynchronous transfer of data, allowing other code to execute while the data is retrieved.

Note There is one control that cannot be placed on an ActiveX document: the OLE Container Control. Also note that embedded objects, such as an Microsoft Excel or Word documents, are not allowed on ActiveX documents.


ActiveX Document Creation Basics

Creating an ActiveX document has great similarities to creating an ActiveX control. Just as with ActiveX controls, you are not necessarily in control of the host application — you are a cog in somebody else's machine. Therefore, much of your code must be defensive — allow your application to gracefully exit when necessary. And, as when developing an ActiveX control, you can expose methods, events and properties that can be used by other components.

The topics in this chapter provide the basic facts and concepts you need to create ActiveX documents. First, however, is a recap of the process of ActiveX document creation.

The Process in a Nutshell

The process of creating an ActiveX document is like creating any other project in Visual Basic:

  1. Begin by starting a new project and selecting either ActiveX Document EXE or ActiveX Document DLL. By default, these template projects contain a single UserDocument. Each UserDocument is the core object for an ActiveX document.

  2. Add controls to the UserDocument, and add code for the controls.

  3. Add a Help menu to the UserDocument.

  4. Add other forms, code modules, or more UserDocument objects to the project.

  5. Test and debug the ActiveX document by running the project and viewing the document in the target container (the container application you will eventually use to view the document — for example, Internet Explorer).

  6. Compile the project as either an in-process component, or an out-of-process component. A .vbd file (required when you use a browser such as Internet Explorer to view an ActiveX document) is also created.

  7. Deploy the ActiveX document(s).

The .exe or .dll file contains the actual compiled code. Initially, the .vbd file contains the class ID of the .exe or the .dll file. If you elect to allow users to save data (using the PropertyBag), the data will also be stored in the .vbd file. Users can view your ActiveX document by opening the .vbd file in a host application.

For More Information Deployment of ActiveX documents is covered in "Building Internet Applications." For more information about the difference between the in-process and out-of-process components, see "In-Process and Out-of-Process ActiveX Documents." Details about the .vbd file are covered in "Parts of an ActiveX Document."


Parts of an ActiveX Document

An ActiveX document created with Visual Basic is always composed of a UserDocument object, code, code modules, and controls that you place on the UserDocument.

Project Files

Like Visual Basic forms, UserDocument objects have code modules and visual designers, as shown in Figure 11.2.

Figure 11.2 UserDocument designer and code window

Like forms, UserDocument objects are stored in plain text files that contain the source code and property values of the UserDocument and the controls placed on it. Visual Basic uses the extension .dob for these source files.

If a UserDocument contains controls that use graphical elements which cannot be stored as plain text, such as bitmaps, Visual Basic stores those elements in a .dox file. This is analogous to the .frx files used to store graphical elements used in forms.

The .dob and .dox files completely define an ActiveX control's appearance and interface (properties, events, and methods). A compiled ActiveX document will then consist of either an ActiveX .exe or .dll and an accompanying .vbd file. In a browser such as Internet Explorer, the user must navigate to the .vbd file to open the ActiveX document.

Compiled Files

An ActiveX document can be built as an out-of-process component (an .exe file) or an in-process component (a .dll file). In both cases, when you run or compile the project, in addition to creating an .exe. or a .dll file, Visual Basic creates a Visual Basic Document file, which has the extension "vbd." When you compile an .exe or .dll, the .vbd file will be placed in the same directory as the compiled component.

The .vbd file is actually an OLE structured storage — this basically means that data in the file can be accessed and manipulated via standard OLE interfaces. Other Microsoft applications, such as Word and Excel, save data in this manner.

Note When you run an ActiveX project from the Visual Basic IDE, a temporary .vbd file will be created in the directory where Visual Basic has been installed. The temporary .vbd file will have the same name as the UserDocument with the "vbd" extension. If a .vbd with the same name exists (in the Visual Basic directory), that .vbd file will be moved to a temporary file location. When the project stops running, the original file will be restored. While the project is running, however, you can navigate to the temporary file (from a browser or other application).

To view the ActiveX document in a container application, such as Internet Explorer or Microsoft Office Binder, you must navigate to the .vbd file. For example, if you created an ActiveX document named "DocObject1.vbd," and it is in the default directory of the Visual Basic program, you might type the following URL in the Address box of Internet Explorer:

file://c:\Program Files\VB\DocObject1.vbd

Note This path will vary depending on which version of Visual Basic you have installed. If Visual Basic has been installed as part of Microsoft Visual Studio, for example, the URL shown above may instead be file://c:\Program Files\Microsoft Visual Studio\VB\DocObject1.vbd.

You can also use the Hyperlink object's NavigateTo method, from within an ActiveX document, to open another ActiveX document, as shown:

Private Sub cmdGoNext_Click()
   UserDocument.Hyperlink.NavigateTo _
      "c:\Program Files\VB\DocObject1.vbd"
End Sub

Note Once you have compiled your ActiveX document, you can change the extension of the .vbd file. In other words, instead of having users navigate to "Nice.vbd," you can have them navigate to "Nice.dog."

For More Information To learn how to navigate between ActiveX documents in a browser, see "Using the HyperLink Object with ActiveX Documents."


Converting Existing Applications into ActiveX Documents

You may have several existing applications that are candidates for conversion into ActiveX documents. If so, you can use the ActiveX Document Migration Wizard to ease the transition.

Note The OLE Container control and embedded objects (for example, Microsoft Word or Microsoft Excel documents) cannot be placed on an ActiveX document, and will be removed.

To use the ActiveX Document Migration Wizard

  1. Open the project you wish to convert.

  2. On the Add-Ins menu, click Add-In Manager.

  3. Highlight VB ActiveX Document Migration Wizard, click the desired behaviors in Load Behavior, and then click OK.

  4. On the Add-Ins menu, click ActiveX Document Migration Wizard.

  5. Follow the directions until the Wizard has finished its task.

What the Wizard Does

The Wizard performs the following tasks

  • Copy form properties to a new UserDocument.

  • Copy all controls from the form to the UserDocument, and retain their names.

  • Copy all code behind the form to the UserDocument.

  • Comments out all illegal code such as Me.Left and End.

  • Switches the project type to ActiveX EXE or DLL.

  • Where there is an exact counterpart on the UserDocument, copy event handlers to the UserDocument, replacing "Form" with "UserDocument". For example, "Form_Click()" becomes "UserDocument_Click()". In cases where there is no counterpart, the event handler is copied over leaving the "Form" part intact. These are copied to the General section of the code window, and can then be called as procedures. In other words, if you need to invoke code from the Form_Load event, simply call it from an appropriate event:
    Private Sub UserDocument_Show()
Form_Load
End Sub

Note The Form object's Load event doesn't have a direct counterpart in the UserDocument. You place Load event procedures in the Show event handler, but you should be aware that the Show event gets called every time the user navigates to the ActiveX document (in a Web browser). To prevent Load event procedures from running every time the Show event occurs, use a module-level variable as a flag — if the flag has been set, don't run the procedures. Code for this technique is shown in "Determining the ActiveX Document's Container Programmatically" in this chapter.


ActiveX Document Similarities to ActiveX Controls

In many ways, ActiveX documents resemble ActiveX controls. Knowing how they are similar can be of some benefit, as the programming techniques, caveats, and strategies apply to both.

Here are a few of the relevant similarities:

  • Like an ActiveX control, an ActiveX document can't exist without a container. Whereas an ActiveX control is contained in a form, UserDocument, or UserControl, the ActiveX document is contained in a container such as Internet Explorer or Microsoft Binder.

  • The author of a control cannot know for sure which development environment the control will eventually be used in. Likewise, the developer of an ActiveX document cannot know which container will be used to view the ActiveX document.

  • The UserDocument object also features several events that are also found on the UserControl object. The common events include: Initialize, InitProperties, ReadProperties, EnterFocus, ExitFocus, WriteProperties, and the Terminate event.

Because of the great similarity between ActiveX documents and ActiveX controls, much of what is written in "Building ActiveX Controls," also applies to building ActiveX documents. The following topics are of particular relevance:

  • "The UserControl Object," discusses the core document for ActiveX controls. This object has great similarities to the UserDocument object.

  • "Controls You Can Use As Constituent Controls" discusses the controls that can be placed on a UserControl. These same points can generally be applied to an ActiveX document.

  • "Object Models for Controls" discusses a strategy of dealing with complex controls, which can also be applied to ActiveX documents.

  • "Adding Internet Features to Controls" explains, in detail, the AsyncRead method, the AsyncReadComplete event, and the Hyperlink object. These same features are available for ActiveX documents.

  • "Adding Properties to Controls" and "Adding Methods to Controls" show you how and why to add public properties and methods to controls. The same methods apply (in part) to ActiveX documents.

  • "Creating Robust Controls" gives a thorough list of do's and don'ts for creating robust controls. Many of the principles apply to ActiveX Documents as well.


Key Events in the Life of an ActiveX Document

The design of an ActiveX document depends greatly on the application(s) you are targeting. All containers are not created equal, and this is especially evident when considering events. However, before examining the events, read the two following notes, which affect the availability and behavior of key events.

A Note on Siting

Like an ActiveX control, an ActiveX document cannot exist by itself. It must be placed in a container. The process of hooking an ActiveX document up to its container is called siting — that is, assigning the ActiveX document a site in the container.

It's only after an ActiveX document has been sited that the properties of the container become available to it. For example, the UserDocument object's Parent property (which returns a reference to the container) is not available until the ActiveX document is sited. The Hyperlink object is also unavailable until the document has been sited.

For More Information Because of the similarity of ActiveX document creation to ActiveX control creation, you may wish to peruse "Control Creation Terminology" in "Building ActiveX Controls."

A Note on Saving Properties

If a container application supports the PropertyBag, you can easily save and retrieve data to a file — whether it saves to the .vbd file or not depends upon the application. For example, Internet Explorer saves data to the .vbd file, but Office Binder saves all data to the .obd (Office Binder Data) file. However, once data has been saved, the InitProperties will not occur; instead, the ReadProperties event will replace it. Similarly, the WriteProperties event will not occur until the user saves a property.

For More Information To find out how to save properties with the PropertyBag, see "Persisting ActiveX Document Data" in this chapter.

Key UserDocument Events

The meanings of the key events in the life of a UserDocument object are as follows:

  • The Initialize event occurs every time an instance of your document is created or re-created. It is always the first event in an ActiveX document’s lifetime.

  • The InitProperties event occurs only as long as none of the ActiveX document properties have been saved using the PropertyBag. Once a property value has been saved, this event will be replaced by the ReadProperties event.

  • The ReadProperties event occurs instead of the InitProperties event only after a property has been saved using the PropertyBag. You must add code to actually read the property.

  • The EnterFocus event occurs when any object, including the ActiveX document, receives focus.

  • The ExitFocus event occurs when no object on the ActiveX document, including the document itself, has focus any longer.

  • The Resize event occurs every time a container is resized.

  • The Scroll event occurs whenever the user clicks on the container's scrollbar or the scrolling region of the container, or if the user drags the scrollbar. Note: Scrollbars appear on the container only when the container's Viewport is sized smaller than the MinWidth or MinHeight of the UserDocument.

  • The WriteProperties event occurs immediately before the Terminate event, when, during the life of the ActiveX document, at least one property value has changed. To notify the container that a property has changed, use the PropertyChanged statement. This is covered in greater detail in "Persisting ActiveX Document Data" in this chapter.

  • The Terminate event occurs when the ActiveX document is about to be destroyed. You can use the Termination event to clean up any object references by setting all global object references to Nothing.

    Note In Internet Explorer 4.0, the ActiveX document will be terminated as soon as the user navigates away from the document. In Internet Explorer 3.0, an ActiveX document is stored in a cache of four documents. When the user loads or navigates to a fifth document, the ActiveX document will be terminated.

Opening an ActiveX Document Before Saving Properties

When the user opens an ActiveX document on which no properties have been saved, the following events will occur.

Event | What Gets Done
Initialize | Document created but not sited in container yet.
InitProperties | Default values for properties are set.
Show | The document is shown in the container. The document has been sited in the container, therefore container properties become available.
EnterFocus | The document gets focus.

Opening an ActiveX Document After Saving Properties

If the user has saved any property, the ReadProperties event will occur instead of the InitProperties event.

Event | What Gets Done
Initialize | Document created but not sited in container yet.
ReadProperties | Properties are read through the PropertyBag object's ReadProperties method.
Show | The document is shown in the container.
EnterFocus | The document gets focus.

Show and Hide Events

Possibly, the two most important events for an ActiveX document are the Show and Hide events.

These events are especially important for these reasons:

  • The Show event occurs whenever the user navigates to the document (in a browser). You can use this event to check for global object references set by other ActiveX documents.

  • The Show event occurs after the ActiveX document is fully sited in the container (unlike the Initialize property). Because you can't count on the InitProperties event to always occur, the Show event can be used as another event to verify the container. See "Determining the ActiveX Document's Container Programmatically" in this chapter for an example.

  • The Hide event occurs whenever the user navigates off the document (in a browser). The event also occurs immediately before the Terminate event. Use the event to destroy any global object references before navigating to another document.

    Note In Internet Explorer 3.0, an ActiveX document is stored in a cache of four documents. When the user loads or navigates to a fifth document, the ActiveX document will be terminated. The Hide event will occur when the user navigates off the ActiveX document to another document, or when Internet Explorer 3.0 is terminated while the document is being viewed or is still within the cache of active documents.

For More Information The topic "Life Cycle of a UserDocument," one of the step by step procedures in "Creating an ActiveX Document" demonstrates the key events in the life of an ActiveX document.


Design Considerations for Document Objects

Design Considerations for ActiveX Documents

Before designing an ActiveX document, you must be aware of a few considerations.

  • ActiveX documents are not stand-alone applications.

    An ActiveX document can only exist in a container. Because containers vary, you can't always predict the capabilities and limitations of your ActiveX document environment. At best, you can target a particular container or set of containers, and degrade gracefully in all others.

  • For ActiveX projects that need to exchange data between multiple ActiveX documents, you must implement global object variables and object references.

    Global object references, however, come with some overhead: you must track them to ensure they aren't overwritten inadvertently, and you must be sure that they are released. This is covered in more detail in "Ensuring Communication Among ActiveX Documents" in this chapter.

  • Navigation, or how to get from one ActiveX document to another, may vary from one container to another.

    For example, to show another ActiveX document in the Visual Basic development environment, you must use the CreateToolWindow function — which doesn't require the .vbd file. In contrast, to show a second ActiveX document in Internet Explorer, you must use the Hyperlink object's NavigateTo method with the .vbd file.

  • To enable one ActiveX document to pass data to another, you must implement public properties or methods.

  • You cannot place an OLE Container control or embedded objects (such as Microsoft Word or Microsoft Excel documents) on a UserDocument.

  • In-Process and Out-of-Process ActiveX Documents


In-Process and Out-of-Process ActiveX Documents

When you create an ActiveX document, you are creating an ActiveX component, and as such, the component can be either in-process or out-of-process.

For More Information For a detailed description about the difference between in-process and out-of-process components, see "In-Process and Out-of-Process Components."

The Case for DLLs

There are a few reasons why you may want to create your ActiveX document as an in-process component (.dll file).

  1. The performance of an in-process component, or .dll file, will surpass the performance of the same component compiled as an .exe.

  2. Multiple clients accessing the same .exe can overwrite global data. For example, imagine a suite of ActiveX documents being used by two different instances of Internet Explorer. Also imagine that the suite uses a global object variable to store a numeral, such as an interest rate. If the value of the variable is changed by one set of documents, the other set will begin using the new value with no indication that it has changed.


Targeting a Container

Targeting an ActiveX Document Container

ActiveX documents, like ActiveX controls, cannot be used alone. ActiveX documents must exist in a container, which is itself an object. Most ActiveX documents, however, will be viewed in an application that provides an object that is the actual container of the ActiveX document. Throughout this document, we will therefore refer to these applications as container applications, or applications that can contain an ActiveX document.

Here are three containers, and their advantages:

  • Microsoft Internet Explorer (version 3.0 or later) — Using Internet Explorer, an ActiveX document can be a familiar version of an existing application with added Internet capabilities. For example, an existing application can be enhanced with Web links (using the Hyperlink object). You can also deploy the ActiveX document over the Internet, allowing users to get the latest version of your application.

    Note The deployment of ActiveX documents differs slightly depending on the version of Internet Explorer that you are targeting. For more information on deploying ActiveX documents, see "Manually Deploying ActiveX Components" in Building Internet Applications.

  • Microsoft Office Binder 1.0 (or later) — The Microsoft Binder is an electronic "paper clip" that can hold several disparate documents, such as Word and Excel documents. Use an ActiveX document to add database programming or multimedia display capabilities to binders. For example, a repair manual assembled as a binder of Word documents could be enhanced by the addition of a Visual Basic ActiveX document that displayed video clips of repair procedures.

  • Visual Basic Development Environment Tool Window — The Visual Basic Environment Tool Window is an object created by the CreateToolWindow function. Using this function, you can create a dockable window in the Visual Basic development environment. This window can then contain ActiveX documents that provide additional user interface capabilities to the development environment. For instance, you could create a resource editor or menu editor using the Visual Basic Extensibility Object Model.

Issues for Containers

Because the capabilities of an ActiveX document can depend on the host application, you should consider the following questions:

  • Will this document depend on the features of a particular host application?

    For example, if you are creating an ActiveX document that will run in Internet Explorer, you may want to exploit its ability to show an ActiveX document in one frame, and HTML documents in another.

  • Will I be creating a suite of documents that work together?

    For example, if the document is a simple calculator, it will probably not demand more than a single ActiveX document, and the answer is "no."

If the answer to either of these is "yes," then you will need to investigate the capabilities of the target application(s). Specifically, you will want to know how to navigate from one ActiveX document to another (if you are creating a suite of documents), or how to access the features of the host application. Other than documentation from the creator of the container application, the only way to discover the object model is by using the Object Browser.

One Example: One Goal, Two Containers

To illustrate this, let's examine a multi-document scenario. Imagine you have created two ActiveX documents, and want to open the second by clicking on a button on the first.

If the host application is Internet Explorer, you must use the Hyperlink object's NavigateTo method to get from one document to another, as shown in the following code:

Private Sub cmdGoTo_Click()
   ' Assuming the name of the
   ' destination is AxDoc2.vbd.
   UserDocument.HyperLink.NavigateTo _
      "file://c:\docs\AxDoc2.vbd"
End Sub

On the other hand, Microsoft Binder operates on an entirely different metaphor. Instead of navigating to another document, you must add a section to the Binder. An example of the code is shown in the following:

Private Sub cmdAddSection_Click()
   ' Use the same document: AxDoc2.
   UserDocument.Parent.Parent.Sections. _
      Add , "c:\docs\AxDoc2.vbd"
End Sub

Looking Into the Target Application's Object Model

From the preceding two different methods shown, it becomes apparent that you cannot count on one method to get from one ActiveX document to another in every container. The question then becomes, "How do I discover the method for a target host application?"

One way (besides documentation) to discover an application's methods is to use the Object Browser. For example, to explore the object model of Microsoft Binder, follow these steps:

To discover the objects and methods of Microsoft Binder

Note You must have Microsoft Office Binder 1.0 or later, which can be installed with Microsoft Office.

  1. On the Project menu, click References.

  2. In the scrolling list of available references, click OLE Automation Binder 1.0 Type Library (or Microsoft Binder 8.0 Object Library for Office ’97).

  3. Click OK.

  4. Press F2 to open the Object Browser.

  5. Click the Project/Library Box, and then click Office Binder.

  6. In the Classes List, click Sections.

  7. In the Members of List, click Add. You will now see the syntax of the Add method, which you can use to add the ActiveX document to the Sections collection.

Navigating Through the Object Model

You may have noticed that the Object Browser hasn't given you a complete guide to the object model. Compare the code:

UserDocument.Parent.Parent.Sections.Add _
, "c:\docs\AxDoc2.vbd"

with the information found in the Object Browser. The difference lies in the addition of "UserDocument.Parent.Parent" to the code.

The discrepancy arises because the ActiveX document is itself "buried" lower within the object model. To Office Binder, the ActiveX document is just one more Section object in a Sections collection. Thus the code TypeName(UserDocument.Parent) returns "Section," while TypeName(UserDocument.Parent.Parent) returns "Binder."

In order to navigate through the hierarchy to the top of the object model, the code uses the reference returned by the Parent property. However the code must navigate one final level — again using the Parent property. This results in the Parent.Parent code. The code then navigates back down to the Sections collection, culminating in the Add method.

For More Information To learn about the Object Browser, see "Finding Out About Objects," in "Programming With Objects," of the Programmer's Guide. For details about navigating through the Object Model, see "Navigating Object Models" in "Programming with Components," of the Programmer's Guide.


The Microsoft Binder as a Container

The Microsoft Binder as an ActiveX Container

The Microsoft Office Binder is an electronic "paper clip" — you can add several disparate kinds of documents to the Binder that have some relation to each other. "Documents" can include:

  • Word documents

  • Excel documents

  • PowerPoint presentations

This topic covers the basics of using the Binder as a container of ActiveX documents.

Adding an ActiveX Document

There are two ways to add an ActiveX document to a Binder:

  • Add an ActiveX document as a section.

  • Add an ActiveX as a file.

Adding the ActiveX Document as a Section

When you add an ActiveX document as a section, the Binder adds the document using its ProgID. When an ActiveX document is added this way, it's akin to adding a blank Word document — the document is a "blank" document. To identify the "blank" document, Binder looks for the ProgID of the ActiveX document. In brief, the ProgId (or programmatic ID) is the class name and the component name separated by a period. For example, the ProgID of an ActiveX document with no changes to the default settings would be "Project1.UserDocument1".

Note When you add a new section, you are create a new object from the ActiveX document class. Thus the InitProperties event will always occur (instead of the ReadProperties event).

For More Information For details about Programmatic IDs, see "Adding Classes to Components" in "General Principles of Component Design."

To add an ActiveX document as a section

  1. Start Microsoft Binder.

  2. On the Section menu, click Add to open the Add Section dialog box.

  3. From the scrollable list of ProgIDs, double-click your ActiveX document.

Adding the ActiveX Document as a File

When you add an ActiveX document as a file, you can select a particular .vbd file that you may have saved earlier.

Note If you have created an ActiveX document that persists data (through the PropertyBag), and you have saved data to the file, adding that .vbd file will cause the ReadProperties event to occur. But if you did not write code to persist data, the InitProperties event will fire instead. For details on persisting data, see "Persisting ActiveX Document Data" in this chapter.

To add an ActiveX document as a section

  1. Start Microsoft Binder

  2. On the Section menu, click Add from File to open the Add from File dialog box.

  3. Navigate to your .vbd file, and click Add.

Saving an ActiveX Document in the Binder

As with any other document in the binder, you can save the ActiveX document as one of a group of documents. When you save the ActiveX document in this way, the WriteProperties event will fire. However, to actually persist data in the document, you must have written the proper code in the WriteProperties event.

To save an ActiveX document in the Binder

  1. Add the ActiveX document, either as a section or a file.

  2. On the File menu, click Save Binder to open the Save Binder As dialog box.

  3. Type the name of the Binder and click Save.

Programming the Binder

You can create ActiveX documents that programmatically manipulate other sections. In order to do this, you must know the object model of the Binder.

The Binder's object model is built around a collection of Section objects, the Sections collection. And each Section object can contain a Microsoft Excel, Word, PowerPoint, or Visual Basic ActiveX document. Using this knowledge, it's possible to walk through the Sections and get a reference to each document. Using that reference, you can then manipulate the document's objects. This is easier to explain in a simple procedure.

Example: Manipulating an Excel Spreadsheet

The following scenario is necessarily simple: add an Excel worksheet and an ActiveX document to the Binder, and using an ActiveX document, retrieve a value from the worksheet.

To retrieve a value from an Excel worksheet

  1. In Visual Basic, on the File menu, click New Project. Then click the ActiveX Document DLL icon.

  2. In the Project Explorer window, double-click UserDocument1 to show its designer.

  3. Draw a TextBox control on the designer, and change its name to "txtCell"

  4. Draw a CommandButton control on the designer, and change it's caption to "Get Value".

  5. On the Project menu, click References to show the References dialog box.

  6. From the scrolling list of references, click the OLE Automation Binder 1.0 Type Library checkbox.

  7. Double-click the CommandButton to show its code window.

  8. Add the following code to the Command1 click event.
    Private Sub Command1_Click()
Dim i As Integer ' Counter
Dim objX As Sections ' Object variable
Set objX = UserDocument.Parent.Parent.Sections
For i = 1 to objX.Count
If TypeName(objX.Item(i).Object) = _
"Worksheet" Then
txtCell = objeX.Item(i).Object.Range. _
("A1").Value
End If
Next i
End Sub
  9. Press f5 to run the project.

  10. Minimize the Visual Basic instance.

  11. Start Microsoft Binder.

  12. On the Section menu, click Add Section.

  13. Double-click Microsoft Excel Worksheet. A blank worksheet is now added to the Binder.

  14. Type a distinctive number or text into cell A1 (it's already selected for you, so you can simply begin typing).

  15. On the Section menu, click Add Section.

  16. Double-click Project1.UserDocument1.

  17. Click Get Value. The text or number you just typed into the spreadsheet will appear in the textbox.

The Code Explained

Because we want to early-bind the object variable, step 6 instructs you to add a reference to the OLE Automation Binder Type Library. Thus in the code, we declare the object variable of type Sections. The code then sets the variable to the Binder's Sections collection.

Once we have a reference to the collection, we can iterate through its members using the For statement. Within the For look, we then use the TypeName function to return the type of each object in the section. If the TypeName function returns "Worksheet," we know that it's the Excel worksheet in the collection. Having determined that, the code simply returns the contents of cell A1 to the TextBox control.

Another Example: Adding a Second ActiveX Object

Adding another ActiveX document to the Binder is also accomplished using the Sections collection. This time, however, we use the Add method of the Sections collection.

Option Explicit
Private mMyDoc2 As axdDoc2 ' Module level variable

Private Sub AddDoc_Click()
   Dim objX As Sections ' Object variable
   Set objX = UserDocument.Parent.Parent.Sections
   Set mMyDoc2 = objX.Add( , "c:\axdDoc2.vbd")
End Sub

The preceding code again uses the Sections collection, but instead uses the Add method to add a second ActiveX document to the binder. You may have noticed that the code also declares a module-level variable as type axdDoc2 (assuming that is the name of the second document). The code sets the object variable to the reference returned by the Add method. With that reference, you can then access the public properties and methods of the second document, as shown:

txtName.Text = mMyDoc2.Name
   ' Assuming there's a public property called Name.
mMyDoc.MyMethod ' Assuming a public function exists.


The Visual Basic IDE as an ActiveX Document Container

Using the Extensibility object model and add-ins, you can greatly extend the Visual Basic development environment. You can also create ActiveX documents to extend the Visual Basic IDE. Develop your ActiveX document as you would any other, but use the CreateToolWindow function to create a tool window in the IDE which will contain the ActiveX document.

Note This topic assumes you have some knowledge of add-ins — what they are, and how to create them. If you are unfamiliar with add-ins, see "Add-Ins Overview," and "Creating a Basic Add-In," in "Extending the Visual Basic Environment with Add-Ins."

Using the CreateToolWindow function has the following advantages over other add-ins:

  • The CreateToolWindow function returns a dockable window.

  • The window correctly follows window minimize and maximize semantics without any coding. For example, tool windows will "remember" where they were last placed or docked.

The CreateToolWindow Function

The CreateToolWindow function is a method of the Windows collection. The function creates a tool window — a container for the ActiveX document — and returns a reference to the window. The following series of steps outline what happens in a typical scenario:

  1. On the Add-Ins menu, the user clicks AddIn Manager, and a list of all available add-ins appears.

  2. From the list, the user clicks "MyActiveXAddIn."

  3. As the add-in is created, the OnConnect event occurs.

  4. In the OnConnect event, the code invokes the CreateToolWindow function, creating the tool window.

  5. The function call returns a reference to the newly created tool window that contains the ActiveX document.

  6. Using the reference, the code can manipulate tool window properties. For example, to show the tool window, the Visible property is set to True.

To create a simple Add-in using an ActiveX document

  1. On the File menu, click New Project to open the New Project dialog box. Double-click the AddIn icon to create a new Add-in project.

  2. Click the Project menu, then click Add UserDocument to open the Add UserDocument dialog box. Double-click the UserDocument icon to add a UserDocument to the project.

  3. In the Properties window, double-click Name, and change the name of the UserDocument to "axdUserDoc."

  4. In the Project Explorer window, double-click the Connect icon to open the class module's code window. There is already a fair amount of code written in the template, and we will just add a bit to it. Look at the Declarations section, and modify it to resemble the following code:
    Implements IDTExtensibility

Public FormDisplayed         As Boolean
Public VBInstance             As VBIDE.VBE
Public mcbMenuCommandBar As Office.CommandBarControl
Dim frmAddIn                  As New frmAddIn
Public WithEvents MenuHandler As CommandBarEvents

' Add the following declarations for the
' CreateToolWindow function:
Private mWin As Window
Private mobjDoc As axdUserDoc
Const guidMyTool$ = "(4244B234-E45F-12dg-8O3f-04884)"

    The code you added declares the necessary object variables for the CreateToolWindow function. The Window variable "mWin" will be set to the reference returned by the function; you can then use this reference to show or hide the window. The second variable, "mobjDoc," will be set to reference the UserDocument. You can then use this reference to manipulate the ActiveX document. Finally, the constant "guidMyTool" is used by the function to identify the window instance; for every tool window you create, this string must be unique.

  5. In the code window, scroll down to the OnConnection event. We will add code to this event, but to avoid confusion, select all of the code in the event, and delete it. Then add the following code:
    Private Sub IDTExtensibility_OnConnection(ByVal VBInst As Object, ByVal ConnectMode As VBIDE.vbext_ConnectMode, ByVal AddInInst As VBIDE.AddIn, custom() As Variant)
Set mWin = AddInInst.VBE.Windows. _
CreateToolWindow(AddInInst, _
"MyAddin.axdUserDoc", _
"ActiveX Caption", guidMyTool, mobjDoc)
mWin.Visible = True
End Sub

    This code uses the Set statement to set the Window variable "mWin" to a reference to the window created by the function. This reference is then used to set the Visible property of the window to True.

  6. Press ctrl+g to view the Immediate window.

  7. Type "AddToIni" and press return. You have just added the progID of the project to the VBAddIn.ini file, a necessary step when creating an Add-in.

  8. Press f5 to run the project.

  9. Minimize the Visual Basic window. This will prevent confusion as you will start another Visual Basic instance.

  10. Start another instance of Visual Basic.

  11. When the New Project dialog appears, press enter to start a new Standard .exe project.

  12. On the Add-Ins menu, click Add-In Manager to display the Add-In Manager dialog box.

  13. From the list of available add-ins, click My Add-In, the click OK. The ActiveX document you just created will now appear as a part of the Visual Basic development environment.

  14. After viewing the document, you will want to shut it down. To do this, open the Add-in Manager dialog box again, and clear the MyAddIn checkbox, then click OK.

Adding a Second Window

Having an understanding of the mechanics of the function allows us to create a more complex scenario: creating a second window, containing a second ActiveX document, from the first.

To add a second ActiveX document

  1. If you haven't already, quit the second instance of Visual Basic.

  2. On the Project menu, click Add UserDocument and add a second UserDocument to the project.

  3. Change the name of the UserDocument to "axdUserDoc2."

  4. Add a TextBox control to the UserDocument.

  5. Double-click the designer, and add the following code:
    Public Property Get Text () As String
Text = Text1.Text
End Property

Public Property Let Text(ByVal newText As String)
Text1.Text = newText
End Property

    The preceding code creates a public property, which you will set when you create the window for the document.

  6. In the Project Explorer window, double-click AddIn to open the code module. Add the following code to the Declarations section.
    Public gAddIn As vbide.Addin

    The new line declares a global variable that will be set with a reference to the add-in instance.

  7. In the Project Explorer window, double-click Connect to open the class module. Look at the OnConnect event, and modify it to resemble the following code:
    Private Sub IDTExtensibility_OnConnection(ByVal _
VBInst As Object, ByVal ConnectMode As _
VBIDE.vbext_ConnectMode, ByVal AddInInst As _
VBIDE.AddIn, custom() As Variant)
Set mWin = AddInInst.VBE.Windows. _
CreateToolWindow(AddInInst, _
"MyAddin.axdUserDoc", _
"ActiveX Caption", guidMyTool, mobjDoc)
mWin.Visible = True

' Add this new code:
Set gAddIn = AddInInst
End Sub

    The new line sets the global variable to the add-in instance, making it available to create more windows.

  8. In the Project Explorer Window, double-click axdUserDocument to bring its designer forward.

  9. On the Toolbox, double-click the CommandButton icon to add a CommandButton control to the UserDocument.

  10. Change the caption of the button to "Show Next."

  11. Double-click the designer to open its code window, and add the following code:
    Private mWin2 As Window
Private mDoc2 As axdUserDoc2
Const guidMyTool$ = "Xiang19X67Hangzhou4/27"

Private Sub Command1_Click()
set mWin2 = gAddIn.VBE.Windows. _
CreateToolWindow(gAddIn, _
"MyAddin.axdUserDoc2", _
"Second ActiveX Document", _
guidMyTool, mDoc2)
mWin2.Visible = True
mDoc2.Text = "This is the second document."
End Sub
  12. Press f5 to run the project.

  13. Run another instance of Visual Basic.

  14. When the New Project dialog appears, press enter to start a new Standard .exe project.

  15. On the Add-Ins menu, click Add-In Manager to display the Add-In Manager dialog box.

  16. From the list of available Add-ins, select the My Add-In checkbox, then click OK. The ActiveX document you just created will now appear as a part of the Visual Basic development environment.


Determining the ActiveX Document's Container Programmatically

The characteristics of your user's target application have a large impact on your ActiveX document design. Consequently, your code should verify which container is being used in order to react appropriately. For example, if you intend your document to be viewed in the Visual Basic Development Environment Tool Window, your code should check the container before accessing extensibility objects (which are unavailable in Internet Explorer).

To determine the container of the ActiveX document, use the TypeName statement with the Parent property of the UserDocument. A simple example of this is shown:

Dim strContainer As String
strContainer = TypeName(UserDocument.Parent)

The following table shows the three possible strings returned by Internet Explorer, Microsoft Binder, and the window created by the CreateToolWindow function in the Visual Basic development environment:

Container | Return String
Internet Explorer | IwebBrowserApp
Microsoft Binder | Section
Window | Window

Defensive Programming

Because you may never know which container the user will actually use to view an ActiveX document, you should prepare for the "wrong" container (a container which lacks the features your ActiveX document needs to function optimally). Although the TypeName function will not return the version of the container, it will at least allow you to determine if the container is unequivocally "wrong." In that case, you may inform the user that some functionality is not available with the container, and that she should use the "right" container to view the document.

Because the Show event occurs when an ActiveX document is sited on the container, it is the best event for determining the container. Be aware, however, that the Show event occurs whenever the document is shown (and this behavior varies according to the container application). To sidestep this problem, declare a module level variable to function as a flag. After verifying the container, the flag is reset. Subsequent Show events will then check the flag, and avoid redundantly checking the container again.

Option Explict
Private mblnSHOWN As Boolean

Private Sub UserDocument_Show()
   If Not mblnSHOWN Then
      Dim strContainer As String
      strContainer = TypeName(UserDocument.Type)
      ' Use the Select Case statement to test.
      Select Case strContainer
         Case "IwebBrowserApp"
         ' Supported container: continue to open app.
      Case "Section"
         ' Supported container: continue to open app.
      Case Else ' Handle other unknown containers.
         ' Unsupported container: exit gracefully.
         MsgBox "Sorry, please open this " & _
         "document with Internet Explorer " & _
         "3.0 or later."
         End Select
      mblnSHOWN = True ' Reset the flag
   End If
End Sub

Note You can't use the Initialize event to test the container because when that event fires the document is not yet sited. For an explanation of when the document is sited, see "Key Events in the Life of a UserDocument" in this chapter.


Designing the User Interface for ActiveX Documents

Designing an ActiveX document's User Interface has some similarities, and some major differences, to designing the User Interface for a standard application. Similarities include the ability to add forms and menus to the project. However, the differences result from one overwhelming fact: The ActiveX document is always viewed in a container. In other words, the User Interface of your ActiveX document will always be viewed within the context of another application. The following topics explain what you should consider as you design a User Interface for an ActiveX document.

  • Menu Design for ActiveX Documents Add menus to your ActiveX documents with the Menu Editor.

  • ActiveX Document Viewport The container's Viewport determines the size of the window an ActiveX document is being viewed in. This topic shows you how to control what is shown in the ViewPort.

  • Adding Forms and Modules to ActiveX Document Projects Add forms to your ActiveX document project as you would with any other. The topic also covers the NonModalAllowed property.


Menu Design for Document Objects

Menu Design for ActiveX Documents

You can create your own menus for your ActiveX designer by using the Visual Basic Menu Editor. Containers that support ActiveX documents will also automatically merge any Help menu you create with the container's Help menu.

Tell Them Who You Are

When a user comes across your document — whether through a browser, binder, or other application — she may not immediately recognize it. For this reason, it is highly recommended that you always add an "About" menu to every ActiveX document you create.

You should also be aware that some containers will not display their menus correctly unless the ActiveX document merges a Help menu with the container's Help menu.

To merge an About box with the container's Help menu

  1. On the Project menu, click Add Form.

  2. In the Add Form dialog box, double-click the AboutBox with SysInfo icon to add an About Box form to the project.

  3. Set the Caption property of the form and Label controls appropriate to your ActiveX document.

  4. In the Project Explorer window, double-click your UserDocument to bring its designer forward.

  5. Click the UserDocument designer to select it, and on the Tools menu, click Menu Editor to display the menu editor dialog box.

  6. Click the Caption box and type &Help.

  7. Click the Name box and type mnuHelp.

  8. Click the NegotiatePosition box, and click Right.

  9. Click the Next button to create a new menu item.

  10. In the Caption box type "About" and the name of your ActiveX document.

  11. In the Name box type mnuAbout.

  12. Click the right-facing arrow button to indent the menu item.

  13. Click OK.

  14. Double-click your ActiveX designer to bring its Code window to the front.

  15. Add the following code to the mnuAbout Click event:
    Private Sub mnuAbout_Click()
frmAbout.Show vbModal
End Sub


The ActiveX Document Viewport

ActiveX Document Viewport

An ActiveX document cannot be viewed as a stand-alone application — it must be viewed in a container application. The area in the container application through which the document is visible is also known as the Viewport. The following figure illustrates the Viewport of Internet Explorer.

Naturally, the Viewport can be resized by the user, and the developer has no control over this. However, using the SetViewPort method, you can manipulate which part of an ActiveX document is shown in the Viewport. First, however, let's examine other Viewport features, as understanding them will make it easier to the SetViewPort method.

The ViewPortTop, ViewPortLeft, ViewPortHeight, and ViewPortWidth Properties

The ViewPortHeight, ViewPortWidth, ViewPortLeft, and ViewPortTop properties are read-only properties. They return the coordinates of the Viewport relative to the ActiveX document. The following figure shows Internet Explorer overlaying the ActiveX document. The Viewport properties are called out. Notice that in this case the ActiveX document is larger than the Viewport. In reality, when the Viewport is too small to show the entire document, scrollbars will appear. (The scrollbars, however, can be turned off by setting the ScrollBars property to False. Note that this is a read-only property, and must be set at design time).

The following figure shows the Viewport properties after the user has scrolled to see the other parts of the document; notice that only the ViewPortLeft and ViewPortTop properties have changed:

Finally the following figure shows the Viewport properties after the user has resized the Viewport. Notice that the ViewPortHeight and ViewPortWidth have now changed:

The MinHeight, MinWidth Properties

Because an ActiveX document is contained by another application, you will have no control over how much space the container application gives to your ActiveX document. However, you will be able to specify the minimum space your ActiveX document needs before scrollbars appear by setting the MinHeight and MinWidth properties.

When you design an ActiveX document, the Height and Width of the UserDocument designer become the default settings for the MinHeight and MinWidth properties. Also, the MinHeight and MinWidth properties are always set to the ScaleMode of the UserDocument.

In the following figure, the ActiveX document is smaller than the Viewport. In this case, the container has no scrollbars, and the "empty" part of the Viewport is filled with the BackColor of the ActiveX document. However, if the user resizes the container so that the Viewport is smaller than the ActiveX document, scrollbars will appear.

The MinHeight and MinWidth properties are settable. In other words, you can cause scrollbars to appear on the container by simply setting the MinHeight and MinWidth properties to be larger than the Viewport properties. For example, imagine an ActiveX document that allows the user to insert any image into an PictureBox control, and that the control resizes automatically to the size of the image. By setting the MinHeight and MinWidth properties to always be larger than the control, you can assure that scrollbars will always appear to let the user see the entire image:

Private Sub Picture1_Resize()
   UserDocument.MinHeight = _
      Picture1.Height + 100
   UserDocument.MinWidth = _
      Picture1.Width + 100
End Sub

Using the SetViewPort Method to Scroll

You can cause the Viewport to scroll to a specific coordinate by using the SetViewPort method. The method requires two arguments, the Top and Left arguments. For example, by setting these arguments to the Left and Top properties of a TextBox placed on the document, you can programmatically ensure that the TextBox with the focus always appears in the top left corner of the ActiveX document. This is illustrated:

The HScrollSmallChange and VScrollSmallChange Properties

The HScrollSmallChange and VScrollSmallChange properties function like the SmallChange property (of the HScolllBar and VScrollBar controls).

Note There is no "HScrollLargeChange" or "VScrollLargeChange" property because the Viewport handles this for you automatically. The "LargeChange" value is determined by the current height and width of the Viewport.


Adding Forms and Modules to ActiveX Document Projects

When you create a Visual Basic project, you are used to starting with a standard Form. When you create an ActiveX document, you are by no means limited to using only ActiveX documents: You can add standard forms and modules to your project, as you would to any other project.

Modeless Dialog Boxes Sometimes Aren't Allowed

When you create an ActiveX DLL, you'll have one limitation: Depending on the container, modeless dialog boxes may or may not be displayed. For example, imagine that you've created an ActiveX DLL that includes an ActiveX document that shows a modeless form. If you view the document in Office Binder, showing the modeless form will present no problems. On the other hand, if you view the same document in Internet Explorer, attempting to display the same modeless form will raise an error.

Note With Internet Explorer 4.0 or later, the ability to show modeless forms is dependent on the threading model. This is discussed in "Apartment-Model Threading in Visual Basic" in "Building Code Components."

Avoid the Problem Using the NonModalAllowed Property

However, the problem of not being allowed to display a modeless dialog box can be side-stepped. Using the NonModalAllowed property, you can check before displaying a form, and react appropriately. In short, the NonModalAllowed property (a read-only property), returns True or False, depending on whether or not the App object allows modeless forms to be displayed. Simple code can then be written as in the following example:

Private Sub cmdShowMe_Click()
   If App.NonModalAllowed Then ' Show the form
                                ' modeless.
      frmModeless.Show vbModeless, Me
   Else ' Show the form modally.
      FrmModeless.Show vbModal, Me
   EndIf
End Sub

For More Information For more details about displaying forms, see "Displaying Forms from Code Components" in "General Principles of Component Design."


Adding Internet Features to ActiveX Documents

ActiveX documents allow you to take advantage of the Internet through use of the Hyperlink object, and the ability to download data asynchronously.

  • Downloading Data Asynchronously Asynchronously download data using the AsyncRead method in conjunction with the AsyncReadComplete event.

  • Using the Hyperlink objects with ActiveX Documents Using the properties and methods of the Hyperlink object, your ActiveX document can request a hyperlink-aware container, such as Microsoft Internet Explorer, to jump to a given URL.


Downloading Data Asynchronously

Asynchronous downloading is like the process of having a pizza delivered to your home. You first make a call to a pizza place and place an order. You then go about your business while others make the pizza. When the pizza is ready, a delivery person knocks on your door and informs you that your dinner has arrived.

With the UserDocument, the equivalent of the phone call is the AsyncRead method. Consequently, the AsyncReadComplete event is the equivalent of the pizza delivery person's knock on your door. This may be easier to understand in a simple scenario: loading a text file into a TextBox control.

To asynchronously load a Picture object into a PictureBox control

  1. Press ctrl+n to begin a new project.

  2. Click ActiveX Document DLL to start a new ActiveX DLL project.

  3. In the Project Explorer window, double-click the UserDocument1 icon to open its designer.

  4. On the Toolbox, click the TextBox control icon and draw a TextBox control on the designer. Set the MultiLine property to True.

  5. Double-click the UserDocument designer to open its code window.

  6. Add the following code to InitProperties event:
    Private Sub UserDocument_InitProperties()
Dim strPath As String ' file path
' Change the path to a different text file if you
' wish. If you are on an intranet, you can
' also set the path to another computer on
' which you have access privileges.
strPath = "c:\Windows\Readme.txt"
' Now invoke the AsyncRead method. Set the
' type to vbAsyncTypeFile (a file), and the
' name of the property to Pizza.
UserDocument.AsyncRead strPath, _
vbAsyncTypeFile,  "Pizza"
End Sub

    The preceding code is the "Pizza call." We gave the third argument (the property name argument) a distinctive name.

  7. Add the following code to the designer. This code is the "knock on the door." It informs you that your "pizza" has arrived, and you should deal with it:
    Private Sub UserDocument AsyncReadComplete _
(AsyncProp As VB.AsyncProperty)
Dim FileNum as Long

' Use a Select Case statement to determine which
' property is being delivered.
Select Case AsyncProp.PropertyName
Case "Pizza"

' Open the file and read the contents
' into the TextBox
FileNum = FreeFile
Open AsyncProp.Value For Input As FileNum
Text1.Text  = Input(LOF(FileNum), FileNum)
Close Filenum
End Select
End Sub
  8. Press f5 to run the project.

  9. Internet Explorer (or your default browser) will open and display your .vbd file. If you have used a file on your own computer, the call will happen too quickly for you to perceive any delay. However, if you have access to a larger file on a remote computer, the effect may be more noticeable.

To take our pizza delivery example one step further, imagine that there is a guarantee that the pizza will be delivered within 30 minutes or it’s free. In this case, you might be very interested in the progress of the pizza delivery person. You can check on the progress of your "pizza" by adding code to the AsyncReadProgress event:

Private Sub UserDocument AsyncReadProgress _
(AsyncProp As VB.AsyncProperty)
   ' Use a Select Case statement to read the StatusCode
   Select Case AsynchProp.StatusCode
      Case vbAsynchStatusCodeSendingRequest
         MsgBox "Attempting to connect"
      Case vbAsynchStatusCodeEndDownloadData
         MsgBox "Download complete"
      Case vbAsynchStatusCodeError
         MsgBox "Error – aborting transfer"
         CancelAsynchRead "Pizza"
   End Select
End Sub

Now when you run the project, the code in the AsyncReadProgress event will display two message boxes: first when an attempt is made to connect to the file, and then when the transfer is complete. If you set a breakpoint on the Select Case statement and step through the code, you’ll notice that the vbAsynchStatusEndDownloadData status code is returned before the AsyncReadComplete event is fired. This gives you one last chance to cancel or redirect the file (akin to refusing to answer the door when your "Pizza" arrives).

If an error occurs during the transfer (for example, losing a network connection) the vbAsyncStatusCodeError status code is returned, allowing you to call the CancelAsyncRead method to cancel the transfer and avoid a run time error.

To see a list of possible status codes returned by the AsyncReadProgress event, look at the AsyncStatusCodeConstants in the Object Browser.

For More Information For an in-depth look at the AsyncRead method and the AsyncReadComplete and AsyncReadProgress events, see "Adding Internet Features to Controls" in "Building ActiveX Controls." Although the topic covers the UserControl object, the mechanics are identical.


Using the HyperLink Object with ActiveX Documents

If you target a Web browser as your container application, and you create more than one ActiveX document, you must use the HyperLink object to navigate between the different documents.

The Hyperlink object gives your documents access to ActiveX hyperlinking functionality. Using the properties and methods of the Hyperlink object, your ActiveX document can request a hyperlink-aware container, such as Microsoft Internet Explorer, to jump to a given URL.

The NavigateTo Method

The NavigateTo method executes a hyperlink jump to the target specified in the URL argument. The URL can be set to an HTML, Word, or Excel document as well as a .vbd file. For example, the following code navigates to the www.microsoft.com Web page:

Private Sub cmdGoTo_Click()
   UserDocument.Hyperlink.NavigateTo _
      "http://www.microsoft.com"
End Sub

To jump from one ActiveX document to another, you can use the same method, as shown.

Private GoNextDoc_Click()
   ' Assuming the next ActiveX document is named
   ' MyDoc2.vbd
      UserDocument.Hyperlink.NavigateTo _
      "file://c:\ActXDocs\MyDoc2.vbd"

Dynamically Constructing an Absolute Path

When you compile an ActiveX project with multiple ActiveX documents, Visual Basic creates the .vbd files in the same directory as the ActiveX .dll or .exe. However, if you move the .vbd files into another directory, you must give the NavigateTo method a fully qualified path for every .vbd file. However, because you cannot determine where a user will place the .vbd files, you must be able to dynamically create the absolute path.

The following code dynamically constructs the path of a .vbd file by parsing the LocationName property of Internet Explorer which returns the absolute path of the document currently displayed by Internet Explorer. The code parses this path, and discards the name of the current .vbd file. The code then appends the name of a second ActiveX document to the remainder of the path.

Dim strPath As String    ' String to be parsed
Dim strAbsPath As String    ' Result of parsing
Dim intI As Integer      ' Character position counter

' Return the path of the current ActiveX document.
strPath = Trim$(UserDocument.Parent.LocationName)

' Find the position of the last separator character.
For intI = Len(strPath) To 1 Step -1
   If Mid$(StrPath, intI, 1) = "/" Or _
      Mid$(StrPath, intI, 1) = "\" Then Exit For
Next intI

' Strip the name of the current .vbd file.
strAbsPath = Left$(StrPath, intI)

' Navigate to the second ActiveX document.
UserDocument.Hyperlink.NavigateTo _
   strAbsPath & "MyDoc2.vbd"

Automatically Starting a Browser

If the NavigateTo method is invoked from an ActiveX document contained in an application that supports the Hyperlink object (such as Internet Explorer), the same application instance will be used to "go" to the target document. If the application does not support hyperlinking (Microsoft Binder, for example), then an application that does (determined by the registry) will be started to handle the request. In other words, if the method is invoked from within an application that doesn't support hyperlinking, another that does will be started.

Navigating Between ActiveX Documents

The NavigateTo method can be used to jump from one ActiveX document to another; in fact, it is the only way to start another document in a browser. Thus, if you create a suite of ActiveX documents, and your target container application is a browser that supports the Hyperlink object, you must use the NavigateTo method to open the next document. For example, the following code will cause the second (in a suite) document to appear in the Internet Explorer when the user clicks the cmdGoNext button on the first document:

Private Sub cmdGoNext_Click()
   ' The second ActiveX document's file is named _
   ' "ActiveDoc2.vbd"
   UserDocument.HyperLink.NavigateTo _
   "file://c:\ActiveX\ActiveDoc2.vbd"
End Sub

The NavigateTo method also includes a second argument, the FrameName argument, that specifies a particular frame in the document to jump to.

The GoBack and GoForward Methods

The GoBack and GoForward methods execute a jump forward or backward to the next document in the browser's history list. These methods only work with hosts that are Hyperlink aware (such as Internet Explorer 3.0 and later).

When implementing the GoForward or GoBack method, be sure to use error checking in case there is no document in the history list to jump to. An example is shown:

Private Sub cmdGoForward_Click()
   On Error GoTo noDocInHistory
   UserDocument.Hyperlink.GoForward
   Exit Sub
noDocInHistory:
   Resume Next
End Sub


Persisting ActiveX Document Data

Data persistence is the ability of a component to store and retrieve data. How an ActiveX document persists data depends greatly on the container application in which the document appears. Some applications allow you to persist data by writing to an interface of the application. For example, the Internet Explorer 3.0 (and higher) and Microsoft Office Binder allows you to write to a file using the PropertyBag. Other applications, however, do not provide any such intrinsic method of storing data. In those cases, you must use another method, such as writing data to files. Let's start with the PropertyBag.

The PropertyBag

The PropertyBag is an object that allows you to save data to a file. The file can be either the .vbd, .obd, or some other kind of file — it depends on the application. The PropertyBag has two methods, the WriteProperty method, and the ReadProperty method. The PropertyBag object is exposed as part of the WriteProperties and ReadProperties event declaration.

Saving Data with the WriteProperty Method

The process of saving data is outlined:

  1. In an event that occurs when a property has changed, invoke the PropertyChanged method. The method notifies the container that a property has changed.

  2. In response to the PropertyChanged method, the container is "dirtied." And before the container terminates, the WriteProperties event occurs.

  3. In the WriteProperties event, use the WriteProperty method to save the property to the .vbd file.

The following code shows a simple illustration of this process:

Private Sub Text1_Change()
   PropertyChanged "Text" ' Notify container a
                           ' property has changed.
End Sub

Private Sub UserDocument_WriteProperties _
   (PropBag As VB.PropertyBag)
   ' Write the Property to the Property Bag.
   UserDocument.WriteProperty "Text1", Text1.Text, _
      "Hello"
End Sub

Reading Properties

The next time the ActiveX document is opened, the ReadProperties event occurs. The PropertyBag is available in the event, and you can retrieve the persisted data using the ReadProperty method. The following code retrieves the data stored in the preceding code:

Private Sub UserDocument_ReadProperties _
   (PropBag As VB.PropertyBag)
   ' Read the property back into the TextBox control.
   Text1.Text = ReadProperty("Text1", "Hello")
End Sub

Saving a Default Value

You may have noticed that two code samples included an extra argument: the default value "Hello." Why provide a default when saving the property value? Before saving the value, the WriteProperty method compares the property value with this default. If they are the same, the property value doesn’t have to be saved, because default values will be set automatically when the control is reloaded. This keeps the data file from being cluttered with hundreds of default entries.

Saving Binary Data

It's also possible to write and read binary data to the PropertyBag. This becomes useful if you have data that is stored in binary form. For example, you may have a graphics file that is stored in a custom format.

The process of saving binary data differs slightly from that of saving control properties:

  1. Declare a module-level byte array.

  2. In an appropriate event, use the ReDim statement, and store the binary data in the array.

  3. Invoke the PropertyChanged method.

  4. In the WriteProperties event, save the byte array using the WriteProperty method.

These steps are shown in the following simple implementation:

Private mbytMyData() As Byte ' Declare byte array.

Private Sub cmdSavePic_Click()
   ReDim mbytMyData(1 to 5000)
   ' Code to move data into byte array not shown.
   PropertyChanged
End Sub

Private Sub UserDocument_WriteProperties(PropBag As _
   VB.PropertyBag)
   PropBag.WriteProperties "myPic", mbytMyData
End Sub

Persisting Data Using the Open Statement

If you are using a container that does not support the PropertyBag, or if you are saving User Defined Type (UDT) data, use the Open statement to read and write the data to disk. Instructions on using the Open statement can be found in "Using Sequential File Access" in "Processing Drives, Folders, and Files" of the Programmer's Guide.

For More Information To learn the do's and don'ts of adding properties to a components, see "Adding Properties And Methods to Classes," in "General Principles of Component Design."


Ensuring Communication Among ActiveX Documents

Designing ActiveX documents requires a different mindset. Not only are you bereft of QueryUnload and similar events, but you don’t even know when your ActiveX document may be invoked: In an ordinary application, you might have a user navigate through three forms to get to a particular point in your application, but if those forms are ActiveX documents, the user could place the last one in her Favorites list, and come back to it after a week, without going through the intermediate forms! This topic explains how you can control this behavior.

A Little Play: Socrates and Plato

The key to determining whether the user has come to an ActiveX document "cold" (for example, from the Favorites list), or from a known ActiveX document is in setting up a signaling system of some kind. The known ActiveX document must have some way of declaring itself to another ActiveX document in the same suite, and when the second document receives that signal, it can proceed.

One way to set up a signal is to use a global variable. When the global variable is set by a known document, the second document can detect it by using the Is Nothing statement. To illustrate, imagine that the global variable is an envelope that is being passed between two people, Socrates and Plato. Whenever Socrates wants to send a message to Plato, he stuffs the envelope with the message, and leaves it on a pedestal where Plato always passes. When Plato passes the pedestal, he always picks up the envelope, opens it, and checks for a message from Socrates. If there is a message, Plato executes the instructions. Otherwise, he goes on his way.

To implement this metaphor, we need three "actors":

  • Socrates: played by UserDocument1

  • Plato: played by UserDocument2

  • The Envelope: played by global variable named "gEnvelope" in a code module named "Module1."

In order to create a global variable, we must first declare it in a code module (Module1):

Public gEnvelope As UserDocument1

Now imagine that UserDocument1 has a single button on it named "cmdGoPlato." The code for it is:

Private Sub cmdGoPlato_Click()
   Set gEnvelope = Me ' Stuff the envelope.
   Hyperlink.NavigateTo "c:\pedestal\UserDocument2"
End Sub

The stage is now set. When the user clicks the button, the "envelope" is stuffed, and, using the Hyperlink object, sent to UserDocument2.

UserDocument2, meanwhile, has code in the Show event that will check the envelope. It does this by first checking if the global variable is not set to Nothing. If it is not set to Nothing, then the "envelope" is filled, and the code can proceed. Otherwise, it displays a message that requests the user to open UserDocument1 and click the "Go Plato" button.

Private Sub UserDocument_Show()
   If Not gEnvelope Is Nothing Then' proceed normally.
      ' Set properties using the object reference etc.

      ' VERY IMPORTANT! Set the variable to Nothing.
      ' Otherwise the object reference will keep the
      ' object in existence!
      Set gEnvelope = Nothing
   Else
      MsgBox "Sorry, please open UserDocument1 first."
      ' If you're really feeling agressive about this,
      ' you can even hyperlink the user to Socrates:
      Hyperlink.NavigateTo _
         "c:\pedestal\UserDocument1.vbd"
   End If
End Sub

Tracking Your Object References

You may notice that the preceding code sets the object reference to Nothing after the first document hyperlinks to the second. It's very important that you set all such global object references to Nothing. Because as long as the object exists, it continues to use resources and memory. A more complete discussion of this can be found in "Object References and Reference Counting," in "Programming With Objects" of the Programmer's Guide.


Debugging ActiveX Documents in Internet Explorer

When you are creating an ActiveX document, you have the full Visual Basic development environment at your disposal, including all debug tools.

Debugging with Internet Explorer

If you switch back to Visual Basic and attempt to stop the project while your ActiveX document is being displayed in Internet Explorer, Visual Basic will alert you that stopping the project will cause an error in the application hosting the ActiveX document. You should avoid doing this by clicking No, then switch back to Internet Explorer and force it to release the reference to the ActiveX document using one of the following methods:

  • Quit Internet Explorer.

  • Navigate to a different document (this can be either an HTML document or another ActiveX document).

    Note Internet Explorer 3.0 keeps a cache of the four most recently viewed documents, including references to ActiveX documents. The reference to the ActiveX document is released after navigating to the fourth document.

After Internet Explorer releases its reference, you can switch back to Visual Basic, stop the project, and modify it as you wish. When you are ready to view the document again, run the project, switch back to Explorer, and select the address of the project from the drop-down list of addresses.


Building Data Sources

Creating Data Sources

A data source is an object that binds other objects to data from an external source. The foundation for a data source object is a data-aware class module, which is essentially a class module that exposes interfaces to an external source of data. Data-aware classes are covered in depth in "Programming with Objects" in the Programmer's Guide.

Data-aware classes can also be used as the basis for ActiveX components. One common example of a data-aware component is the ADO Data control, which provides a visual interface for binding controls to a database through ADO. Although you could create a data-aware class that does the same thing as the ADO Data control, sharing that class between multiple applications or multiple programmers could prove difficult.

A much better approach would be to create an ActiveX component that duplicates or expands on the functionality of the ADO Data control. As with any ActiveX component, this could take any one of several forms: an ActiveX control, an ActiveX DLL, or an ActiveX EXE. In any case, your ActiveX data source can be easily shared, simplifying access to data regardless of where that data resides: in a local database like Access, in a remote database such as SQL Server, or even in a private OLE DB data store.

The series of step-by-step procedures in this chapter will demonstrate two different approaches to creating data source components. First we'll build an ActiveX data source control, MyDataControl, which emulates the ADO Data control. Next we'll bind the MyDataControl to other controls using both simple and complex binding. Finally, we'll create an ActiveX DLL, MyData, that demonstrates binding to a private data store in an OLE DB simple provider.

The procedures for creating the data source components build on each other. The sequence in which you perform the procedures is therefore important. You should already be familiar with the basics of building ActiveX components before proceeding with this example.

Topics

Creating the MyDataControl Project

Sets up a new project for an ActiveX data source control that duplicates the ADO Data control.
Drawing the MyData Control

Creates the user interface for MyData data source control.
Adding the AXDataSource Project

Creates a test project and a project group for the MyData data source control.
Adding Data Handling Code

Illustrates the data handling procedures required for the MyData data source control.
Running the MyDataControl Project

Demonstrates the functionality of the MyData data source control using both ODBC and OLE DB interfaces.
Creating the MyData Component Project

Sets up a project for an ActiveX DLL data source component.
Creating the MyOSPObject Class

Creates a class that implements the OLE DB Simple Provider interfaces.
Creating the MyDataSource Class

Creates a class that uses the MyOSPObject class to provide data.
Testing the MyData Component

Demonstrates the use of the MyDataComponent ActiveX DLL.
Data Sources Recap

Summarizes the concepts involved in creating ActiveX data source components.

These procedures will be easier to follow if you set up your Visual Basic development environment to show the necessary windows.

Before You Begin

  1. On the View menu, click Toolbox to open the Toolbox.

  2. On the View menu, click Project Explorer to open the Project Explorer window. The Project Explorer window will be used extensively to switch between project files.

  3. If the Project Explorer window is in Folder view, click the Toggle Folders button on the Project Explorer window toolbar to turn the folders off.

  4. On the View menu, click Properties Window to open the Properties window.

  5. On the View menu, click Immediate Window to open the Immediate window. You will need this window open at design time, in order to demonstrate the control's code running at design time.

  6. On the Tools menu, click Options to open the Options dialog box.

    Select the Editor tab, and make sure the Require Variable Declaration box is selected. This makes it much easier to catch typing errors.

    Select the Environment tab. Make sure Prompt To Save Changes is checked, then click OK. This will make it easy to save the changes to the project as you go along.

For More Information See "Creating Data-Aware Classes" in "Programming with Objects" in the Programmer's Guide.

Sample application

Axdata.vbg

Provides a prebuilt example of the data source project discussed in this chapter. In addition, the bitmaps and text file needed for the sample are located in the same directory as the Axdata.vbg sample, which is listed in the Samples directory.


Creating the MyDataControl Project

The MyDataControl control will be compiled into an .ocx file in a later step in this chapter, so it can be used in other projects. Thus the MyDataControl project will be created as an ActiveX control project.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To create the MyDataControl project

  1. On the File menu, click New Project to open the New Project dialog box. (This will close your current project or project group; you will be prompted to save any changes you have made.) Double-click the ActiveX Control icon to create a new project.

    Visual Basic automatically adds a UserControl designer to the project. The default name, UserControl1, appears as the caption of the designer.

  2. On the Project menu, click Project1 Properties to open the Project Properties dialog box. In the General tab, fill out the information shown in the table below, and then click OK.
    Property | Setting
    Project Name | MyDataControl
    Project Description | ActiveX data source control demo.

  3. Double-click UserControl1 in the Project Explorer window to bring the designer to the front.

  4. In the Properties window, double-click the Name property and change the name of the user control to MyData. The new name appears in the title bar and in the Project Explorer window.

  5. In the Properties window, double-click the DataSourceBehavior property to change its value to 1 – vbDataSource. The DataSourceBehavior property allows the control to act as a data source; setting it also adds a GetDataMember event to the control.

  6. On the File menu, click Save Project to save the project files. Name them as shown in the following table. Visual Basic will provide the indicated extensions automatically.
    File | File name | Extension
    User control | MyData | .ctl
    Project | MyDataControl | .vbp

    Binary information in a control — such as bitmaps — will be saved in a binary file with the same name and an extension of .ctx.

For More Information See "Setting Up a New Control Project and Test Project" and "Debugging Controls," in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Drawing the MyData Control
Start from the beginning | Creating Data Sources


Drawing the MyData Control

A data control needs a user interface to allow the user to move back and forth through the data. In this case, we'll duplicate the functionality and appearance of the familiar Data control using four CommandButtons and a Label control, with a PictureBox to act as a container.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To add constituent controls to the MyData control

  1. In the Project Explorer window, double-click MyData to open its designer.

  2. In the Toolbox, double-click the PictureBox control to place a PictureBox control on the MyData designer. Move it near the upper left-hand corner of the designer.

  3. In the Properties window, set the following property values for the PictureBox control:
    Property | Value
    BackColor | &H80000005 (Window Background)
    Width | 4500

  4. With the PictureBox control selected, select the CommandButton control in the Toolbox and draw a CommandButton control on top of Picture1. Repeat the process to add a total of four CommandButton controls (but don't create a control array).

  5. In the Properties window, set the following property values for the CommandButton controls. Where multiple values are shown, values represent settings for Command1 through Command4; otherwise, the value applies to all four CommandButton controls:
    Property | Values
    (Name) | cmdFirst; cmdPrev; cmdNext; cmdLast
    Caption | (Empty)
    Picture | First.bmp; Prev.bmp; Next.bmp; Last.bmp
    Style | 1 - Graphical
    Width | 300

    Note The bitmaps for the Picture property can be found in the same directory as the AXData sample application.

  6. In the Toolbox, select the Label control draw a label on top Picture1. In the Properties window, set the following property values for the Label control:
    Property | Value
    (Name) | lblCaption
    BackStyle | 0 - Transparent
    Caption | MyData

  7. Rearrange the Shape and Label controls to look similar to the example shown below. Don't worry about precise placement because the code in the Resize event will take care of that. Select the MyData control and use the grab handles to resize it slightly larger than the area containing the controls.

  8. Double-click the MyData designer to bring the code window to the front, and add the following code to the UserControl_Resize event procedure:
    Private Sub UserControl_Resize()
   Picture1.Move 0, 0, Width, Height
   cmdFirst.Move 0, 0, cmdFirst.Width, Height - 60
   cmdPrev.Move cmdFirst.Left + cmdFirst.Width, 0, _
      cmdPrev.Width, Height - 60
   cmdLast.Move (Width - cmdLast.Width) - 60, 0, _
      cmdLast.Width, Height - 60
   cmdNext.Move cmdLast.Left - cmdNext.Width, 0, _
      cmdNext.Width, Height - 60

   lblCaption.Height = TextHeight("A")
   lblCaption.Move cmdPrev.Left + _
      cmdPrev.Width, ((Height - 60) _
      / 2) - (lblCaption.Height / 2), _
      cmdNext.Left - (cmdPrev.Left _
      + cmdPrev.Width)
End Sub

    This code will execute when the MyData is initialized or whenever it is resized to rearrange the constituent controls and provide a consistent appearance.

  9. Add a pair of Property Let / Property Get procedures to expose the Caption property of lblCaption:
    Public Property Get Caption() As String
   Caption = lblCaption.Caption
End Property

Public Property Let Caption(ByVal NewCaption As String)
   lblCaption.Caption = NewCaption
   PropertyChanged "Caption"
End Property
  10. In the Object box, select (General). In the Procedure box, select (Declarations) to position yourself at the top of the code module. Add the following code:
    Option Explicit

' Enumerations for the BOFAction Property.
Public Enum BOFActionType
   adDoMoveFirst = 0
   adStayBOF = 1
End Enum

' Enumerations for the EOFAction Property.
Public Enum EOFActionType
   adDoMoveLast = 0
   adStayEOF = 1
   adDoAddNew = 2
End Enum

' Declare object variables for ADO connection
' and Recordset objects.
Private cn As ADODB.Connection
Private WithEvents rs As ADODB.Recordset

' Default Property Values:
Const m_def_RecordSource = ""
Const m_def_BOFAction = BOFActionType.adDoMoveFirst
Const m_def_EOFAction = EOFActionType.adDoMoveLast
Const m_def_ConnectionString = ""

'Property Variables:
Private m_RecordSource As String
Private m_BOFAction As BOFActionType
Private m_EOFAction As EOFActionType
Private m_ConnectionString As String

  11. In the Object box, select UserControl. In the Procedure box, select the InitProperties event. Add the following code to the UserControl_InitProperties event procedure:
    Private Sub UserControl_InitProperties()
   m_RecordSource = m_def_RecordSource
   m_BOFAction = m_def_BOFAction
   m_EOFAction = m_def_EOFAction
   lblCaption.Caption = Ambient.DisplayName
   m_ConnectionString = m_def_ConnectionString
   Set UserControl.Font = Ambient.Font
End Sub
  12. Before we move on, now would be a good time to save your changes. Choose Save Project from the File menu to save your project.

For More Information See "Drawing the ShapeLabel Control" and "Adding an Event to the ShapeLabel Control" in "Creating an ActiveX Control."

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Adding the AXDataSource Project
Start from the beginning | Creating Data Sources


Adding the AXDataSource Project

In order to test the MyData control, you need a test project. To allow debugging of in-process components, Visual Basic allows you to load two or more projects into a project group. In addition to enabling in-process debugging, the project group makes it easier to load your .ocx project and test project.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To add a test project to the project group

  1. On the File menu, click Add Project to open the Add Project dialog box.

    Important Do not click Open Project or New Project, as these will close your control project.

  2. Double-click the Standard EXE icon to add an ordinary .exe project. You can now see both projects in the Project Explorer window, and the caption of the Project Explorer window shows the default project group name.

  3. Right-click Project1 in the Project Explorer and choose Set as Start Up. The Project Explorer window identifies the Startup project by displaying its name in bold type.

  4. Select the designer for the MyData control and press CTRL+F4 to close it.

  5. Select the MyData control in the ToolBox and add it to Form1. If the code in the UserControl_Resize event executes correctly, your form should look like this:

  6. On the File menu, click Save Project Group to save the test project and the project group. Name the files as shown below. Visual Basic will provide the indicated extensions automatically.
    File | File name | Extension
    Form | DataSourceTest | .frm
    Project | DataSourceTest | .vbp
    Project group | AXData | .vbg

For More Information Test projects for ActiveX controls are discussed in more detail in "Debugging Controls" in "Building ActiveX Controls."

Step by Step

This topic is part of a series that walks you through creating a sample ActiveX control.

To | See
Go to the next step | Adding Data Handling Code
Start from the beginning | Creating Data Sources


Adding Data Handling Code

In order to turn the MyData control into a data source, you'll need to add some code to handle connecting to the data and moving through the records. You'll also need to expose a number of properties to allow a developer using the control to select a data source at design time.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To add data handling code to the MyData control

  1. In the Project Explorer window, click MyData to select it, then press F7 or click the Code button on the Project Explorer window toolbar, to open the Code window.

  2. Select References from the Project menu to open the References dialog box. Add a reference to the Microsoft ActiveX Data Objects 2.0 Library.

  3. Add the following Property Let / Property Get procedures to expose design-time properties:
    ' read only
Public Property Get RecordSet() As ADODB.RecordSet
   Set RecordSet = rs
End Property

Public Property Get RecordSource() As String
   RecordSource = m_RecordSource
End Property

Public Property Let RecordSource(ByVal New_RecordSource As String)
   m_RecordSource = New_RecordSource
End Property

Public Property Get BOFAction() As BOFActionType
   BOFAction = m_BOFAction
End Property

Public Property Let BOFAction(ByVal New_BOFAction As BOFActionType)
   m_BOFAction = New_BOFAction
End Property

Public Property Get EOFAction() As EOFActionType
   EOFAction = m_EOFAction
End Property

Public Property Let EOFAction(ByVal New_EOFAction As EOFActionType)
   m_EOFAction = New_EOFAction
End Property

Public Property Get ConnectionString() As String
   ConnectionString = m_ConnectionString
End Property

Public Property Let ConnectionString(ByVal New_ConnectionString _
      As String)
   m_ConnectionString = New_ConnectionString
End Property
  4. In the Object box, select cmdFirst. In the Procedure box, select the Click event. Add the following code to the cmdFirst_Click event procedure to move to the first record in a recordset:
    Private Sub cmdFirst_Click()
   If rs Is Nothing Then Exit Sub
   rs.MoveFirst
End Sub
  5. In the Object box, select cmdLast. In the Procedure box, select the Click event. Add the following code to the cmdLast_Click event procedure to move to the last record in a recordset:
    Private Sub cmdLast_Click()
   If rs Is Nothing Then Exit Sub
   rs.MoveLast
End Sub
  6. In the Object box, select cmdPrev. In the Procedure box, select the Click event. Add the following code to the cmdPrev_Click event procedure to move backwards through a recordset:
    Private Sub cmdPrev_Click()
   If rs Is Nothing Then Exit Sub

   If rs.BOF Then
      Select Case m_BOFAction
         Case BOFActionType.adDoMoveFirst
            rs.MoveFirst
         Case BOFActionType.adStayBOF
            Exit Sub
         Case Else
            Exit Sub
      End Select
   Else
      rs.MovePrevious
   End If
End Sub
  7. In the Object box, select cmdNext. In the Procedure box, select the Click event. Add the following code to the cmdNext_Click event procedure to move forward through a recordset:
    Private Sub cmdNext_Click()
   If rs Is Nothing Then Exit Sub

   If rs.EOF Then
      Select Case m_EOFAction
         Case EOFActionType.adDoAddNew
            rs.AddNew
         Case EOFActionType.adDoMoveLast
            rs.MoveLast
         Case EOFActionType.adStayEOF
            Exit Sub
         Case Else
            Exit Sub
      End Select
   Else
      rs.MoveNext
   End If
End Sub
  8. In the Object box, select UserControl. In the Procedure box, select the Terminate event. Add the following code to the UserControl_Terminate event procedure to ensure that the objects created by the control are shut down properly:
    Private Sub UserControl_Terminate()
   On Error Resume Next

   If Not rs Is Nothing Then
      rs.Close
      Set rs = Nothing
   End If

   If Not cn Is Nothing Then
      cn.Close
      Set cn = Nothing
   End If

   Err.Clear
End Sub
  9. In the Object box, select UserControl. In the Procedure box, select the WriteProperties event. Add the following code to the UserControl_WriteProperties event procedure to save the property values that are entered at design time:
    Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
   'Write property values to storage
   Call PropBag.WriteProperty("Caption", _
      lblCaption.Caption, Ambient.DisplayName)
   Call PropBag.WriteProperty("RecordSource", _
      m_RecordSource, m_def_RecordSource)
   Call PropBag.WriteProperty("BOFAction", _
      m_BOFAction, m_def_BOFAction)
   Call PropBag.WriteProperty("EOFAction", _
      m_EOFAction, m_def_EOFAction)
   Call PropBag.WriteProperty("ConnectionString", _
      m_ConnectionString, m_def_ConnectionString)
End Sub
  10. In the Object box, select UserControl. In the Procedure box, select the ReadProperties event. Add the following code to the UserControl_ReadProperties event procedure to save the property values that are entered at design time:
    Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
   'Load property values from storage
   lblCaption.Caption = PropBag.ReadProperty("Caption", _
      Ambient.DisplayName)
   m_RecordSource = PropBag.ReadProperty("RecordSource", _
      m_def_RecordSource)
   m_BOFAction = PropBag.ReadProperty("BOFAction", m_def_BOFAction)
   m_EOFAction = PropBag.ReadProperty("EOFAction", m_def_EOFAction)
   m_ConnectionString = PropBag.ReadProperty("ConnectionString", _
      m_def_ConnectionString)
End Sub
  11. In the Object box, select UserControl. In the Procedure box, select the GetDataMember event. Add the following code to the UserControl_GetDataMember event procedure to connect to the data specified in the ConnectionString and RecordSource properties:
    Private Sub UserControl_GetDataMember(DataMember As String, _
      Data As Object)
   Dim conn As String

   On Error GoTo GetDataMemberError

   If rs Is Nothing Or cn Is Nothing Then
      ' make sure various properties have been set
      If Trim$(m_ConnectionString) = "" Then
         MsgBox "No ConnectionString Specified!", _
               vbInformation, Ambient.DisplayName
         Exit Sub
      End If

      If Trim$(m_RecordSource) = "" Then
         MsgBox "No RecordSource Specified!", _
            vbInformation, Ambient.DisplayName
         Exit Sub
      End If

      If Trim$(m_ConnectionString) <> "" Then
         ' Create a Connection object and establish
         ' a connection.
         Set cn = New ADODB.Connection
         cn.ConnectionString = m_ConnectionString
         cn.Open

         ' Create a RecordSet object.
         Set rs = New ADODB.RecordSet
         rs.Open m_RecordSource, cn, adOpenKeyset, adLockPessimistic
         rs.MoveFirst
      Else
         Set cn = Nothing
         Set rs = Nothing
      End If
   End If

   Set Data = rs

   Exit Sub

GetDataMemberError:

   MsgBox "Error: " & CStr(Err.Number) & vbCrLf & vbCrLf & _
      Err.Description, vbOKOnly, Ambient.DisplayName
   Exit Sub
End Sub
  12. Save your work before going on to the next step.

In the next step we'll run our project to see the results.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Running the MyDataControl Project
Start from the beginning | Creating Data Sources


Running the MyDataControl Project

In the previous topic, we added the code that allows the MyData control to provide data to other objects. In this step we'll connect the MyData control to an ADO source and bind to it using both simple and complex binding.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To test the MyData control

  1. If it's still open, close the MyData designer and open the designer for Form1.

  2. Select the MyData control and switch to the Properties window.

  3. Double-click the ConnectionString property and enter the DSN connection string for the Northwind database. It should look something like this: DSN = Northwind.

    Important If you haven't previously created a DSN for the Northwind database, you'll need to create one first.The procedure for creating a DSN is discussed in "Connection Objects" in "About the Data Environment Designer" in the Data Access Guide.

  4. Double-click the RecordSource property and enter Customers. This specifies the Customers table in the Northwind database; you could also enter a SQL statement here in order to filter the data.

  5. Select a TextBox control in the Toolbox and add it to Form1, then switch to the Properties window.

  6. Select the DataSource property and choose MyData1 from the drop-down list.

  7. Select the DataField property. The drop-down list should contain a list of the fields in the Customers table. Select the CompanyName field.

  8. Select Start from the Run menu to run the project.

    As you click the buttons on the MyData control, names from the CompanyName field should appear in the TextBox. Try editing a name in the TextBox, then move forward and back to the same record. You'll notice that your change has been saved to the database. The TextBox is simple-bound to the Northwind database by way of the MyData control.

  9. Stop the project (by selecting End from the Run menu), then select the form.

  10. Choose Components from the Project menu to open the Components dialog. Check the Microsoft Data Grid 6.0 to add it to your project, then click OK.

  11. Select the DataGrid in the Toolbox and add it to Form1, then switch to the Properties window.

  12. Select the DataSource property and set it to MyData1.

  13. Select Start from the Run menu to run the project.

    The grid should contain all of the data from the Customers table. As you click the buttons on the MyData control, the selected row in the grid changes to match the data in the TextBox, scrolling as necessary. As with the TextBox, you can edit data in the grid and the changes are saved in the database. The DataGrid is complex-bound to the Northwind database by way of the MyData control.

  14. Stop the project (by selecting End from the Run menu), then select the MyData control on Form1.

  15. In the Properties window, double-click the ConnectionString property and enter an OLE DB connection string, such as Provider=Microsoft.Jet.OLE DB.3.51;Data Source=c:\northwind.mdb.

    Important If you haven't previously created a OLE DB connection for the Northwind database, you'll need to create one first.The procedure for creating a DSN is discussed in "Creating the Northwind OLEDB Data Source" in "Using Visual Basic's Standard Controls" in the Programmer's Guide.

  16. Select Start from the Run menu to run the project.

    You'll see that the data looks and acts exactly like it did before. The data is now being accessed through an OLE DB interface rather than a ODBC interface, and all you had to do was change the connection string!

As long as you have the project open, take some time to play around with other property settings such as BOFAction and EOFAction to see their behavior.

In the next few steps, we'll build a ActiveX DLL and connect our form to an OLE DB Simple Provider.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Creating the MyData Component Project
Start from the beginning | Creating Data Sources


Creating the MyData Component Project

The MyData component is an ActiveX DLL that acts as a data source. In this example, we'll use it to provide data through an OLE DB Simple Provider — it could just as easily be used as an ODBC or OLE DB data source. First we'll need to add another project to the AXData project group.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To add a test project to the project group

  1. On the File menu, click Add Project to open the Add Project dialog box.

    Important Do not click Open Project or New Project, as these will close your control project.

  2. Double-click the ActiveX DLL icon to add a DLL component project. This will add a new project, Project 2, and a new class module, Class1, to the Project Explorer window.

  3. Select Project2 in the Project Explorer, then double-click the Name property in the Properties window and change the Name to MyDataComponent.

  4. Choose MyDataComponent Properties from the Project menu to display the Project Properties dialog. Set the properties for the MyDataComponent project as follows:
    Property | Setting
    Project Description | Sample OLE DB Simple Provider Component

  5. Select References from the Project menu. Add the following references in the References dialog box:
    Reference | Purpose
    Microsoft Data Source Interfaces | Allows the component to act as a data source.
    Microsoft OLE DB Simple Provider 1.5 Library | Used to access data sources through the OLE DB Simple Provider interface.
    OLE DB Errors Type Library | Provides error messages.

  6. Select Save Project Group from the File menu to save your changes before moving on to the next step.

In the next step, we'll create a class that implements the OLE DB Simple Provider interface.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Creating the MyOSPObject Class
Start from the beginning | Creating Data Sources


Creating the MyOSPObject Class

In the previous topic, we added an ActiveX DLL project to the AXData sample. In this step, we'll create a class that implements the OLE DB Simple Provider (OSP) interfaces to access data stored in a text file.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To create the MyOSPObject class

  1. In the Project Explorer, select Class1 from the MyDataComponent project. In the Properties window, set the properties for Class1 as follows:
    Property | Setting
    (Name) | MyOSPObject

    You may have noticed that the DataSourceBehavior is set to none. If this component is to act as a data source, shouldn't the DataSourceBehavior be set to another value? Don't worry — we'll add another class in a later step that provides the data source capability for the component.

  2. Double-click MyOSPObject in the Project Explorer to open its code window.

  3. In the Object box, select (General). In the Procedure box, select (Declarations) to position yourself at the top of the code module. Add the following code:
    Option Explicit
Implements OLEDBSimpleProvider
Dim MyOSPArray()
Dim RowCount As Integer
Dim ColCount As Integer
Dim colListeners As New Collection
Dim ospl As OLEDBSimpleProviderListener
Public FilePath As String

    Note the use of the Implements keyword for OLEDBSimpleProvider. Remember, Implements is like a contract — it means that you'll need to implement all of the interfaces of the OLEDBSimpleProvider class.

  4. Add the following procedures to read and write data from a file:
    Public Sub LoadData()
   ' This procedure loads data from a semi-colon
   ' delimited file into an array.
   Dim GetLine As Variant
   Dim Spot As Integer, Position As Integer
   Dim Row As Integer, Col As Integer

   On Error GoTo ErrorTrap
   Open FilePath For Input Lock Read Write As #1
   Position = 1
   Row = 0
   Line Input #1, GetLine
   Spot = InStr(1, GetLine, ";")
   RowCount = val(Left$(GetLine, Spot))
   ColCount = val(Right$(GetLine, Len(GetLine) - Spot))
   ReDim MyOSPArray(RowCount + 1, ColCount + 1)
   While Not EOF(1)
      Line Input #1, GetLine
      Col = 1
      Spot = InStr(1, GetLine, ";")
      While Spot <> 0
         MyOSPArray(Row, Col) = Left$(GetLine, Spot - 1)
         Col = Col + 1
         GetLine = Right$(GetLine, Len(GetLine) - Spot)
         Spot = InStr(1, GetLine, ";")
      Wend
      If Len(GetLine) <> 0 Then
         MyOSPArray(Row, Col) = GetLine
      End If
      Row = Row + 1
   Wend
   Close #1
   Exit Sub

ErrorTrap:
   Err.Raise (E_FAIL)
End Sub

Public Sub SaveData()
   ' This procedure writes data from an array to a semi-colon
   ' delimited file
   Dim PutLine As Variant
   Dim iRow As Integer, iCol As Integer

   On Error GoTo ErrorTrap
   Open FilePath For Output Lock Read Write As #1
   Print #1, RowCount & ";" & ColCount

   For iRow = 0 To RowCount
      For iCol = 1 To ColCount
         PutLine = PutLine & MyOSPArray(iRow, iCol) & ";"
      Next iCol
      Print #1, PutLine
      PutLine = ""
   Next iRow
   Close #1
   Exit Sub

ErrorTrap:
   Err.Raise (E_FAIL)
End Sub
  5. In the Object box, select Class. In the Procedure box, select the Terminate event. Add the following code to the Class_Terminate event procedure to save the data when the class is terminated:
    Private Sub Class_Terminate()
   On Error Resume Next
   ' Call the SaveData method
   SaveData
End Sub

To implement OLEDBSimpleProvider

Since the MyOSPObject class implements the OLEDBSimpleProvider class, we have to implement all of its interfaces, even if we aren't going to use them:

  1. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the addOLEDBSimpleProviderListener function. Add the following code to the function procedure to add listeners that will notify the class when data changes:
    Private Sub OLEDBSimpleProvider_addOLEDBSimpleProviderListener _
   (ByVal pospIListener As OLEDBSimpleProviderListener)
   ' Add a listener to the Listeners collection.
   If Not (pospIListener Is Nothing) Then
      Set ospl = pospIListener
      colListeners.Add ospl
   End If
End Sub
  2. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the deleteRows function. Add the following code to the procedure to delete a row of data from a file:
    Private Function OLEDBSimpleProvider_deleteRows _
   (ByVal iRow As Long, ByVal cRows As Long) As Long
   Dim TempArray()
   Dim listener As OLEDBSimpleProviderListener
   Dim v As Variant

   ' Make sure iRow is in the correct range:
   If iRow < 1 Or iRow > RowCount Then
      Err.Raise (E_FAIL)
   End If

   ' Set cRows to the actual number which can be deleted
   If iRow + cRows > RowCount + 1 Then
      cRows = RowCount - iRow + 1
   End If

   ' Establish a Temporary Array
   cNewRows = RowCount - cRows
   ReDim TempArray(cNewRows + 1, ColCount + 1)

   ' Notify each listener:
   For Each v In colListeners
      Set listener = v
      listener.aboutToDeleteRows iRow, cRows
   Next

   ' Copy over the first rows which are not being deleted
   For Row = 0 To iRow - 1
      For Col = 0 To ColCount
         TempArray(Row, Col) = MyOSPArray(Row, Col)
      Next Col
   Next Row

   ' Copy the last rows which are not being deleted
   For Row = iRow + cRows To RowCount
      For Col = 0 To ColCount
         TempArray(Row - cRows, Col) = MyOSPArray(Row, Col)
      Next Col
   Next Row

   ' Re-allocate the array to copy into it
   ReDim MyOSPArray(cNewRows + 1, ColCount + 1)

   ' Set the real row count back in
   RowCount = cNewRows

   ' Copy over the rows
   For Row = 0 To cNewRows
      For Col = 0 To ColCount
         MyOSPArray(Row, Col) = TempArray(Row, Col)
      Next Col
   Next Row

   ' Clear the temporary array
   ReDim TempArray(0)

   ' Notify each listener
   For Each v In colListeners
      Set listener = v
      listener.deletedRows iRow, cRows
   Next

   ' Return number of deleted rows
   OLEDBSimpleProvider_deleteRows = cRows
End Function
  3. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the find function. Add the following code to the procedure to find data within a file:
    Private Function OLEDBSimpleProvider_find(ByVal iRowStart As Long, _
   ByVal iColumn As Long, ByVal val As Variant, _
   ByVal findFlags As OSPFIND, ByVal compType As OSPCOMP) As Long

   Dim RowStart As Integer, RowStop As Integer
   If (findFlags And (OSPFIND_UP Or OSPFIND_UPCASESENSITIVE)) _
      <> 0 Then
      RowStart = RowCount + 1
      RowStop = 0
      StepValue = -1
   Else
      RowStart = 0
      RowStop = RowCount + 1
      StepValue = 1
   End If

   If (findFlags And (OSPFIND_CASESENSITIVE Or _
      OSPFIND_UPCASESENSITIVE)) <> 0 Then
      CaseSens = 1   'Use a Text Compare not Case Sensitive
   Else
      CaseSens = 0   'Not Case Sensitive use Binary Compare
   End If

   If VarType(val) = vbString Then
      StringComp = True
   Else
      StringComp = False
   End If

   iAnswerRow = -1
   For iRow = RowStart To RowStop Step StepValue
      If StringComp Then
         CompResult = StrComp(MyOSPArray(iRow, iColumn), _
            val, CaseSens)
         Select Case (compType)
            Case OSPCOMP_DEFAULT, OSPCOMP_EQ:
               If CompResult = 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_GE
               If CompResult >= 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_GT
               If CompResult > 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_LE
               If CompResult <= 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_LT
               If CompResult < 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_NE
               If CompResult <> 0 Then
                  iAnswerRow = iRow
                  Exit For
               End If
         End Select
      Else
         Select Case (compType)
            Case OSPCOMP_DEFAULT, OSPCOMP_EQ:
               If MyOSPArray(iRow, iColumn) = val Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_GE
               If MyOSPArray(iRow, iColumn) >= val Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_GT
               If MyOSPArray(iRow, iColumn) > val Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_LE
               If MyOSPArray(iRow, iColumn) <= val Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_LT
               If MyOSPArray(iRow, iColumn) < val Then
                  iAnswerRow = iRow
                  Exit For
               End If
            Case OSPCOMP_NE
               If MyOSPArray(iRow, iColumn) <> val Then
                  iAnswerRow = iRow
                  Exit For
               End If
         End Select
      End If
   Next iRow
   OLEDBSimpleProvider_find = iAnswerRow
End Function
  4. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getColumnCount function. Add the following code to the procedure to return the number of fields within a file:
    Private Function OLEDBSimpleProvider_getColumnCount() As Long
   OLEDBSimpleProvider_getColumnCount = ColCount
End Function
  5. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getEstimatedRows function. Add the following code to the procedure to return the estimated number of rows of data within a file:
    Private Function OLEDBSimpleProvider_getEstimatedRows() As Long
   OLEDBSimpleProvider_getEstimatedRows = RowCount
End Function
  6. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getLocale function. Add the following code to the procedure:
    Private Function OLEDBSimpleProvider_getLocale() As String
   OLEDBSimpleProvider_getLocale = ""
End Function

    Note that in this case the function simply returns a null value. Even though it doesn't do anything, the function has to be added — since this class implements OLEDBSimpleProvider, all of its interfaces have to be included.

  7. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getRowCount function. Add the following code to the procedure to return the number of rows of data within a file:
    Private Function OLEDBSimpleProvider_getRowCount() As Long
   OLEDBSimpleProvider_getEstimatedRows = RowCount
End Function
  8. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getRWStatus function. Add the following code to the procedure to set the Read/Write status by column — in this case, the first column will be read-only while the remaining columns will be read-write:
    Private Function OLEDBSimpleProvider_getRWStatus _
   (ByVal iRow As Long, ByVal iColumn As Long) As OSPRW
   If iColumn = 1 Then
      ' Make the first column read-only
      OLEDBSimpleProvider_getRWStatus = OSPRW_READONLY
   Else
      ' Make the column read-write
      OLEDBSimpleProvider_getRWStatus = OSPRW_READWRITE
   End If
End Function
  9. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the getVariant function. Add the following code to the procedure to return data stored in a specific row and column:
    Private Function OLEDBSimpleProvider_getVariant _
   (ByVal iRow As Long, ByVal iColumn As Long, _
   ByVal format As OSPFORMAT) As Variant
   OLEDBSimpleProvider_getVariant = MyOSPArray(iRow, iColumn)
End Function

    The getVariant function also accepts a format argument which can be used to determine the formatting of the data returned.

  10. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the insertRows function. Add the following code to the procedure to insert a new row of data into a file:
    Private Function OLEDBSimpleProvider_insertRows _
   (ByVal iRow As Long, ByVal cRows As Long) As Long
   Dim TempArray()
   Dim listener As OLEDBSimpleProviderListener
   Dim v As Variant

   ' Establish a temporary array
   cNewRows = RowCount + cRows
   ReDim TempArray(cNewRows + 1, ColCount + 1)

   ' If inserting past the end of the array, insert at
   ' the end of the array
   If iRow > RowCount Then
      iRow = RowCount + 1
   End If

   ' Notify listener
   For Each v In colListeners
      Set listener = v
      listener.aboutToInsertRows iRow, cRows
   Next

   ' Copy over the existing rows
   For Row = 0 To iRow
      For Col = 0 To ColCount
         TempArray(Row, Col) = MyOSPArray(Row, Col)
      Next Col
   Next Row

   ' Copy the last rows which follow the inserted rows
   For Row = iRow + 1 + cRows To cNewRows
      For Col = 0 To ColCount
         TempArray(Row, Col) = MyOSPArray(Row - cRows, Col)
      Next Col
   Next Row

   ' Re-allocate the array to copy into it
   ReDim MyOSPArray(cNewRows + 1, ColCount + 1)

   ' Copy over the rows
   For Row = 0 To cNewRows
      For Col = 0 To ColCount
         MyOSPArray(Row, Col) = TempArray(Row, Col)
      Next Col
   Next Row

   ' Clear the temporary array
   ReDim TempArray(0)

   ' Set the real row count back in
   RowCount = cNewRows

   ' Notify listeners
   For Each v In colListeners
      Set listener = v
      listener.insertedRows iRow, cRows
   Next

   ' Return number of inserted rows
   OLEDBSimpleProvider_insertRows = cRows
End Function
  11. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the isAsynch function. Add the following code to the procedure to determine if the OSP can return data asynchronously:
    Private Function OLEDBSimpleProvider_isAsync() As Long
   OLEDBSimpleProvider_isAsync = False
End Function
  12. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the removeOLEDBSimpleProviderListener function. Add the following code to the procedure to remove a listener:
    Private Sub OLEDBSimpleProvider_removeOLEDBSimpleProviderListener _
   (ByVal pospIListener As OLEDBSimpleProviderListener)
   ' Remove the listener
   For i = 1 To colListeners.Count
      If colListeners(i) Is pospIListener Then
         colListeners.Remove i
      End If
   Next
End Sub
  13. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the setVariant function. Add the following code to the procedure to retrieve data from a particular row and column and to designate a listener to provide notification that data has changed:
    Private Sub OLEDBSimpleProvider_setVariant(ByVal iRow As Long, _
   ByVal iColumn As Long, ByVal format As OSPFORMAT, _
   ByVal Var As Variant)
   Dim listener As OLEDBSimpleProviderListener
   Dim v As Variant

   For Each v In colListeners
      Set listener = v
      listener.aboutToChangeCell iRow, iColumn   ' Pre-notification
   Next

   MyOSPArray(iRow, iColumn) = Var

   For Each v In colListeners
      Set listener = v
      listener.cellChanged iRow, iColumn         ' Post-notification
   Next
End Sub
  14. In the Object box, select OLEDBSimpleProvider. In the Procedure box, select the stopTransfer function. Add the following code to the procedure:
    Private Sub OLEDBSimpleProvider_stopTransfer()
   ' Do nothing because we are already populated
End Sub

    Note that there is no code in this procedure, but the procedure must be included because this class implements OLEDBSimpleProvider. You could add code here that would allow you to cancel loading during a long transfer.

  15. Choose Save Project Group from the File menu to save your changes. When prompted for a file name for the Class module, choose the default (MyOSPObject.cls). When prompted for a file name for the Project, choose the default (MyDataComponent.vbp).

Whew! If that seemed like a lot of code, there's a good reason for it — the MyOSPObject class provides much of the functionality that you might find in a database. With OSP, you can use almost any file as you might have used a database in the past.

In the next step, we'll create another class that acts as the data source to the MyOSPObject class.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Creating the MyDataSource Class
Start from the beginning | Creating Data Sources


Creating the MyDataSource Class

In the previous topic, we created a class that implements an OLE DB Simple Provider. In this step we'll create another class that will provide data from the MyOSPObject class to other objects.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To create the MyDataSource class

  1. Select the MyDataComponent project in the Project Explorer.

  2. Select Add Class Module from the Project menu, then choose Class Module from the Add Class Module dialog box.

  3. Select the Properties window and set the following properties for the new class module:
    Property | Setting
    (Name) | MyDataSource
    DataSourceBehavior | 2-vbOLEDBProvider

  4. In the Object box, select Class. In the Procedure box, select the GetDataMember event. Add the following code to the Class_GetDataMember event procedure:
    Private Sub Class_GetDataMember(DataMember As String, Data As Object)
   ' Declare an instance of the MyOSPObject class
   Dim MyOSP As New MyOSPObject

   ' Make sure the DataMember is valid
   If DataMember = "" Then
       Err.Raise (E_FAIL)
   End If

   ' Set the FilePath property
   MyOSP.FilePath = DataMember

   ' Call the LoadData method to populate the class
   MyOSP.LoadData

   ' Set MyDataSource's data to MyOSPObject's data
   Set Data = MyOSP
End Sub
  5. Choose Save Project Group from the File menu to save your changes. When prompted for a file name for the Class module, choose the default (MyDataSource.cls).

As you may have noticed, this class is much simpler than the MyOSPObject class. In fact, if you go back and compare it with the GetDataMember event, you'll see that it's doing essentially the same thing. The main difference here is that we're using a class that we created rather than the pre-existing ADODB class.

In the next step, we'll convert the form that we created earlier to use our new data source object, and we'll test it to see the results.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Testing the MyData Component
Start from the beginning | Creating Data Sources


Testing the MyData Component

In the previous topics, we created an ActiveX DLL containing two classes, MyOSPObject and MyDataSource. In this topic we'll use the MyDataComponent object as a data source for the form that we created in an earlier topic.

Note This topic is part of a series that walks you through creating sample data source components. It begins with the topic Creating Data Sources.

To test the MyData control

  1. Select Project 1 (DataSourceTest.vbp) in the Project Explorer.

  2. Select References from the Project menu to open the References dialog. Add a reference to MyDataComponent.

  3. Add a reference to the Microsoft Data Adapter Library. The Data Adapter object acts as an intermediate layer between OLE DB and OSP.

  4. Double-click Form1 in the Project Explorer to open its designer.

  5. Select DataGrid in the ToolBox and add a second DataGrid to Form1. You don't need to set any properties for DataGrid2.

  6. Double-click Form1 to open its code window. In the Object box, select (General). In the Procedure box, select (Declarations) to position yourself at the top of the code module. Add the following code:
    Option Explicit
Dim da As New DataAdapter
Dim ds As New MyDataSource
  7. In the Object box, select Form. In the Procedure box, select the Load event. Add the following code to the Form_Load event procedure to initialize the MyDataComponent object and assign it to the controls:
    Private Sub Form_Load()
   ' Set the Object property of the Data Adapter
   ' to the MyDataSource object
   Set da.Object = ds

   ' Set the DataMember property
   DataGrid2.DataMember = App.Path & "\Customer.txt"
   ' Set the DataSource to the DataAdapter
   Set DataGrid2.DataSource = da
End Sub

    Note The above code assumes that the Customer.txt file is located in the same directory as your application. If you have the Customer.txt file in a different location, change the path accordingly. A copy of the Customer.txt file is included with the AXData sample application.

  8. Select Start from the Run menu to run the project.

    Notice that the second DataGrid contains the same data as the first; like the first, you can edit data in the second grid and the changes will be saved. The big difference between the two is that the first DataGrid is bound to a database, but the second DataGrid isn't — it's bound to a text file through the OLE DB Simple Provider interface.

That does it for the data sources example. Of course, you could easily expand on this. For example, you might add methods to the MyOSPObject class to navigate through the records, or perhaps methods that validate content or formatting for a particular field. You can take it as far as you want.

Step by Step

This topic is part of a series that walks you through creating sample ActiveX data sources.

To | See
Go to the next step | Data Sources Recap
Start from the beginning | Creating Data Sources


Data Sources Recap

The preceding topics demonstrated how easy it is to create data source components — first as an ActiveX control, then as an ActiveX DLL. Those aren't the only options either. You could just as easily create an ActiveX EXE data source, or simply use a data-aware class as a data source. So how do you decide? There aren't any hard and fast rules, but here are a few suggestions:

  • Use a data-aware class when you don't anticipate that you'll need to reuse a data source — for example, to access data that keeps track of scores for a game.

  • Use an ActiveX DLL when you need a data source that doesn't require a visual interface and that runs in-process. An example of this might be a component that supplies stock quotes to be displayed in another control or in a spreadsheet. Such a component can easily be reused in different applications.

  • Use an ActiveX EXE for a data source that needs to run out-of-process, such as a middle-tier component that enforces business rules against a back-end database. An ActiveX EXE can also be reused in different applications.

  • Use an Active X control when you need a visual interface for your data source. The obvious example for this is the Data control; however, you might also want to combine the display interface as a part of your data source by using a grid, chart, or group of text boxes. Of course, ActiveX controls can also easily be shared and distributed.

In addition to choosing the type of data source, you also have a lot of flexibility when it comes to choosing a data interface. As demonstrated in the preceding topics, you can easily switch between OLE DB and ODBC data source using just about any database as a back-end.

Perhaps the most intriguing option is the OLE DB Simple Provider. The example demonstrated the use of a simple text file as a data source, but that's just the tip of the iceberg. With OSP, you have complete control over how and where the data is stored. You can create your own custom file formats, storing data in a binary format that only your component can read. You could even use an OSP component to store and retrieve objects.

With all the possibilities, one thing is certain — data source components elevate data binding to a whole new level.

Step by Step

This topic is the final topic in a series that walks you through creating sample ActiveX data sources.

To | See
Start from the beginning | Creating Data Sources


ActiveX Component Standards and Guidelines

Your component will be easier to use if your object model is similar in style to those of other components, and if the methods and properties of your objects have the same names as methods and properties that provide the same functionality for objects provided by other components.

This appendix includes the following topics relating to these issues.

Topics

Object Naming Guidelines

How to select names for constants, for the classes your objects are created from, for the members — properties, methods, and events — of your classes, and for the named arguments of those members.
ActiveX Component Shutdown

Describes what you have to do to create well-behaved components, and what Visual Basic does to help you. Explains Visual Basic’s shutdown rules for components.
Object Model Creation Guidelines

Offers guidelines for creating robust collection classes and root objects for your component’s object model, and provides references to additional information.
Creating Interfaces for Use With the Implements Statement

Rules and guidelines for creating components that are compatible with Visual Basic's Implements statement.

For More Information "Programming with Objects" and "Programming ActiveX Components," in the Visual Basic Programmer’s Guide, contain information you may find helpful in understanding these issues.


Object Naming Guidelines

When selecting names for objects, properties, methods, and events, choose names that can be easily understood by the users of your component. These elements comprise the programming interface of your component — the more clear you make their names, the more usable your code will be.

The rules in this topic apply to names for:

  • Objects.

  • The properties, methods, and events that comprise the interfaces of your objects.

  • Named arguments of properties, methods, and events.

Use Entire Words or Syllables Whenever Possible

It is easier for users to remember complete words than to remember whether you abbreviated Window as Wind, Wn, or Wnd. The following table lists two examples of recommended naming conventions.

Use | Don’t use
Application | App
SpellCheck | SpChk

When you need to abbreviate because an identifier would be too long, try to use complete initial syllables. For example, use AltExpEval instead of either AlternateExpressionEvaluation or AltExpnEvln.

Use Mixed Case

All identifiers should use mixed case, rather than underscores, to separate the words in the identifier. The following table lists two examples of recommended naming conventions.

Use | Don’t use
ShortcutMenus | Shortcut_Menus, Shortcutmenus, SHORTCUTMENUS, SHORTCUT_MENUS
BasedOn | basedOn

Use Consistent Terminology

Use the same word you use in the interface; don’t use identifier names like HWND, which are based on Hungarian notation. Remember that this code will be accessed by other users, so try to use the same word your users would use to describe a concept.

Use the Correct Plural for Collection Class Names

Using plurals rather than inventing new names for collections reduces the number of items a user must remember. It also simplifies the selection of names for collections. The following table lists some examples of collection class names.

Use | Don’t use
Axes | Axiss
SeriesCollection | CollectionSeries
Windows | ColWindow

For example, if you have a class named Axis, a collection of Axis objects is stored in an Axes class. Similarly, a collection of Vertex objects is stored in a Vertices class. In rare cases where the same spelling is used for both singular and plural, append the word "Collection" — for example, SeriesCollection.

Note This naming convention may not be appropriate for some collections, especially where a set of objects exists independently of the collection. For example, a Mail program might have a Name object that exists in multiple collections: ToList, CcList, and so forth. In this case, you might specify the individual name collections as ToNames and CcNames.

Use a Prefix for Your Constants

Select a three- or four-letter, lowercase prefix that identifies your component, and use it on the names of constants your component provides in its type library, as well as on the names of the Enums that define those constants.

For example, a code component that provides loan evaluations might use ‘levs’ as its prefix. The following Enum for loan types uses this prefix. (In addition, the constants include the upper-case characters ‘LT’ to indicate the enumeration they belong to.)

Public Enum LoanType
   levsLTMortgage = 1
   levsLTCommercial
   levsLTConsumer
End Enum

Using a prefix reduces the chance that the constants for your component will have name conflicts with constants for other components. Name conflicts of this type can cause difficult bugs for your users.

The shorter the constant name, the more important this rule becomes. In the worst case — constant names that are common words, like the names of colors — such conflicts become almost inevitable.

Verb/Object vs. Object/Verb

If you create method names that combine a verb with the name of the object it acts on, you should be consistent about the order. Either place the verb before the object in all cases, as with InsertWidget and InsertSprocket, or always place the object first, as with WidgetInsert and SprocketInsert.

Both schemes have their advantages. Verb/object order creates names that are more like normal speech, and thus show the intent of the method better. Object/verb order groups together all the methods that affect a particular object.

It doesn’t matter which order you choose, but mixing the two orders will confuse the users of your component.


ActiveX Component Shutdown

To create a well-behaved component, you have to give up control over your component’s lifetime. Your component is started automatically when a client requests an object. It should shut down when all clients have released the objects they were using — and not a moment sooner.

The reason for this is that the objects your component provides become integral parts of the client applications that use them. A client that uses one of your objects must be able to count on the object existing until the client is done with it.

By the same token, if clients have released all references to the objects your component provides, your component should shut down and release the memory and resources it was using.

Therefore, in designing a well-behaved component the two most important guidelines are:

  • Don’t force your component to shut down while client applications have references to your objects.

  • Don’t force your component to remain in memory when all object references have been released.

Visual Basic provides a great deal of assistance in following these basic guidelines. This topic explains how you can do your part.

Don’t Force Your Component to Shut Down

This means that you should never use the End statement to shut down your component, as long as client applications are holding references to your objects.

This guideline is easy to follow for in-process components, because Visual Basic prevents the use of the End statement in ActiveX DLL and ActiveX control projects. If you include the End statement in your code, an error will occur when you make the compiled component.

Note Visual Basic doesn’t allow the End statement in an in-process component because executing it would also terminate the client.

Out-of-Process Components

If you shut down an out-of-process component with the End statement, client applications are left holding references to objects that no longer exist. When they attempt to invoke the properties and methods of those objects, they will receive errors.

In addition, your component may not shut down correctly, because your objects never receive their Terminate events. This is because the End statement halts execution of your component abruptly, releasing objects and freeing memory. No further Visual Basic code is executed, including code you have placed in the QueryUnload and Unload events of forms, and in the Terminate events of forms and classes.

If you observe the four rules Visual Basic uses to determine when to shut your component down, as described in "Visual Basic Component Shutdown Rules," you should never need to force your component to shut down.

Don’t Force Your Component to Remain Loaded

Visual Basic will automatically unload your component according to the shutdown rules enumerated In "Visual Basic Component Shutdown Rules." As you’ll see from the discussion of those rules, it’s possible to keep your component loaded after clients have released their references to its objects.

Generally speaking, this is a bad idea. Your component goes on taking up memory, even when it’s not supplying objects. In fact, an out-of-process component that refuses to shut down may go on taking up memory long after all clients have terminated.

Note You can’t force an in-process component to remain loaded beyond the termination of its client. When the client closes, the component is unloaded regardless of any outstanding object references, open forms, and so on. This includes references the client may have obtained, and then passed to other client processes.

Reasons to Keep a Component Loaded

What rule is without exceptions? There are two important circumstances in which you may want to keep your component in memory deliberately:

  • If your component is a standalone desktop application that also provides objects, the way Microsoft Excel does, and it’s been started by the end user, then it’s inappropriate for it to close when the last client releases its last reference.

    Note Visual Basic helps you with this, by not shutting down an out-of-process component that has a loaded form, as described in "Visual Basic Component Shutdown Rules" under Rule 2: Forms.

  • If you’re designing a component to work with a particular client — for example, if client and component are parts of a larger system — and the client frequently releases all of its references to objects the component provides, you may want to keep the component running to avoid the delay caused by reloading it into memory.

    Note You can keep your component loaded in this situation using the information in "Visual Basic Component Shutdown Rules," under Rule 1: References and Rule 2: Forms.

In either of these special cases, forcing your component to remain loaded means that you must find some other way to determine when it should unload. For more information, see "Visual Basic Component Shutdown Rules."


Visual Basic Component Shutdown Rules

This topic is closely related to "ActiveX Component Shutdown," which describes the correct shutdown behavior for components.

The rules Visual Basic uses to determine when to shut down are different for in-process and out-of-process components.

Rule | In-process component | Out-of-process component
References | There are no references —internal or external — to the component’s public objects. | No out-of-process clients have references to the component’s public objects.
Forms | The component has no forms visible. | The component has no forms loaded.
Code | No code in the component’s modules is currently executing, or in the calls list waiting to be executed. | Same as in-process.
Startup | The component is not in the process of being started, in response to a client request for an object. | Same as in-process.

Rule 1: References

When determining whether to shut down a component, Visual Basic considers only the references to public objects. References to objects created from classes whose Instancing property is Private will not count toward keeping a component running.

Important Classes in the VB type library are also private, and references to objects of these classes will not keep a component loaded. Classes in the VBA type library are public. (To view the classes in each of these libraries, select the library of interest in the Project/Library box on the Object Browser.)

In-Process Components

For in-process components, the References rule has additional consequences in two areas: private objects and internal references to public objects.

Note A client application written in Visual Basic may not attempt to unload an in-process component immediately after the last reference is released. The frequency of attempts depends on how frequently idle time becomes available. Two minutes is a good rule of thumb.

Invalid References to Private Objects May Be Fatal to In-Process Clients

Suppose your in-process component passes a client a reference to a private object, and the client subsequently releases its last reference to your public objects. Your component may unload at any point thereafter — because the reference to the private object doesn’t prevent shutdown — leaving the client with an invalid reference to the private object.

If the client attempts to use this invalid reference, a program fault will occur, and the client will terminate abruptly. If the client rarely takes a code path in which it attempts to use the invalid reference, such program faults may appear random and difficult to debug.

Important Private objects — that is, objects from classes in the VB library, or objects from classes whose Instancing property is Private — are usually private for a reason. If you need to pass an instance of one of your private classes to a client, you should do the work to make the object safe for public use, and then change the Instancing property of the class to PublicNotCreatable.

Internal References to Public Objects Prevent DLL Unloading

For in-process components, Visual Basic has no way to distinguish between external references to public objects (that is, references held by a client) and internal references to public objects (that is, references in object variables within your in-process component).

Therefore, if your in-process component is holding a reference to one of its own public objects, Visual Basic will not unload it. Any objects your component is holding references to will continue taking up memory and resources.

The most common causes of such internal references are global object variables — for example, a global collection — and circular references. Circular references arise when two objects keep references to each other, as discussed in "Dealing with Circular References," in "General Principles of Component Design." The sample application for "General Principles of Component Design" explores workarounds to global collections and circular references.

Multiple In-Process Components May Keep Each Other Loaded

If several in-process components are in use, and they obtain references to each other’s public objects, the object references will keep Visual Basic from shutting the components down.

Out-of-Process Components

For an out-of-process component, Visual Basic keeps separate counts of external and internal references to public objects. Only external references — that is, references held by clients — will keep your component running. This is in contrast to the counting rule for in-process components.

Note In contrast to in-process components, Visual Basic’s marshaling rules prohibit out-of-process components from passing private object references to clients.

Avoid Keeping Internal References

Internal references to public objects — global object variables, global collections, or circular references — should still be avoided. Although internal references will not keep an out-of-process component running, they may keep orphaned objects from being destroyed.

For example, suppose an out-of-process component is being used by two clients, and that client A releases all of its object references before client B does. If the component is holding internal references to the objects client A was using, they will go on taking up memory and resources until the component shuts down — which will not happen until client B releases the last of its objects.

If in the meantime a third client begins using objects provided by the component, orphaned objects from both client A and client B may go on taking up memory and resources.

References Held by In-Process Components Don’t Count

Your out-of-process component may be using objects provided by an in-process component, running in the out-of-process component’s process space. The out-of-process component may pass references to its public objects to the in-process component, or the in-process component may independently request an object from the out-of-process component.

Regardless of how they are obtained, references held by an in-process component will not keep the out-of-process component from unloading when out-of-process clients have released all their object references.

Rule 2: Forms

It may seem odd that Visual Basic takes forms into account when determining whether it’s safe to close a component. To understand the logic of this, consider applications like Microsoft Excel, which can be started by the person who uses the computer, and can also provide objects to client applications.

If it’s wrong to pull your component out from under client applications that are using its objects, it’s even worse to pull a user’s spreadsheet, perhaps representing hours of painstaking labor, out from under the user just because client applications have released all the objects they were using.

In other words, the person using the user interface (if your component has one) is also a client.

How Visual Basic Interprets the Forms Rule

In-process components depend on their clients in a different way than out-of-process components do, so Visual Basic interprets the Forms rule differently for the two cases.

A visible form will keep an in-process component in memory, even if the client has released all object references; an invisible form will not.

Note In-process components can be kept running in this fashion only until the client application closes. At that time, all forms are forced to unload. Forms unloaded at this time do not receive QueryUnload events, and the cancel argument of the Unload event is ignored.

By contrast, any loaded form will keep an out-of-process component running past the point at which all references have been released — even past the termination of all clients.

Visual Basic makes this distinction because an out-of-process component may be a standalone desktop application, and the fact that its forms are invisible may be only a temporary state.

Controlling Form Lifetime

In general, forms should be unloaded when the object that created them terminates. For example, your component may use a hidden form to hold a Timer control. You can put code to unload the form in the Terminate event procedure of the object that creates and loads the form.

Tip If multiple objects are sharing the same form, you can create a UseCount property for the form. Increment and decrement the UseCount property in the Initialize and Terminate events of the objects, and unload the form when the UseCount reaches zero.

Components That Are Desktop Applications

If your component is also a standalone desktop application, its main form will keep your component running even if no client applications are using your objects. When the user chooses Exit from your File menu, or clicks the close box on your main form, you should simply unload the form, along with any hidden forms that are loaded, such as frequently used dialog boxes.

As far as the user is concerned, your application has closed. However, if client applications still have references to your objects, Visual Basic keeps your component running in the background.

When Client Applications Can Manipulate the User Interface

The most difficult shutdown case is for a component that provides objects that control its user interface. For example, you might create a Window class that controls the MDI child forms in your application’s main form. If the user closes your main window, should you hide the main form even though client applications have child windows visible?

Questions like this keep user interface designers awake at night. You have many options. For example, the QueryUnload event of your main form has an unloadmode argument you can test to determine if the form is closing because the user clicked the close box, or because your program code is trying to unload it.

You can unload the child forms that the user opened through your application’s menus and toolbar buttons, hide the main form, and set the Visible property of your Application object to False. Client applications can set the Visible property to True again if necessary.

Alternatively, you can unload all the user’s forms, and leave the remaining child forms visible in your main window. You may want to show the user a message indicating what you’re doing, and perhaps provide the option of hiding the main window anyway, because the computer user is generally regarded as the ultimate owner of the visual interface.

There are no right answers. Your best bet is to do lots of usability testing.

Rule 3: Code Executing

Visual Basic will not unload your component if it is currently executing code. This includes the case where your component has called a routine in another component, a function in a DLL, or a Windows API function. While the external routine is executing, the procedure that made the call is waiting in the calls list, and Visual Basic will not close your component.

There are no actions you need to take — or refrain from — in regard to this shutdown condition. Like external object references, Visual Basic manages it for you.

Rule 4: Startup

The last condition is a special case that covers the awkward gap between the time your component to starts up because a client application has requested one of your objects, and the moment when your component returns the object reference. There may be moments in the startup sequence when there are no forms loaded and no code executing, and there certainly aren’t any external object references.

As with the code execution condition, you don’t need to take any action because Visual Basic handles this case for you.

Summary of Guidelines for Component Shutdown

The following guidelines summarize what you need to remember about component shutdown.

  • A well-behaved component does not control its own lifetime.

  • Don’t shut an out-of-process component down forcibly using the End statement.

    As a corollary to this, don’t implement a Quit method. Releasing a component by releasing all references to its objects is good programming practice. Educate users of your component by explaining in your Help file that this is the correct way to release your component. Don’t encourage bad programming practices by giving developers a shortcut that could disrupt other clients.

  • Don’t prolong the life of your component needlessly by keeping a form loaded but hidden when there are no objects using it.

  • Avoid circular references that artificially prolong the life of your component, such as an object that holds a reference to a form that in turn holds a reference to the object. Circular references are discussed in "Dealing with Circular References," in "General Principles of Component Design."

  • If your component is a standalone desktop application that also provides objects, remember that the computer user is one of your clients.

Most of the work of being a good component is done for you by Visual Basic. You can go a long way by simply letting it do its job.


Object Model Creation Guidelines

As discussed in "Organizing Objects: The Object Model," in "General Principles of Component Design," the hierarchy you use to organize the objects in your component can range in complexity from none at all — that is, only independent externally creatable objects with no dependent objects — to a large structure with many dependent objects.

This section offers guidelines for two aspects of designing object models for the components you build using Visual Basic:

  • Guidelines for Creating Collection Classes Summarizes the recommended collection class implementation guidelines, and provides references to additional discussion and examples.

  • Guidelines for Creating Root Objects A root object acts as the base of your component’s object model — as if it contains all the other objects. There are many ways to implement a root object, and many possible names.

For More Information The Microsoft Press OLE 2 Programmer’s Reference contains in-depth information on standard objects, including additional component functionality you can gain by building your component with a tool such as Microsoft Visual C++.


Guidelines for Creating Collection Classes

A collection provides a way to connect a group of dependent objects with an object that ‘contains’ them. For example, an Invoice object might have a collection of LineItem objects.

As discussed earlier in "Naming Guidelines," the name of a collection should be the plural of the name of the object it contains. Thus an Invoice object might have a LineItems collection to contain its LineItem objects.

Implementing Collections

Visual Basic provides a generic Collection class for managing groups of objects, as discussed in "The Visual Basic Collection Object," in "Programming with Objects" in the Visual Basic Programmer’s Guide.

You can implement collections for your component’s object model by wrapping a private Collection object in a class module, thus defining a collection class from which you can create instances as needed. This is explained in "Creating Your Own Collection Classes," in "Programming with Objects" in the Visual Basic Programmer’s Guide.

In addition to describing the steps you need to take, "Creating Your Own Collection Classes" includes code examples that show why collection classes are the most robust way to use the Collection object.

A robust implementation is critical to the successful reuse of component code. Other programmers will rely on the robustness of the objects you create. Don’t take shortcuts.

Collection Class Properties and Methods

The following table shows properties and methods you should implement for collection classes.

Method or property | Description
Add | Adds an item to a collection. If used for object creation, the method should return a reference to the newly created object.
Count (property) | Returns the number of items in the collection.
Item | Returns a reference to a single item from the collection. The argument may be a numeric index or a key value. This is usually the default property.
Remove | Removes an item from a collection. The argument may be a numeric index or a key value.
NewEnum | Returns the IUnknown interface of an enumerator object that For Each … Next can use to iterate over the items in a collection. Should be hidden in the type library. Must have a Procedure ID value of -4 to work with For Each … Next.

Implementing an Add or Insert Method

One of the keys to creating a robust collection class is implementing your own Add method. Your Add method can delegate to the Add method of a Visual Basic Collection object, while providing type safety or controlling access to the collection.

Type safety is very easily implemented, as shown in the following code fragment for the Add method of a hypothetical Widgets collection class:

Public Sub Add(ByVal NewWidget As Widget, _
      Optional ByVal Key As String = "")
   If String = "" Then
      mcolWidgets.Add NewWidget
   Else
      mcolWidgets.Add NewWidget, Key
   End If
End Sub

All the work of maintaining the collection and generating error messages is delegated to a private instance of the Visual Basic Collection class, a reference to which is kept in mcolWidgets. The Add method of the private Collection object, which can accept objects of any type, is shielded by the declaration shown above, which can accept only Widget objects.

Note Many existing collection implementations use Add as the name of a method that creates a new element within a collection. You may want to name a method like that shown above — which puts an externally created object into a collection — "Insert" instead of "Add."

You can use the Add method to control access to a collection by making the class of objects the collection contains PublicNotCreatable. In this way, the Add method becomes the only way to create new objects within the collection. If you implement your Add method in this fashion, it should return a reference to the newly created object.

The Add method is a good place to call Friend functions that set the values of read-only properties such as the Parent property.

Examples and Utilities

"Creating Your Own Collection Classes" provides sample code for a collection class implemented according to these guidelines, and explains how to make Item the default method; how to hide NewEnum in the type library; and how to give NewEnum the correct Procedure ID.

The Class Builder utility, included in the Professional and Enterprise Editions of Visual Basic, creates collection class source code that follows these guidelines. You can customize this source code with your own events, properties, and methods.

For More Information See the "Add Method," "Remove Method," "Item Method," or "Count Property" reference topic, or see "The Visual Basic Collection Object," in "Programming with Objects," in the Visual Basic Programmer’s Guide.


Guidelines for Creating Root Objects

As the name implies, a root object is at the base of an object model. Thus it can be thought of as the object that contains all the other objects in the object model. Root objects are usually externally creatable — that is, clients can create instances of the root class using the New operator or the CreateObject function.

For example, the root object of a billing system might have, as one of its properties, an Invoices collection. Each Invoice object in the collection might contain a LineItems collection, so that you could access a particular LineItem object in this fashion:

Dim li As LineItem
Set li = <root object>.Invoices(1138).LineItems(3)

Naming the Root Object

The name of the root object will vary, depending on the type of component. If your component is a standalone desktop application that also provides objects, the way Microsoft Excel does, then "Application" might seem like a logical name for your root object.

On the other hand, if your component runs in process, providing objects to clients but having no user interface of its own, "Application" is probably not an appropriate name for the root object. Use a name that suggests the functionality of your component, or that is similar to your component’s type library name.

Tip So many object models use "Application" as the name of their root object that it has become a source of global name space pollution. That is, in order to get the right Application object, you frequently have to qualify it with the type library name. Be original. Find a name that suggests the unique character of your component’s root object.

Note Although "Application" is generally recognized as the prototypical root object name, the code examples in this topic use "Root" instead — simply because "Application" is so thoroughly overused.

Root Object Properties and Methods

A root object is the logical place to define properties and methods that affect the entire component. For example, a root object might have a read-only Version property. By definition, a root object will also contain collections or object properties for objects at the next level of the hierarchy.

Global Root Objects and Global Helper Objects

It’s fairly common for a root object to be a global object. You can do this in your component by setting the Instancing property of your root object’s class module to GlobalMultiUse, as described in "Global Objects," in "Building Code Components." This makes the properties and methods of the root object appear to be global procedures, the way the members of the VB and VBA libraries are.

If your root object is global, you can allow users to start an instance of your component simply by referring to the root object in code. Provide the root object with a read-only property of the same name, which returns a self-reference. The following code fragment shows how this might look for a root object named "Root."

Public Property Get Root() As Root
   Set Root = Me
End Sub

This sounds self-referential, but it works — when a client refers to Root in code, the global object is created before the Property Get is called.

Backing Your Root Object with a Global Object

For some components, it may not be appropriate to have all of the properties and methods of your root object dumped willy nilly into the global name space. In this case, don’t make your root object global. Instead, create a class named Globals, and set its Instancing property to GlobalMultiUse.

You can add to this Globals class those properties and methods you want to have in the global name space. In those cases where you want to expose the root object’s methods, add methods with the same arguments to the Globals class, and delegate to the method of the same name on the root object.

You can still allow users to start your component simply by referring to its root object in code; simply add a property such as the following to the Globals class:

Public Property Get Root() As Root
   If gRoot Is Nothing Then
      Set gRoot = New Root
   End If
   Set Root = gRoot
End Property

This example assumes your root object is named "Root," and that you keep a reference to it in the global variable gRoot.

The Globals class is a good place to put general-purpose procedures you want your users to be able to access globally.

Tip Implement any object properties you add to the Globals class using Property Get procedures, rather than with public object variables. Property Get defines a read-only property, whereas users may accidentally set public object variables to Nothing.

The GetObject Function

"Programming with ActiveX Components," in the Visual Basic Programmer’s Guide, describes the use of the GetObject function to get a reference to a running instance of a component, or to test to see if one is running. A client application cannot automatically use the GetObject function to obtain a reference to an existing instance of a class defined in a Visual Basic component.

You can do the work yourself, by putting your root object in the Running Object Table (ROT). COM provides the ROT as a location for running objects to register themselves, so that functions like GetObject can be used to locate them.

An alternative technique is to make your root object PublicNotCreatable, so that clients can’t create instances directly. Instead, you can provide a MultiUse Connector object which clients can create. The Connector object can provide a property (generally with the same name as your root object) that returns a reference to a single shared root object.

This technique is described in "Asynchronous Call-Backs and Events" in "Building Code Components," and demonstrated in the sample applications for that chapter.

Tip A Globals object, described earlier in this topic, can provide the same functionality as a Connector object, in an unobtrusive fashion.

For More Information The ROT is explained in many books about COM, such as Inside OLE, by Kraig Brockschmidt, or the OLE Programmer’s Reference, both of which are available from Microsoft Press.

Instancing for Root Objects

The functionality your root object provides depends heavily on which value you choose for the Instancing property of its class module: PublicNotCreatable, SingleUse, GlobalSingleUse, MultiUse, or GlobalMultiUse.

PublicNotCreatable Root Objects

If your root object is marked PublicNotCreatable, client applications cannot create instances of it using the New operator or the CreateObject function. While this may seem somewhat upside down — most people will expect the root object to be a way to get at all the other objects in your object model — in conjunction with a global object or a Connector object, as described earlier in this topic, it can be a way to allow clients to share a single instance of your root class.

In order to give clients access to the root object in this case, you must give your public creatable objects (such as the Connector object or the Globals object) a property that returns a reference to the root object, as shown here:

Property Get Root() As Root
   Set Root = gRoot
End Property

Note that this does not create circular references, because the dependent objects are not holding actual references to the Root object; they are only using Property Get to return a global reference.

You can also implement a global object with such a property, as described earlier in this topic.

SingleUse Root Objects

If the class that defines your root object is has the Instancing property settings SingleUse or GlobalSingleUse, a client application will start a new running instance of your component every time it uses the New operator or the CreateObject function to create a root object.

In its Initialize event procedure, your root object might set a global reference to itself that other objects can use to gain access to it, as shown in the following code fragment:

Private Sub Class_Initialize()
   ' Variable gRoot is declared Public in a standard
   '   module.
   Set gRoot = Me
   ' ...additional initialization code...
End Sub

If you don’t keep a global reference to the root object, and the client releases its reference — while keeping references to one or more dependent objects — your component can get into a state in which the dependent objects exist without the root.

Note Because a SingleUse root object causes a new instance of your component to be loaded for each client request, keeping a global reference doesn’t result in orphaned objects (as described in "ActiveX Component Shutdown"), unless clients pass each other references to root objects.

SingleUse Root Objects and Exclusive Use of Components

Depending on whether you mark additional class modules as externally creatable, marking your root object SingleUse can also give each client application exclusive use of a copy of your component.

If the only externally creatable class in your component is the root object, every client application will have its own instance of your component. Because Visual Basic classes do not support the use of the GetObject function to obtain references to existing objects, there is no way another client can get access to a running copy of your component.

The disadvantage of this is that every client must start an instance of your component, which will use more memory and system resources. If you need to give each client application exclusive use of a copy of your component, you may find it more efficient to write an in-process component.

SingleUse Root Objects and Shared Components

If you mark other classes in your component MultiUse or GlobalMultiUse, clients can create objects from those classes without first creating a root object.

This is similar to the way Microsoft Excel’s externally creatable objects work. If you create a Microsoft Excel Application object, you start a new running instance of Microsoft Excel. If you create a new Sheet or Chart object, however, they will be provided by a previously running instance of Microsoft Excel, if there is one.

To handle additional externally creatable objects, the Initialize event procedures for all externally creatable objects should test for the existence of the root object, and create it if it doesn’t yet exist:

' Code fragment from any externally creatable class.
Private Sub Class_Initialize()
   If gRoot Is Nothing Then
      Set gRoot = _
         CreateObject("MyComponent.Root")
   End If
   ' ...more initialization code for this object...
End Sub

It’s important to use the CreateObject function to create the root object, rather than the New operator. Otherwise a client can create another root object in the same instance of the component at a later time. For more details, see "Scalability Through Multiple Processes: SingleUse Objects" and "How Object Creation Works in Visual Basic Components" in "Building Code Components."

Tip Why not create the root object in your Sub Main procedure? For one thing, creating objects in Sub Main is not a good idea. It can lead to deadlocks and object creation time-outs. Furthermore, if your component is starting in response to a request from a client, you cannot tell what object is being created. If you create the SingleUse root object in Sub Main when a client has requested the root object, the client’s request will fail.

Note Marking multiple classes in your component SingleUse or GlobalSingleUse may produce unexpected results, as discussed in "Scalability Through Multiple Processes: SingleUse Objects" in "Building Code Components."

MultiUse Root Objects

If you mark your root class MultiUse or GlobalMultiUse, each client that will get its own root object, but all the instances may come from the same running instance of your component.

The implementation decision you face is this: Should all those instances share the same collections of dependent objects, or should each root object have its own collections? This depends entirely on what use you intend for your component. If it’s an out-of-process component that’s also a standalone desktop application, you may want each of the root class instances to act like an Application object — in which case they should all share global collections of dependent objects.

Independent Root Objects

On the other hand, if each root object and the dependent objects it contains represent a unit of functionality, and you want clients to be able to use more than one unit, then the root class instances should be independent of each other, each with its own collections of dependent objects.

You can still back your root objects with a Globals object, as described earlier in this topic, only in this case the Globals object would need to contain a collection of root objects instead of a single instance.

Data that’s global to the running instance of your component, such as version number, could also be exposed using read-only properties of the Globals object.


Creating Interfaces for Use With the Implements Statement

When you create interfaces for use with the Implements statement, you can use Visual Basic or any tool that creates type libraries, such as the MkTypLib utility or the Microsoft Interface Definition Language (MIDL) compiler.

Most classes created in Visual Basic define interfaces that work with Implements. When you create an interface by defining a class in Visual Basic, simply make sure that none of the properties or methods have underscores in their names.

Interfaces created with tools other than Visual Basic must follow certain restrictions in order to work with Implements. The following list includes most of these restrictions.

  • Interface methods cannot have underscores in their names.

  • Only [in] and [in,out] params are allowed; neither [out] only params nor [lcid] arguments are allowed.

  • Method return types must be HRESULT, in order for errors to be propagated. You will not see the HRESULT in Visual Basic, as it is translated into an exception (raised error). To create a method that will have a return type when used in Visual Basic code, you must use [out, retval] on the final parameter.

  • Only Automation data types may be used:
    VB Data Type | MIDL Equivalent
    Integer | short
    Long | long
    Single | float
    Double | double
    Byte | unsigned char
    Boolean | boolean or VARIANT_BOOL
    String | BSTR
    Variant | VARIANT
    Date | DATE
    Currency | CURRENCY or CY
    Object | IDispatch
    IUnknown | IUnknown

  • SAFEARRAY parameters containing any of the simple data types from the list above are allowed.

  • Enum parameters are allowed.

  • Dispinterface interface pointers are allowed as parameters.

  • Dual interface pointers are allowed as parameters.

  • CoClass parameters are allowed.

Warning If you're creating a type library in order to make a system interface usable with Implements, you must not use the [oleautomation] or [dual] attributes. Type libraries must be registered before you can add them to the Visual Basic References dialog box, and registering a type library with the [oleautomation] attribute will overwrite information required to remote the system interface. This will cause other applications on the system to fail. The [dual] attribute must not be used because it implies [oleautomation].

Note It may be useful to specify [oleautomation] while creating the typelib, in order to enforce correct types, but the type library must be built without the attribute before you reference it through the Visual Basic References dialog box.

  • Unsigned long and unsigned short parameters are not included in the data type table and are not allowed.

  • User-defined data types (structures) are not allowed as parameters.

  • Interfaces must be based on IUnknown or IDispatch. The full vtable (after IUnknown/IDispatch) must be described in a single interface.

  • Restricted vtable entries are ignored and do not prevent the Implements statement from working.

  • Most pointers cannot be passed as [in] parameters. (For example, as a remoting optimization, a C++ interface can declare a parameter as [in] VARIANT* pVar. This will not work with Implements.) An [in] parameter can be a BSTR, a pointer to an interface (for example, IDispatch*), or a SAFEARRAY pointer (SAFEARRAYs are always passed as pointers). An [in,out] parameter can be a pointer to an Automation type, or a double pointer to an interface (for example, IDispatch**). (Note that 'ByVal As String' in Visual Basic maps to [in] BSTR. You cannot use [in] BSTR* with Visual Basic.)

  • Implements does not work with dispinterfaces.

For More Information See "Polymorphism" in "Programming with Objects" in the Programmer's Guide, and also "Providing Polymorphism by Implementing Interfaces" in "General Principles of Component Design" in Creating ActiveX Components in the Component Tools Guide.


Extending the Visual Basic Environment with Add-Ins

The Microsoft Visual Basic development environment comes with a fixed level of functionality and a select set of tools. You may find that you require some functionality or a tool that doesn't exist in Visual Basic's Integrated Development Environment (IDE), or find yourself repeatedly doing a simple task. To solve this dilemma with prior versions of Visual Basic, you might have purchased pre-fabricated controls or toolsets to augment the IDE. Now, you can elect to create your solutions from within Visual Basic using its extensibility object model.

Objects from this model can be programmatically used to construct add-ins — modular ActiveX components that connect to the IDE and provide it additional functionality. In many cases, if the functionality is generic enough, these ActiveX components can even be used in other applications (that can use ActiveX components).

Add-ins can be constructed to work inside or outside the IDE, or both. They can automate repetitive tasks, monitor the IDE for the occurrence of certain events, act upon those events, or perform complex tasks. Add-ins can be activated through menu commands, toolbar buttons, or through Windows events.

One variation of add-in is a Wizard. A Wizard is a step-by-step instructive program you can create to lead users of your applications through a particularly difficult procedure.

With add-ins and the extensibility model, you are now able to tailor the Visual Basic IDE to your unique needs with applications that do exactly what you want them to do.

Chapters

Add-Ins Overview

Explains extensibility and how it's implemented in Visual Basic in the form of add-ins, and provides a step-by-step procedure for creating a simple, working add-in.
Extensibility Model Objects and Structure

Provides a map to the objects you can use to create and connect add-ins, along with links to more detailed reference information on each.
How to Build an Add-In

Offers in-depth discussion of the steps and issues associated with creating add-ins to automate programming tasks. Uses code from a sample application included with Visual Basic.
Connecting and Exposing Add-Ins

Covers add-in essentials and illustrates methods for connecting and exposing your add-ins in the Visual Basic environment.


Add-Ins Overview

Consider, for a moment, a socket wrench.

By attaching different sockets to it, you can use it for different tasks. Add-ins are to the Microsoft Visual Basic development environment what sockets are to a socket wrench; they provide it additional and interchangeable functionality. Unlike a socket wrench, though, you can attach several add-ins to Visual Basic at the same time, among other things.

In the following topics, you'll discover exactly what add-ins are, what they can do to help you program in Visual Basic, and how you can create and use them.

Topics

What is Extensibility?

Explains the concept of extensibility, how extensibility is implemented in Visual Basic, and what it means to you as a Visual Basic programmer.
What is an Add-In?

Describes what an add-in is in Visual Basic and the assorted add-in variants.
Creating a Basic Add-In

Leads you step-by-step though the process of creating a simple, basic working add-in. Afterward, there is an explanation of what was done and why.


What is Extensibility?

Extensibility is the capacity to extend or stretch the functionality of the development environment — to add something to it that didn't exist there before. Extensibility is not a new concept to development environments. Some language development packages give you the ability to add functionality to their user environments. However, the extent of control provided to you can be limited, or difficult to understand and implement.

Visual Basic provides you with a powerful, easy-to-understand modular system for customizing its environment through a programming interface known as the Extensibility object model. Its sole purpose is to allow you to "hook into" the workings of Visual Basic and create extensions known as add-ins.

The Visual Basic extensibility model consists of six related groups of individual code objects which control each major facet of the Visual Basic IDE. Each functional group enables you to create tools to customize the Visual Basic development environment and help you accomplish your programming tasks more quickly and easily. These groups are:

  • Form manipulation objects

  • User-interface manipulation objects

  • Event response objects

  • Add-in management objects

  • Project and component manipulation objects

  • Code manipulation objects

Each group consists of one or more collections and objects which together contribute to a particular purpose. For example, the primary function of the forms object group is to allow you to programmatically manipulate forms in your Visual Basic projects. One object in this group is the CommandBar object, which allows you to manipulate menus and toolbars. Another is the CodePane object, which allows you to display code contained in an object.

Other groups contain objects which allow you to manipulate and maintain the projects themselves, their source code, how they respond to events, and so forth.

What does all this mean to you? This means that you don't have to settle for the constraints of the programming environment or rely on third-party extensions that don't do exactly what you want them to do. You might, for example, create add-ins to:

  • Customize the Visual Basic user interface. For example, you can create an enhanced version of the Project Explorer window where you can customize views, add or delete folders, navigate and view project elements, and so on.

  • Control projects. You can design an add-in that loads a group of projects, one after another.

  • Programmatically edit code. You can use an add-in to automatically add comments to certain lines of code.

  • Control forms. You can create an add-in that closes all currently open forms, or one that adjusts all open forms to the same size.

These are just a few ideas. Add-ins give you the freedom to create custom programming solutions to save you time and help you become more productive. Just think of the possibilities!

For more information For information on different kinds of add-ins, see "What is an Add-In?" in this chapter.


What is an Add-In?

You can use the Visual Basic extensibility object model to ease development through add-ins. Add-ins are tools that you create programmatically using objects and collections in the extensibility model to customize and extend the Visual Basic environment. In a sense, add-ins "snap on" to the Visual Basic integrated development environment (IDE).

Whether created in Visual Basic or Visual C++, the primary goal of an add-in is to enable you to automate something in the development environment that is difficult, or tedious and time-consuming to accomplish manually. Add-ins are time- and labor-saving automation tools for the Visual Basic programming environment.

Note When using C++ to develop add-ins, whenever you get a pointer from Visual Basic using Automation, you must call "release" when you are finished using it. Visual Basic follows the standard reference counting rules. For example, when you get a pointer out of Visual Basic by calling _NewEnum, you must call release through that pointer. Also, when you use pDispatch = m_ourMenuItems.Item(pszCaption), you must also use pDispatch->Release().

There are four types of add-ins: add-ins, Wizards, utilities, and builders.

  • Add-in is the generic term for a program you create that performs task(s) within the IDE, often in response to certain events (such as a mouse click or a form opening). Its actions may or may not be visible to the user.

  • A Wizard is a special type of add-in which leads a user step-by-step through a task, often an especially complex or tricky one. Although they can be constructed any way you like, Microsoft implements its Wizards in a specific manner. A Microsoft Wizard consists of a number of frames, each frame containing an image in the upper left corner, a label description to the right of the image which may also contain instructions, and an optional area near the bottom in which other controls (such as ListBox or CommandButton controls) can be placed. To help you create Wizards, we've included an add-in called the Wizard Manager.

    For more information about wizards, see "Wizards and the Wizard Manager" in "How to Build an Add-In."

  • A utility is an add-in, sometimes a rather large one, which doesn't necessarily require Visual Basic to run. This means that it is compiled as an ActiveX executable, but it retains the ability to also be called as an add-in in the Visual Basic IDE. VisData is an example of a utility.

  • A builder is a type of add-in which helps a user to view or set properties of a control, or properties that several controls have in common. However, builders aren't normally created as add-ins anymore because Visual Basic gives you many builder-like capabilities at design time through the PropertyPage object.

How to Build an Add-In" discusses the specifics of creating these add-in variants.

Most add-ins that you've seen or used before were probably visually-based; that is, they appeared as commands in menus or as buttons on toolbars in the Visual Basic IDE. Now, add-ins don't necessarily have to be visible — they can sit hidden in the background and respond to events such as the sizing of a form or control. Or they may be invisible and do something like reset a timer control when a certain project loads, or they may be entirely visual; for example they may size a group of like objects by sizing only one of the objects.


Creating an Add-In

Creating a Basic Add-In

Building an add-in consists mainly of creating a class module that handles events specific to add-ins and any events you want to specify, along with your support modules and forms. Unless you need to have multiple instances of an add-in running in the same IDE, all class procedures should be declared Private to prevent other routines from inadvertently referencing them.

Once the add-in code is complete, you must compile it as an ActiveX .dll or .exe file, since add-ins must be ActiveX components. For various reasons, it's generally best to create add-ins as ActiveX .dll files.

For more information For a brief comparison of add-ins as .exe and .dll files, see "Compiling Add-Ins" in "How to Build an Add-In." get your feet wet, start by building a very simple add-in that demonstrates an essential component of an add-in — its Class module. How this example works will be explained afterward.

Note The procedure below leads you manually through creation of an add-in. Alternatively, you can simply create an Add-In project, and all of the basic add-in infrastructure is created for you.

To create the AddInProject add-in

  1. On the File menu, click New Project to open the New Project dialog box. (This will close your current project or project group; you will be prompted to save any changes you have made.) Double-click the ActiveX DLL icon to create a new project.

  2. Click the Project menu, then click References. Select the Microsoft Visual Basic Extensibility and Microsoft Office 8.0 Object Library check boxes.

    This gives you access to the extensibility objects and collections that you need to create add-ins.

  3. Click the Project menu, then click Add Module. In the Add Module dialog box, double-click the Module icon to create a new module.

    Note that there is an icon for an AddIn template. The template includes some of the code necessary for beginning an add-in.

    Enter the following code in the new module:

    Declare Function WritePrivateProfileString& Lib _
"kernel32" Alias "WritePrivateProfileStringA" _
(ByVal AppName$, ByVal KeyName$, ByVal _
keydefault$, ByVal FileName$)
Sub AddToINI()
Dim rc As Long
rc = WritePrivateProfileString("Add-Ins32", _
"AddInProject.AddInClass", "0", "VBADDIN.INI")
MsgBox _
"Add-in is now entered in VBADDIN.INI file."
End Sub

    Alternatively, you can use the Add-In designer to create a reference to your add-in. For detailed instructions about how to do this, see "Referencing Add-Ins" in Chapter 4, "Connecting and Exposing Add-Ins".

  4. In the Project Explorer window, double-click the Class module Class1 to bring it to the front. Set its Name property to "AddInClass" and make sure that its Instancing property is set to "5 – MultiUse".

  5. Add the following line of code to the Class module:
    Implements IDTExtensibility

    This adds a reference to the IDTExtensibility object to your project.

  6. Click IDTExtensibility in the Object box.

    Notice that four new events appear in the Procedure box: OnConnection, OnDisconnection, OnStartupComplete, and OnAddInsUpdate.

  7. Click each of the events in the Procedure box to add their procedures to your Class module.

    While you can enter the procedure syntax manually, it's strongly recommended that you click the event name in the box to add the procedures to the Class module to ensure that all names and arguments are entered correctly. Plus, it's faster!

    All four of these event procedures must be present in your Class module for add-ins to work correctly. Also, if one or more of the event procedures has no code in it, it will be removed upon compilation, so it's important that you add at least a comment to each of the four events to ensure that they remain in the Class module when you compile.

  8. Now that you have all four events added, add the following code to them:
    Private Sub IDTExtensibility_OnConnection(ByVal _
VBInst As Object, ByVal ConnectMode As _
VBIDE.vbext_ConnectMode, ByVal AddInInst As _
VBIDE.AddIn, custom() As Variant)
MsgBox "Add-in is now connected"
End Sub

Private Sub IDTExtensibility_OnDisconnection(ByVal _
RemoveMode As VBIDE.vbext_DisconnectMode, _
Custom () as Variant)
MsgBox "Add-in is now disconnected"
End Sub

Private Sub IDTExtensibility_OnStartupComplete _
(custom() As Variant)
' Comment to prevent procedure from being
' deleted on compilation.
End Sub

Private Sub IDTExtensibility_OnAddInsUpdate _
(custom() As Variant)
' Comment to prevent procedure from being
' deleted on compilation.
End Sub
  9. On the Project menu, click Project1 Properties, then enter AddInProject in the Project Name box. Click OK.

  10. On the File menu, click Save Project to save the project files. Name them as shown in the following table. Visual Basic will provide the indicated extensions automatically.
    File | File name | Extension
    Basic module | AddIn | .bas
    Class module | AddInClass | .cls
    Project | AddInProject | .vbp

To test the AddInProject add-in

  1. Click the File menu, then click Make AddInProject.dll. In the Make Project dialog box, click OK.

    This will register the add-in in the system registry.

  2. In the Immediate window, enter AddToINI and press RETURN. You get a message box that says "Add-in is now entered in VBADDIN.INI file."

  3. On the File menu, click New Project to open the New Project dialog box. (This will close AddInProject.vbp; you will be prompted to save any changes.) Double-click the Standard EXE icon to create a new project.

  4. Click the Add-Ins menu, then click Add-In Manager. Notice that a new entry, "AddInProject.AddInClass," appears in the list.

  5. Select AddInProject.AddInClass check box, then click OK.

    At this point, you should get the following dialog box:

  6. Click OK. Clear the AddInProject.AddInClass check box, then click OK.

    You should get the following dialog box:

What Just Happened?

The purpose of this add-in is to demonstrate the components and behavior of an essential component of an add-in — its Class module. Let's go through each of the previous steps and discuss what it did, and more importantly, how and why it was done the way it was.

  • The AddToINI procedure has only one purpose in life: to add an entry for your new add-in to a file in the Windows directory called Vbaddin.ini. Why do this? So that it is recognized by Visual Basic as an available add-in. You can add this entry any way you like, but the Windows WritePrivateProfileString API function call works well for this.

    This brings up an important point: When you distribute an add-in to other users, either you or they must run this function (or use some alternative method) on their machines to update the Vbaddin.ini file before they'll be able to use your add-in. If you don't do this, Visual Basic won't know that the add-in is available, and the add-in won't appear in the list of add-ins in the Add-In Manager.

    You can put such a procedure in a basic module (as was done here) and instruct the user to run the function, but a more elegant solution is to do it for the user in a setup program that you supply to install the add-in.

    Alternatively, you can use the Add-In designer to create a reference to your add-in. For detailed instructions about how to do this, see "Referencing Add-Ins" in Chapter 4, "Connecting and Exposing Add-Ins".

  • The Class module is the heart of an add-in. It is the place where the add-in responds to events. In this example, it admittedly doesn't do very much — it just puts up message boxes when you connect and disconnect the add-in — but you could design an add-in that responded to literally hundreds of events!

    Although the main Class module is useful, it's best not to put all of the code in it, unless it's brief. You can put other code in basic modules or other Class modules and call the procedures from the main Class module. Just follow your normal procedural programming guidelines.

  • The act of compiling the DLL registers it in the system registry.

  • Running the AddToINI procedure in the Immediate window creates an .ini file entry for the add-in in Vbaddin.ini. For more information on how to add this reference, see "Adding an Add-In Reference to the Vbaddin.Ini File" in "Connecting and Exposing Add-Ins."

  • The rest of the steps show how you can now activate the add-in in another project as well as demonstrate how it appears and behaves in the Add-In Manager. Also, they show how selecting and clearing the add-in entries in the Add-In Manager triggers the OnConnection and OnDisconnection events.


Extensibility Model Objects and Structure

When driving through an unfamiliar city, it’s usually helpful to have a road map to find your way around. Consider this chapter your road map to the extensibility model. Studying and becoming familiar with it will make it quicker and easier for you to program that perfect add-in that does exactly what you need it to do.

Many objects in the model relate to each other for specific functional purposes, such as manipulating source code or connecting and using add-ins. In the diagram in “The Extensibility Model Objects,” in this chapter, these related objects are grouped together by similar boxes. This makes it easier for you to locate the objects you need to accomplish the tasks you want to perform.

When you click on a group in the diagram, you are brought to the appropriate topic containing information about the group. In those topics, you can click the object boxes to go to a specific topic on that object. From there, you can get information on its syntax, usage, examples, and what properties, methods, and events apply to it.

For example, if you were to click anything in the group “Objects that Extend the User Interface,” you’d be taken to a topic about that group. If you were then to click the CommandBar object box in the group topic diagram, you’d be taken to the CommandBar object topic. In that topic, you’d be able to see a detailed description of the CommandBar object, plus all the properties, events, and methods that apply to it.

This chapter presents the overall structure of the Visual Basic extensibility object model — the “big picture.” In addition, it shows each functional group of objects, along with descriptions of all the objects in each group.

Topics

The Extensibility Model Objects

An overall map of the extensibility object model with the functional groups identified.
Objects that Extend the User Interface

Diagrams the objects that allow you to alter the user interface, for example by sizing and moving windows and viewing code.
Objects that Allow You to Manipulate Projects

Diagrams the objects that allow you to view, add, or remove projects.
Objects that Allow You to Manipulate Forms

Diagrams the objects that allow you to create forms, add them to projects, hide or display controls, and so forth.
Objects that Allow You to Respond to Events

Diagrams the objects that allow you to programmatically respond to Windows events, such as those notifying you when a particular form has been loaded.
Objects that Allow You to Manipulate Source Code

Diagrams the objects that allow you to view, alter, or delete code anywhere in your project.
Objects that Allow You to Use Add-Ins

Diagrams the objects that allow you to connect your add-ins to Visual Basic through a menu or toolbar button.


The Extensibility Model Objects

Here is an overview diagram of the extensibility object model:


Objects that Extend the User Interface

This group of objects helps you customize the Microsoft Visual Basic user interface.

Using these objects, you might:

  • Add a new button or command to an existing command bar. (A command bar is a menu or toolbar in Visual Basic.)

  • Create an entirely new menu or toolbar for your add-in.

  • Open, close, move, and alter the size of user interface windows.

  • Use the CodePanes collection and CodePane object to display code and determine what code a user has selected.


Objects That Allow You to Manipulate Projects

This group of objects helps you control and manipulate projects in Visual Basic.

Using these objects, you might:

  • Select one project out of several to operate on, or select several for group project operations.

  • Remove all other projects in Visual Basic and start a new project.

  • Add new projects to the current Visual Basic session.

  • Notify yourself or react in a certain manner when particular projects are loaded or unloaded.

  • Display project names elsewhere, such as in a combo box.

  • Change project options.

  • Add and remove components.


Objects that Allow You to Manipulate Forms

This group of objects helps you control and manipulate designers in Visual Basic.

Using these objects, you might programmatically manipulate the designers for a form, control, property page, or ActiveX document in order to:

  • Add a designer.

  • Add code and controls.

  • Hide or unhide the designer.

  • Position controls.

  • Change the designer’s properties.


Objects that Allow You to Respond to Events

This group of objects helps you respond to events that occur in Visual Basic. The VBA object can be either the Visual Basic environment, in design view, or the program that you create, at run time.

Events also occur on the following collections:

  • VBProjects

  • VBComponents

  • VBControls

  • References

Using these objects, you might monitor and/or intercept and handle events that occur in the Visual Basic interface, such as when a form is added to the project, or when a project is added.


Objects that Allow You to Manipulate Source Code

Objects that Allow You to Manipulate Code

This group of objects helps you programmatically control and manipulate code in Visual Basic.

Using these objects, you might:

  • Select, add, or delete lines of code.

  • Search and replace occurrences of certain strings.


Objects that Allow You to Use External Objects

Objects that Allow You to Use Add-Ins

This group of objects helps you control and manipulate add-ins in Visual Basic.

Using these objects, you connect and disconnect add-ins, and access objects provided by add-ins.


How to Build an Add-In

In this chapter, you'll learn the basics of building add-ins. We'll show you how to program each functional group of objects in the extensibility model, as well as how to use the Wizard Manager to easily create professional-looking Wizards. You'll also learn about the Add-In toolbar — a great place to place add-ins and Wizards for quick and easy user access.

Topics

Add-In Creation Basics

All add-ins share a number of common, essential elements. This section covers these elements, as well as use of the CreateToolWindow function, the Add-In template, and how to choose the best compile type for your add-in.
Programming in the Extensibility Model

The extensibility model contains major functional groups of objects and collections that enable you to take control of the Visual Basic IDE. This section covers each functional group and gives example code of how to use the objects and collections in each.
Wizards and the Wizard Manager

This section covers how to use the Wizard Manager to easily create Wizards which help users of your applications through difficult or thorny tasks.
The Add-In Toolbar

This section covers the Add-In toolbar; a dedicated toolbar for your most often-used add-ins and Wizards, allowing you to start them with a single button click.


Add-In Creation Basics

Every add-in project shares some common elements, and there are some decisions you should make before you get started with creating your add-in. This section is an overview of these elements.

The following topics describe requirements and issues for creating add-ins.

  • Add-In Essentials All add-ins, regardless of what they do, require that you reference these items.

  • Design Considerations for Add-Ins Two factors that impact your design consideration are usage of the CreateToolWindow function, and choosing a project compile type.


Add-In Essentials

All add-ins, regardless of what they do, require that you reference the items discussed in this topic. When you begin an add-in project, selecting the add-in template that comes with Visual Basic will provide a project containing code for all of the items listed in this topic. The template is described in this topic.

All add-ins you create with Visual Basic require the following:

  • Root Object The top-most, or "root" object in the extensibility model is the VBE object. The VBE object represents the Visual Basic environment. It hosts all of the extensibility objects and collections, which in turn have a number of properties, methods, and events which expose its functionality.

For More Information See "Visual Basic Instance Variable" in this chapter for information on using the VBE object.

  • lDTExtensibility Interface An interface named IDTExtensibility is provided to give you quick access to necessary events in the extensibility model. These interface methods are meant to be contained in a class module in your add-in. To expose the interface's methods, use the Implements statement in a class module's Declaration section. The syntax is:
    Implements IDTExtensibility

    The IDTExtensibility interface contains four methods for handling add-in events:

    • OnConnection

    • OnDisconnection

    • OnStartupComplete

    • OnAddInsUpdate

    Although these are methods to the IDTExtensibility interface, to you as a programmer, they act exactly like events, triggering when an event occurs.

    The IDTExtensibility interface speeds up the process of creating these four essential procedures and eliminates problems with add-ins due to entry errors in the parameter list, or other procedure syntax errors.

    When you click the Objects drop down box in the code window for the class module, you'll see IDTExtensibility. When you select IDTExtensibility, the four required add-in event procedures appear in the Procedures drop down box. Simply click the name of each procedure to add it to the class module.

    Since add-ins require that all members of an interface be implemented, the class module must contain all four procedures. You can add as many other procedures to the class module that you want, but you must have those four at a minimum for your add-in to work.

    Note One important caveat is that each of the four procedures must contain at least one line of code, such as a statement, procedure call, comment, and so on. If the procedure is empty, it will be removed by the compiler. If you don’t have any particular code you want to put in these procedures, just insert a comment.

For More Information Add-in events are also covered in Chapter 4, "Connecting and Exposing Add-Ins" in the section "Connecting or Disconnecting Add-Ins." Other events that you'll want to handle are covered in this chapter under the section "Responding to Events."

  • Visual Basic Instance Variable Each collection and object in the extensibility model requires a variable to store and reference the current instance of Visual Basic. An instance is a dynamic identification variable for the current session of Visual Basic. Since you can conceivably have more than one session of the Visual Basic IDE running at any given time, the instance variable differentiates one Visual Basic session from another.

    To declare a Visual Basic instance variable in the declarations section of a module or class module, you might enter:

    ' The variable VBInst is set to the current instance ' of Visual Basic.
Global gVBInst As VBIDE.VBE

For More Information "Creating a Basic Add-In," in Chapter 1, "Add-Ins Overview," steps you through creating a simple add-in and provides a brief illustration of the items presented here at work together.

Add-In Template

Visual Basic's add-in template contains the basic, necessary code that all add-ins require. To use the add-in template to create a new add-in project, choose Add-In in the New Project dialog box. This template contains the essentials:

  • A basic module which declares global variables, such as a Visual Basic instance variable.

  • A class module with pre-created IDTExtensibility interface procedures

  • An Add-In designer.

The template also includes these other useful features:

  • A CommandBar event handler

  • Rudimentary error-handling code

  • Code to access the add-in as a button on the Standard toolbar

The add-in template is a great starting point for your add-in projects.


Design Considerations for Add-Ins

Before creating an add-in, you should decide what the goal you want your add-in to accomplish and how you want it to work. Two factors that have an impact on this decision are usage of the CreateToolWindow function, and choosing a project compile type.

CreateToolWindow Function

While not a required add-in feature, the CreateToolWindow function is a feature you may want to consider using for many add-ins. When you create an add-in, any forms it uses are Visual Basic forms by default. While this is fine for most applications, some programmers may desire that the add-in window act as other windows in Visual Basic do and dock with other IDE windows. The way to create windows like this is to use ActiveX documents and the CreateToolWindow function.

A UserDocument window inside a ToolWindow creates an ActiveX document which can dock with existing windows and act like other windows in the Visual Basic IDE. Writing your add-ins to use CreateToolWindow will also aid you in migrating your add-ins to future versions of Visual Basic.

For an example of how CreateToolWindow is used in an add-in, see the following code fragment, taken from the TabOrder sample application (which is listed in the Samples directory):

Dim docTabOrderObject As Object  'user doc instance
' The guidMYTOOL$ constant is the unique registry
' identifier for your add-in.
Const guidMYTOOL$ = "{B7AFC8D0-EBE5-11cf-A497- _
   00A0C911E8B0}"
Set winWindow = _
gVBInstance.Windows.CreateToolWindow(gVBInstance. _
Addins("TabOrder.Connect"), "TabOrder.docTabOrder", _
LoadResString(10), guidMYTOOL$, docTabOrderObject)

The code above creates a Tool Window to hold an ActiveX document.

You can generate your own values for the guid string constant using a tool called Guidgen.exe, which is located in the \tools\idgen directory of Visual Basic.

Choosing a Project Type

Before getting too involved writing an add-in, you should decide how you want to compile the project.

In most situations, you’ll likely want to create and compile your add-ins as ActiveX DLL projects, which are in-process components. As in-process components, they provide better overall performance in Visual Basic.

You may want to compile your add-in as a .dll file if:

  • The add-in will always be used in conjunction with the Visual Basic IDE (or other programming environment that can use Visual Basic-created add-ins).

  • The add-in contains references to certain properties that are available only in Visual Basic.

  • The add-in must run as quickly as possible. ActiveX out-of-process components may perform more slowly than in-process components due to the time involved in marshaling across process boundaries.

You might, however, choose to compile an add-in as an ActiveX .exe file if you want the add-in to be able to run on its own — independent of Visual Basic — as well as in the Visual Basic environment.

When you use the Add-In template to create your add-in (which is recommended), your project type defaults to ActiveX EXE. You can change the project type on the General tab of the Project Properties dialog box.


Programming in the Extensibility Model

Once you have a framework in place for your add-in, you can begin to attach code to manipulate and/or extend Visual Basic. As mentioned in Chapter 2, "Extensibility Model Objects and Structure," the extensibility model can be broken down into a number of functional groups, each affecting a major portion of Visual Basic.

Note Many of the example code fragments below are included to demonstrate how the various collections and objects, and their properties, methods, and events, are used in a real-world program. As such, they may contain references to variables, controls, and such that aren't fully explained in this text. For example, many of the samples reference a variable named "vbi", which is a variable to reference the current instance of Visual Basic. (For more information on this, see "Visual Basic Instance Variable" in "Add-In Essentials".) The important thing to realize is that these examples should act as a basis for your own exploration of the extensibility model, not as complete, free-standing programs. To gain a good feel for the entire feature set, we recommend that you examine the VBIDE library in the Object Browser. We also urge you to examine the TabOrder (which is listed in the Samples directory) for more examples of programming add-ins.

The following topics cover most functional groups of objects in the extensibility model, and provide suggestions for using each group to achieve your goals:

  • Manipulating the IDE with Add-Ins These objects and collections allow you to select, view, and alter visual elements of the Visual Basic IDE, such as windows, CommandBars, and CodePanes.

  • Manipulating Projects with Add-Ins This set of objects allows you to monitor, select, and alter project elements in the Visual Basic IDE, which are typically displayed in the Project Explorer window.

  • Manipulating Forms and Controls with Add-Ins This set of objects gives you control over forms and the controls that reside on them.

  • Responding to Events with Add-Ins With Visual Basic, you're given a new, higher level of control over events; using this group of objects, you can respond to events that occur in the IDE itself, such as when a user clicks a form or project name in the Project Explorer window.

  • Manipulating Code with Add-Ins Using this set of objects, you can take control of source code in any object. Find out information about the code, such as what the starting or ending line is, how many lines of code exist in a module, or even delete or insert lines of code.

  • Manipulating Add-Ins The Add-Ins collection gives you information about other add-ins on the system. This allows you to make better decisions about how to run a particular add-in and make it respond differently, or you can use other existing add-ins for your own purposes.

Note When using C++ to develop add-ins, whenever you get a pointer from Visual Basic using Automation, you must call "release" when you are finished using it. Visual Basic follows the standard reference counting rules. For example, when you get a pointer out of Visual Basic by calling _NewEnum, you must call release through that pointer. Also, when you use pDispatch = m_ourMenuItems.Item(pszCaption), you must also use pDispatch->Release().


Manipulating the IDE with Add-Ins

Several objects in the extensibility object model allow you to manipulate and extend the Visual Basic IDE. The code in this topic illustrates a few examples.

Commandbars Collection and CommandBar Object

These allow you to manipulate command bars, which are a meld of toolbars and menu bars. You can do nearly anything to a CommandBar object, including creating new ones, deleting existing ones, and changing their size, location, and icons.

Here is an example of how to manipulate command bars:

Dim mcbMenuCommandBar As Office.CommandBarControl

' Make sure the Standard toolbar is visible.
gVBInstance.CommandBars("Standard").Visible = True
' Add as button to the Standard toolbar.
' This adds the button to the right of the Toolbox
' button.
Set mcbMenuCommandBar = _
gVBInstance.CommandBars("Standard").Controls.Add _
(1, , , gVBInstance.CommandBars("Standard") _
.Controls.Count)
' Set the caption of the button.
mcbMenuCommandBar.Caption = "My Test"
' Copy an icon to the clipboard.
Clipboard.SetData LoadPicture("c:\windows\circles.bmp")
' Paste the icon on the button.
mcbMenuCommandBar.PasteFace

' Handle the CommandBarEvents object.
Set Me.MenuHandler = _
gVBInstance.Events.CommandBarEvents _
(mcbMenucommandBar)

For More Information For additional information on using CommandBars, see Chapter 4, "Creating a Toolbar Button for Add-In Activation."

Windows Collection and Window Object

These allow you to create and delete windows, as well as move and size windows open in the IDE. All windows are contained in the Windows collection. This means that these windows are never really closed, but rather their visibility is altered with the Visible property.

Note that while you can use the Window object to manipulate a window frame itself, you cannot use it to manipulate the objects contained in a window (such as controls). To do that, you must reference the appropriate object.

The following code fragment demonstrates how to reference the Window object and Windows collection:

' Toggle window visibility. Assumes ordering of
' windows in lists matches ordering of windows in
' collection. A better method is to look up window by
' caption and type.
Private Sub cmdToggle_Click()
   Dim w As Window
   Dim sw As String

   sw = Combo1.Text
   If sw <> "" Then
      If sw = "MainWindow" Then
         Set w = vbi.MainWindow
      Else
         Set w = vbi.Windows(Combo1.ListIndex)
      End If
      w.Visible = Not w.Visible
      RefreshWindows
   End If
End Sub

LinkedWindows Collection

This contains all of the panes in a given LinkedWindowFrame. A "linked window" is defined as two or more windows which are docked together into a single, joint window surrounded by a frame known as a "LinkedWindowFrame." Each sub-window in the LinkedWindowFrame is known as a "pane". LinkedWindowFrames exist around all windows that can be linked or docked together, with the exception of code windows, designer windows, the Object Browser window, and the Search and Replace window.

If you remove all panes from a LinkedWindowFrame, it is deleted. (The sole exception is the Main window.)

Only windows of type LinkedWindowFrame have a LinkedWindows collection. For other window types, this collection is Nothing.

The code fragment below demonstrates how to reference the LinkedWindows collection:

' Unlink or undock window if it is linked or docked.
Private Sub cmdUnlinkWindow_Click()
   Dim w As Window
   Dim sw As String

   sw = Combo1.Text
   If sw = "" Then Exit Sub
   If sw = "MainWindow" Then
      Set w = vbi.Windows.MainWindow
   Else
      Set w = vbi.Windows(Combo1.ListIndex)
   End If
   If Not w.LinkedWindowFrame Is Nothing Then
      w.LinkedWindowFrame.LinkedWindows.Remove w
   End If
   RefreshWindows
End Sub

CodePane Object

This is a window provided by an object which visually displays the object's code. You cannot create or destroy CodePane objects in the IDE, but you can manipulate their height, width, and location. You can also insert or delete lines of code (using the InsertLines and DeleteLine methods), as well as select certain lines and make the highlight visible or not. You can use the GetSelection method to copy selected code into the Windows clipboard.

While the CodePane object allows you to visually examine or select existing code, it doesn't allow you to alter it. To do this, you must use the CodeModule object, as described in "Manipulating Code with Add-Ins" in this chapter.

The following code fragment demonstrates how to reference the CodePane object:

' Scroll codepane using scrollbar.
Private Sub sclCodePane_Change()
   Dim p As VBProject
   Dim c As VBComponent
   Dim cp As CodePane
   Dim sc As String
   Dim sp As String

   sp = cmbProj.Text
   sc = cmbComp.Text
   If sp <> "" And sc <> "" Then
      Set c = _
         vbi.VBProjects.Item(sp). _
         VBComponents.Item(sc)
      Set cp = c.CodeModule.CodePane
      cp.TopLine = sclCodePane.Value + 1
   End If
End Sub


Manipulating Projects with Add-Ins

This group of objects allow you to manipulate projects — that is, add or remove projects to or from an existing project group. Also, you can add or remove VBComponents to or from projects. A VBComponent is any object that can be added to a Visual Basic project, such as forms, controls, code modules, and so forth.

The following code fragments demonstrate how to reference the VBProject object and VBProjects collection, as well as other extensibility objects:

' Create new project.
Private Sub cmdCreateNew_Click()
   Dim p As VBProject
   Set p = vbi.VBProjects.Add _
      (cmbProjKind.ItemData(cmbProjKind.ListIndex))
   If txtProjName.Text <> "" Then
      p.Name = txtProjName.Text
   End If
End Sub

' Activate component.
Private Sub cmdActivate_Click()
   Dim sc As String
   Dim sp As String
   Dim c As VBComponent

   sp = cmbProj.Text
   sc = cmbComp.Text
   If sp <> "" And sc <> "" Then
      Set c = _
         vbi.VBProjects.Item(sp).VBComponents.Item(sc)
      c.Activate
   End If
End Sub

' Create new component of the type indicated in the
' cmbCompKind combo.
Private Sub cmdCreateNewVBComponent_Click()
   Dim p As VBProject
   Dim c As VBComponent
   Dim sp As String
   sp = cmbProj.Text
   If sp <> "" Then
      Set p = vbi.VBProjects.Item(sp)
      Set c = p.VBComponents.Add _
         (cmbCompKind.ItemData(cmbCompKind.ListIndex))
   End If
End Sub

' Get the text from the code module and display in text
' control.
Private Sub cmdGetText_Click()
   Dim i As Long
   Dim str As String
   Dim p As VBProject
   Dim c As VBComponent
   Dim sc As String
   Dim sp As String

   Screen.MousePointer = vbHourglass
   sp = cmbProj.Text
   sc = cmbComp.Text
   If sp <> "" And sc <> "" Then
      Set c = _
         vbi.VBProjects.Item(sp).VBComponents.Item(sc)
      txtDisplay.Text = ""  ' Clear the text control
      For i = 1 To c.CodeModule.CountOfLines
         str = str & c.CodeModule.Lines(i, 1) & CRLF
      Next i
      txtDisplay.Text = str
      SynchCodePaneScroll
   End If
   Screen.MousePointer = vbDefault
End Sub

' Refresh the list of projects. Called from events in
' the main object.
Public Sub RefreshProjects()
   Dim p As VBProject
   Dim tempIndex As Long

   Screen.MousePointer = vbHourglass
   If cmbProj.ListCount > 0 Then
      tempIndex = cmbProj.ListIndex
      ' Temp index to restore prior selection.
   End If
   cmbProj.Clear
   For Each p In vbi.VBProjects
      cmbProj.AddItem p.Name
   Next p
   ' Restore prior selection.
   If cmbProj.ListCount > 0 Then
      If tempIndex <= cmbProj.ListCount - 1 Then
      cmbProj.ListIndex = tempIndex
      Else
         cmbProj.ListIndex = 0
      End If
   End If
   Screen.MousePointer = vbDefault
End Sub


Manipulating Forms and Controls with Add-Ins

A designer is a base on which you create your visual interface. You can think of it in the extensibility model as an empty socket which you can plug in different designer types.

From its beginnings, Visual Basic used one kind of designer. Now, however, Visual Basic can host other kinds, including UserDocuments and UserControls that allow you to create ActiveX documents and ActiveX controls that can be used not only in Visual Basic, but in Microsoft Office applications such as Microsoft Word or Microsoft Excel.

The following code fragments demonstrate how to reference the VBForms object, as well as other extensibility objects:

' Add controls to form. Type of controls to add is
' based on the type of control selected in ComboBox.
Private Sub cmdAddControl_Click()
   Dim c As VBComponent
   Dim p As VBProject
   Dim vbc As VBControl
   Dim vbc2 As VBControl
   Dim vbf As VBForm
   Dim sc As String
   Dim sp As String
   Dim svbc As String
   Dim pid As String

   sp = cmbProj.Text
   sc = cmbComp.Text
   svbc = cmbControls.Text
   If sp <> "" And sc <> "" And svbc <> "" Then
      Set p = vbi.VBProjects.Item(sp)
      Set c = p.VBComponents.Item(sc)
      If c.Type = vbext_ct_VBForm Then
         Set vbf = c.Designer
         Set vbc = vbf.VBControls.Item(svbc)
         pid = vbc.ProgId
         Set vbc2 = vbf.VBControls.Add(pid)
      End If
   End If
End Sub

' Refresh the list of controls on a form. Sub is called
' from event handler in the main object when controls
' are added.
Public Sub RefreshControls()
   Dim c As VBComponent
   Dim p As VBProject
   Dim vbc As VBControl
   Dim vbf As VBForm
   Dim sc As String
   Dim sp As String
   Dim tempIndex As Long

   Screen.MousePointer = vbHourglass
   If cmbControls.ListCount > 0 Then
      tempIndex = cmbControls.ListIndex
      ' Temp index to restore prior selection
   End If
   sp = cmbProj.Text
   sc = cmbComp.Text
   If sc <> "" And sp <> "" Then
      cmbControls.Clear
      Set p = vbi.VBProjects.Item(sp)
      Set c = p.VBComponents.Item(sc)
      If c.Type = vbext_ct_VBForm Then
            c.Activate
            Set vbf = c.Designer
            For Each vbc In vbf.VBControls
               cmbControls.AddItem _
               vbc.Properties("name")
            Next vbc
         Else
            cmbControls.Text = "No Form Selected"
         End If
         ' Restore the prior selection.
         If cmbControls.ListCount > 0 Then
            If tempIndex <= cmbControls. _
            ListCount - 1 Then
               cmbControls.ListIndex = tempIndex
            Else
               cmbControls.ListIndex = 0
            End If
         End If
   End If
   Screen.MousePointer = vbDefault
End Sub

' Remove control from form
Private Sub cmdRemoveControl_Click()
   Dim c As VBComponent
   Dim p As VBProject
   Dim vbc As VBControl
   Dim vbf As VBForm
   Dim sc As String
   Dim sp As String
   Dim svbc As String

   sp = cmbProj.Text
   sc = cmbComp.Text
   svbc = cmbControls.Text
   If sp = "" Or sc = "" Or svbc = "" Then Exit Sub
   Set p = vbi.VBProjects.Item(sp)
   Set c = p.VBComponents.Item(sc)
   If c.Type = vbext_ct_VBForm Then
      Set vbf = c.Designer
      Set vbc = vbf.VBControls.Item(svbc)
      vbf.VBControls.Remove vbc
   End If
End Sub


Responding to Events with Add-Ins

As a Visual Basic programmer, you're accustomed to handling events such as a mouse move, a mouse click, the selection of an item in a ComboBox, and so on. In the Visual Basic version extensibility model, you are given an additional level of control — the ability to respond to events that occur in the IDE itself, such as when a user selects a project in the Project Explorer window or highlights code in a module. The Events object is your gateway to these events.

The Events object, which is referenced directly from the root VBE object, supplies methods that allow add-ins to connect to all events in the extensibility model. (Note that events can also occur in each object.)

The Events object contains the following objects:

  • CommandBarEvents

  • FileControlEvents

  • ReferencesEvents

  • SelectedVBControlsEvents

  • VBComponentsEvents

  • VBControlsEvents

  • VBProjectsEvents

Each of these objects allows you to respond to events that pertain to that object. For example, the CommandBarEvents object allows you to respond to events that occur to CommandBar objects, such as when a command bar is clicked. The SelectedVBControlsEvents objects allows you to respond to events that occur to any currently selected Visual Basic controls, and so on.

The WithEvents keyword exposes these events. For example, the VBComponentsEvents object has the ItemAdded event, which occurs when a component is added to a project, the ItemRemoved event, which occurs when a component is removed from a project, and so on.

Here's an example of how to declare such an object in a class module:

Private WithEvents evtVBProjects As VBProjectsEvents

In order to gain access to these events, however, the object must be referenced. Let's examine and reference one such object, the VBControlsEvents object, which is a property of the VBIDE.Events object. It has three events: ItemAdded, ItemRemoved, and ItemRenamed, and two parameters, VBProject, and VBForm.

In order to gain access to the ItemAdded, ItemRemoved, and ItemRenamed events, you must first reference the VBControlEvents object using the Set statement in the Declarations section of the main class module:

' Expose the Events.
Public WithEvents CtrlHandler As VBControlsEvents

' Gain access to the events by referencing the object.
Set Me.CtrlHandler = gVBInstance.Events.VBControlsEvents(Nothing, Nothing)

You now have programmatic access to the ItemAdded, ItemRemoved, and ItemRenamed events, and you now can build an add-in that can respond when a user adds, removes, or renames a control in a project. How it responds to these events is up to you and the code you place in the event procedures.

The arguments in the previous Set statement are set to Nothing. This means that we are not referring to a specific form in a project, but rather to any form in any project. You can consider the Events objects' arguments as a sort of filter which determines which events in a project, component, or control you're interested in monitoring. If you specify a particular project and form when setting an instance to a VBControlsEvents object, for example, control events are raised only for that particular form in that particular project. If you set both values to Nothing, however, all control events for all forms in all associated projects are raised. In other words, you are not filtering any of the control events.


Manipulating Code with Add-Ins

The Visual Basic extensibility model provides you with the ability to view and manipulate code in Visual Basic projects through the CodeModule object. Using the CodeModule object, you can programmatically add or remove code to or from a project, search through code, or alter it. You can browse code to find out how many total lines it has, or in a particular procedure, what the starting and ending lines are, and so on. You can also add, delete, or replace lines of code.

Important You should realize the distinction between the CodeModule and CodePane objects. The CodePane object only allows you to view code, but not alter it. The CodeModule object allows you to alter code, but not view it. The two objects work together to allow you to both view and alter code.

The Members object allows you to view and manipulate code and attributes for procedures in a given module. The attributes pertaining to each procedure change depending on the type of module and procedure you're viewing. To see the attributes of a procedure, choose Procedure Attributes from the Tools menu.

A designer is not an object per se; it's really a base on which you create your visual interface. You can think of it in the extensibility model as an empty socket which you can plug in different designers. For example, if you are using Visual Basic forms, whenever you create or edit a form, you use the Visual Basic forms designer. If you are creating or editing a UserDocument, you are using the UserDocument designer. When creating or editing a UserControl, you are using the UserControl designer. While designers are programmatically different from each other, though, they may or may not have any visual differences to the user.

The following code fragments demonstrate how to reference the CodeModule object:

' Clear the code module of all text
Private Sub cmdClearText_Click()
   Dim p As VBProject
   Dim c As VBComponent
   Dim sc As String
   Dim sp As String

   Screen.MousePointer = vbHourglass
   sp = cmbProj.Text
   sc = cmbComp.Text
   If sp <> "" And sc <> "" Then
      Set c = _
         vbi.VBProjects.Item(sp).VBComponents.Item(sc)
      c.CodeModule.DeleteLines 1, _
         c.CodeModule.CountOfLines
      SynchCodePaneScroll
   End If
   Screen.MousePointer = vbDefault
End Sub

' Insert text into code module.
Private Sub cmdInsertText_Click()
   Dim p As VBProject
   Dim c As VBComponent
   Dim sc As String
   Dim sp As String

   Screen.MousePointer = vbHourglass
   sp = cmbProj.Text
   sc = cmbComp.Text
   If sp <> "" And sc <> "" And txtDisplay.Text _
      <> "" Then
      Set c = _
         vbi.VBProjects.Item(sp).VBComponents.Item(sc)
      c.CodeModule.AddFromString txtDisplay.Text
      SynchCodePaneScroll
   End If
   Screen.MousePointer = vbDefault
End Sub

' Sync up the position of the scrollbar control with
' current CodePane position.
Private Sub SynchCodePaneScroll()
   vbi.VBProjects.Item(sp).VBComponents.Item(sc)
   Dim cp As CodePane
      Set cp = GetCodePane
      sclCodePane.Max = cp.CodeModule.CountOfLines
      sclCodePane.Value = cp.TopLine - 1
End Sub


Manipulating Add-Ins

The AddIns collection contains references to AddIn objects and is accessed directly from the root VBE object. Every add-in listed in the Add-In Manager dialog box in an instance of Visual Basic has an AddIn object in this collection. The AddIn object provides information about an add-in to other add-ins.

Using the AddIns collection and AddIn object, you can manipulate add-ins — connect and disconnect them programmatically, add or remove them with the Update method, and so forth.

For More Information For information on using the rest of the objects in the extensibility object model to create and use add-ins, see "Programming in the Extensibility Model."


Experimenting With the Extensibility Model

Once you become more familiar with the extensibility model, it can help to experiment with the various properties, methods and events contained in it. The following is one way to do this:

  1. Start Visual Basic and choose Addin as the project type in the New Project dialog.

  2. In the Visual Basic Project window, double click on the Connect class module to view its code.

  3. Use Find from the Edit menu to search for OnConnection. This should place the cursor in the IDTExtensibility_OnConnection procedure.

  4. There is a comment 3 or 4 lines into the procedure suggesting that the following statement is a good place to put a breakpoint for testing code. Place a breakpoint on the suggested line.

  5. Place your cursor in the Immediate window, type AddToIni, then press Enter to execute that procedure. (AddToIni is a procedure in the module Addin.Bas.)

  6. Press F5 to put the Addin into Run mode.

  7. Start another instance of Visual Basic. Choose the default (Standard Exe) from the initial dialog, then choose Add-In Manager from the Add-Ins menu.

  8. Check My Addin-In on the list of Available Add-Ins. Press OK in the Add-In Manager dialog. The IDTExtensibility_OnConnection is called in the first instance of Visual Basic, and execution is suspended at the breakpoint that you set in step #4.

  9. Use Step Into from the debug menu to execute the line:
    Debug.Print VBInst.FullName

    Notice that the path and name of the current instance of Visual Basic is printed in the Immediate window.

  10. You can now use VBInst as the object for the example code. Simply replace the dummy object Application.VBE with the Visual Basic object VBInst before executing the example lines in the Immediate window.

For example, you can modify the example:

 Print Application.VBE.VBProjects(1).VBComponents.Count

to read as follows:

 Print VBInst.VBProjects(1).VBComponents.Count

When you press Enter on the latter line in the Immediate window, the number of Visual Basic components is printed on the next line.

You now can experiment with various methods and properties in the Immediate window.


Wizards and the Wizard Manager

A Wizard is a tool which leads a user step-by-step through an unusually long, difficult, or complex programming task. If you want to create Wizards that look and act similarly to those used in Microsoft products, you can use the Wizard Manager.

First, let's clarify what the Wizard Manager does not do. It does not automatically create complete, functional Wizards. This makes sense, because it's up to you, the programmer, to decide what your Wizard will do. What it will do for you is create a framework of steps in your Wizard, and help you manage the order in which they will appear when the Wizard is run. You add your code to the framework to perform the actual tasks. The resulting Wizards look and operate like other Wizards.

How the Wizard Manager Works

Programmatically, a Wizard is a form which contains a variable number of frames (or "steps"), each of which comprises a step to completing a Wizard’s task. For example, one step of a Wizard might allow the user to choose a style for an output report, such as Classic, Modern, and so on, while the next screen might allow the user to decide how and where to display fields on the report.

The Wizard Manager uses only one form for the entire Wizard. This form (which must be named "frmWizard") has a visible area that is the same size as each step frame. When a step is presented, its Left property is set to that of the visible area of the form, making it visible to the user. The Left properties of steps not in use are set away from the visible area of the form. You can think of it like viewing a stack of pictures from a nearby table, where you place one picture at a time in front of you to view it, then return it to the stack on the table when you’re done. In similar fashion, the current step is moved to the visible area, then moved away to the "storage" area when you move to another step.

All About Steps

When you first start the Wizard Manager, you're provided you with a preset "stack" of steps as a starting point. These steps are: Introduction, Step x, and Finished!.

  • The Introduction step, which usually contains a graphic of some sort, is used for informing the user about the name and function of the Wizard, or any other information you want to impart.

  • The Step x steps are a set of four generic, template-like steps that you can replicate or delete as many times as needed. These steps form the bulk of the Wizard and are where questions are asked, input gathered, calculations made, properties changed, and so forth. Four identical generic steps are initially provided for your use. You may require more or less steps, depending on what you want your Wizard to do.

  • The Finished! step, which contains a picture of a checkered flag, is the last step of the Wizard and is normally displayed when all of the steps of the Wizard are complete, alerting the user that they've completed the task.

Each step provided by the Wizard Manager consists of three areas:

  • An Image control The Image control is located in the upper left corner of the step and is used to display a graphic for that step, if desired.

  • A Label control The Label control is located in the upper right corner of the step and is normally used to display instructions to the user about what to do in that step.

  • A TextBox control The TextBox control is located in the bottom half of the step and is normally used to display additional information to the user. Alternatively, it can be deleted and other controls placed there instead.


The Add-In Toolbar

While you can give users access to your add-in nearly anywhere in Visual Basic (this is covered in detail in Chapter 4), you might want to consider putting them on the Add-In toolbar.

The Add-In toolbar is provided with Visual Basic as a sort of "one-stop shopping" for all of your add-ins and Wizards. It's a site where you can place your add-ins and Wizards as buttons for easy access. Also, the add-in or Wizard is not loaded until a user clicks the button. It saves the user from the task of going into the Add-In Manager, clicking the box of an add-in that they want, then clicking OK. If you place your add-in on the Add-In toolbar, it appears as a button. To invoke and load the add-in, simply click its button. Very convenient!

Note You can place Wizards on the Add-In Toolbar as well as add-ins.

To start the Add-In toolbar, choose "VB Add-In Toolbar" in the Add-In Manager. You should see a small toolbar appear beneath the Standard toolbar:

The first button on the left (the "+/-" button) allows you to add or remove items from the toolbar. To add it, browse for your add-in, check its box in the Available Add-Ins list, then click OK. Your add-in (or Wizard) should appear on the Add-In toolbar. For more information on the Add-In toolbar dialog box, search the Visual Basic documentation.

Of course, you're also given programmatic control of the Add-In toolbar, since it's probably impractical to visit the computer of every person you give your add-in to make sure it's showing on their Add-In toolbar.

The Add-In toolbar object model has an object known as the Manager object. It contains two methods:

  • AddToAddInToolbar

  • RemoveAddInFromToolbar

These methods allow you to programmatically place and remove buttons to and from the Add-In toolbar.

This code is an example of how to programmatically add an add-in to the Add-In toolbar and ensure that the Add-In toolbar is automatically loaded the next time Visual Basic is started:

Sub Main()
   dim x as Object
   Set x=CreateObject("AddInToolbar.Manager")
      x.AddToAddInToolbar ("C:\VB\MyAdd.DLL", _
      "MyAddIn.Connect", "MyAddIn Title", True, True)
End Sub

This code is an example of how to remove an add-ins button from the Add-In toolbar:

Sub Main()
   dim x as Object
   Set x=CreateObject("AddInToolbar.Manager")
   x.RemoveAddInFromToolbar sAddInName:="MyAddIn Title"
End Sub

The registry location for the Add-In toolbar is (HKEY_CURRENT_USER\Software\Microsoft\VBA\Microsoft Visual Basic\AddInToolbar).


Connecting and Exposing Add-Ins

This chapter covers the essentials of what every add-in you build requires. It also gives you some methods and ideas for connecting and exposing your add-ins in the Microsoft Visual Basic environment.

Topics

Registering Add-Ins

Since add-ins are ActiveX components, they must be registered. This section discusses how this is done.
Referencing Add-Ins

In order for Visual Basic to recognize your add-in, you must set up a reference to it. This section discusses two methods for doing so.
Connecting or Disconnecting Add-Ins

Once your add-in is complete, it must be connected to Visual Basic in order to run. This can be accomplished manually or programmatically. This section discusses methods of doing this.
Exposing Add-Ins to the Visual Basic Environment

There are various ways that you can run your add-in once it is connected to Visual Basic. This section discusses various methods for triggering and running your add-in through the Visual Basic IDE and/or through events.
Add-In Troubleshooting

Even the most careful programmer can run into an occasional problem. This section covers the most commonly encountered problems and their solutions.


Registering Add-Ins

As an ActiveX component, an add-in must be properly registered in the system registry before it can be recognized or used by any Windows program, including Visual Basic. The process of compiling the add-in in Visual Basic automatically registers it on your system, but anyone who intends to use your add-in on another system must register it there before they can use it.

This means that if the add-in is an in-process component (.dll file), you (or your customer) must use a utility such as Regsvr32.exe to register it on their system. For ease of use to the customer, you would normally do this in the add-in’s setup program.

If the add-in is an out-of-process component (.exe file), running the .exe file registers it. If you’d rather register the add-in without running it, add the /regserver parameter to the .exe file’s command line, such as when you're creating a setup:

MyAddIn /regserver

When the add-in reference is written to the system registry, the registered name is the programmatic ID — comprised of the name of your add-in project (as shown in the Project Name box on the General tab of the Project Properties dialog box) plus the name of the class module that contains the add-in's connect and disconnect event-handling code.

For example, if you create an add-in using the Visual Basic default names — Project1 for the project name and Class1 for the class module name — then the registered name of the add-in will be Project1.Class1.


Adding an Add-In Reference to the Vbaddin.ini File

Referencing Add-Ins

Once an add-in is registered in the Windows system registry, you must set up a reference to it before it appears in the Add-In Manager and becomes useable to Visual Basic. There are two ways to do this:

  • Create an entry in the [Add-Ins32] section of the Vbaddin.ini file.

  • Create a reference with the Add-In designer.

Creating an Entry in the Vbaddin.ini File

You can create the entry any way you like, but the most often used programmatic method is through the Windows API function WritePrivateProfileString. Here is example code demonstrating how to do this (the function's arguments are described after the example):

Declare Function WritePrivateProfileString& Lib _
"kernel32" Alias "WritePrivateProfileStringA" _
(ByVal AppName$, ByVal KeyName$, ByVal _
keydefault$, ByVal FileName$)

Sub AddToINI()
Dim rc As Long
   rc = WritePrivateProfileString("Add-Ins32", _
   "MyAddInProject.MyAddInClass", "0", _
   "VBADDIN.INI")
   MsgBox "Add-in is now entered in VBADDIN.INI file."
End Sub

What Do the Arguments Mean?

The first argument of WritePrivateProfileString is the name of the section of the .ini file in which to place the information. In the case of add-ins, this should always be "Add-Ins32".

The second argument is the programmatic ID, which consists of the names of your project and class, separated by a dot. The project name is entered in the Properties dialog box in the Projects menu. The class name is entered in the Name property of the class module. (Note that the name of the module plays no part in this.)

The third argument is a value for the add-in’s entry in the Vbaddin.ini file — in this case, 0. Setting the entry to 1 means that the add-in will be loaded (that is, selected in the Add-In Manager) at IDE startup. Setting the entry to 0 means that it will not be loaded at IDE startup, but it will be included in the list of available add-ins in the Add-In Manager.

The fourth and final argument is the name of the file to apply the setting to. In the case of add-ins, this is the Vbaddin.ini file.

A More Friendly Name

While the name format of project.class (its programmatic ID) looks fine in the .ini file, it might be a bit confusing for users seeing it in the Add-In Manager’s add-in list. You can change this to a more friendly name.

To change the add-in name that appears in the Add-In Manager

  1. In Visual Basic, click the View menu, then click Object Browser.

  2. In the Classes list, right-click the name of the class which handles your add-in’s OnConnection and OnDisconnection events, then click Properties.

  3. In the Member Options dialog box, enter your friendly name in the Description box. Click OK.

The text you enter will be saved along with the add-in, so users will see the same description in their Add-In Manager once it’s installed on their system.

Creating a reference with the Add-In designer

Another (and much easier) way to create a reference to an add-in is with the Add-In designer. To do this:

  4. Create a new Add-In project (if you haven't done so already),

  5. Double-click the Add-In designer "Connect" in the Project window. This brings up the Add-In designer.

  6. In Display Name, enter the name of your add-in that will later appear in the Add-In Manager dialog box.

  7. In Add-In Description, enter a description of what the add-in does.

  8. In Application and Application Version, choose the host application and its version, in this case, Visual Basic version 6.0.

  9. In Initial Load Behavior, choose how your add-in loads:
    • Command Line/Startup: Add-in loads either when specifically invoked from a command-line parameter, or when Visual Basic starts.

    • Command Line: Add-in loads only when specifically invoked from a command-line parameter.

    • Startup: Add-in loads when you start Visual Basic.

    • None: Add-in doesn't load when you start Visual Basic.
  10. Check "Add-In is command-line safe (Does not put up any UI)" if you wish your add-in to run without displaying any dialog boxes boxes or forms—that is, run "behind the scenes" without any visible interface.

  11. Select the Advanced tab to enter additional information on localization resources, and registry settings, if applicable.

Once the above information is filled out and you compile your project, your new add-in will appear in the Add-In Manager's list of available add-ins.


Connecting or Disconnecting Add-Ins

Once an add-in is registered in the system registry and referenced in the Vbaddin.ini file, it can be connected to the IDE. The connection allows the add-in to respond to events and perform activities. You can connect or disconnect an add-in either manually or programmatically.

Handling the OnConnection and OnDisconnection Events

Whether you manually or programmatically connect an add-in, the add-ins IDTExtensibility_OnConnection event procedure is called. When you disconnect it, the IDTExtensibility_OnDisconnection event is called.

Private Sub IDTExtensibility_OnConnection(ByVal _
VBInst As Object, ByVal ConnectMode As _
VBIDE.vbext_ConnectMode, ByVal AddInInst As _
VBIDE.AddIn, custom() As Variant)
   MsgBox "Add-in is now connected"
End Sub

Private Sub IDTExtensibility_OnDisconnection(ByVal _
RemoveMode As VBIDE.vbext_RemoveMode, _
Custom () as Variant)
   MsgBox "Add-in is now disconnected"
End Sub

Private Sub IDTExtensibility_OnStartupComplete _
(custom() As Variant)
   ' Comment to prevent procedure from being
   ' deleted on compilation.
End Sub

Private Sub IDTExtensibility_OnAddInsUpdate _
(custom() As Variant)
   ' Comment to prevent procedure from being
   ' deleted on compilation.
End Sub

In this case, the only thing the program does is display a message box to notify you when the add-in was connected or disconnected. The last two event procedures are not used in this example, but must be present for the add-in to work correctly.

What Do the Parameters Mean?

The VBInst parameter in the OnConnection event procedure is an object reference to the current instance of Visual Basic. This object is the means through which you can access the Visual Basic extensibility object model.

The ConnectMode parameter notifies Visual Basic as to how the add-in was invoked. It has three possible settings:

Vbext_cm_AfterStartup | Add-in was connected after startup of development environment.
Vbext_cm_Startup | Add-in was connected on standard startup of development environment.
Vbext_cm_ExternalStartup | Add-in was connected by something other than the Visual Basic development environment.

The RemoveMode parameter in the OnDisconnection event procedure notifies Visual Basic as to how the add-in was disconnected.

Manually Connecting and Disconnecting Add-Ins

The primary (and simplest) way to connect an add-in to Visual Basic is to manually select its check box in the Add-In Manager, available from the Visual Basic Add-Ins menu. The primary way of disconnecting an add-in is to clear the check box.

Note that an add-in isn’t truly connected or disconnected until you click the Add-In Manager's OK button. When you select an add-in and then click OK, Visual Basic immediately attempts to create an instance of the add-in. If it can do so, the add-in becomes active. If for some reason Visual Basic cannot create an instance of the add-in, you’ll get an error message. (See “Add-In Troubleshooting” in this chapter for probable causes and solutions to this.) When you disconnect the add-in, the memory the add-in occupied is released.

This is important to remember. If you close only the visible portions of a an add-in — by double-clicking its system menu or by clicking its close button, for example — its forms disappear from the screen, but the add-in is still present in memory. While it is possible to write code in the form’s Unload event to release certain handles and pointers to free up some memory, the add-in object itself will always stay resident in memory until the add-in is disconnected through the Add-In Manager dialog box.

Programmatically Connecting and Disconnecting Add-Ins

Another way that you can connect a previously unconnected add-in — programmatically and behind the scenes — is to set the Connect property of the AddIns collection to True, then use the Update method on the Addins collection in the OnAddInsUpdate event procedure.

Changing the AddIns collection’s Connect property to True sets the add-in’s connection flag from 0 to 1 (meaning that it is connected at IDE startup). This change triggers the OnAddInsUpdate event, which occurs whenever changes are made to any add-in listed in the Add-In Manager (or Vbaddin.ini file entries). Using the Update method on the AddIns collection forces the Add-In Manager to re-read the list of add-in entries in the Vbaddin.ini file or in the Add-In designer. When this occurs, since the flag is now set to True, the add-in is connected.

In this following example, Visual Basic iterates through the Addins collection to check whether the given add-in’s programmatic ID matches a programmatic ID in the list of available add-ins. This is done only to certify that the index number of the add-in that will be passed to the Connect property actually exists. You cannot directly enter the add-in’s programmatic ID as a parameter for the AddIns collection’s Connect property. Once the programmatic ID is ascertained as valid, the add-in’s Connect property is set to 1 (connected).

Alternatively, you may elect to insert the number of the add-in, but this is not recommended because the add-in's number may change if it is stopped and re-started.

Note This example assumes that you’ve already set up the requisite variables for a basic add-in, such as a variable for the Visual Basic instance (here called "vbi"). These requisites are covered later in this chapter. You must also replace “proj.class” with the programmatic ID of the add-in you wish to activate.

Sub ConnectAddIn()
   For Each x In vbi.AddIns
   ' Go through each add-in in the collection.
      If x.ProgId = "proj.class" Then x.Connect = True
      ' If any of the progIDs match that of the given
      ' add-in, set its Connect flag to True.
   Next x
   ' Continue iterating through the collection.
End Sub

If you then perform an Update method on the AddIns collection, the collection is updated. Visual Basic then notices that the add-in is marked for connection, and connects it. To perform this update, add the following line of code to the OnAddInsUpdate event procedure in the class module:

vbi.AddIns.Update
' Perform an Update on the AddIns collection.

This method forces Visual Basic to re-read the Vbaddin.ini file or the Add-In designer list. Since the flag on the add-in you want to start has been set to 1, or connect, the add-in is connected.

To programmatically disconnect an add-in, follow the same procedure, only set the Connect property of the AddIns collection to False.


Exposing Add-Ins to the Visual Basic Environment

Once you’ve connected your add-in, you’ll likely want to expose it in the IDE. Of course, add-ins don’t necessarily have to be initiated from the IDE or even have a visible user interface. You can activate them behind the scenes through events and they can work unnoticed in the background. Most add-ins, however, are activated through either a menu command or toolbar button.

You must decide how the add-in will be initiated — by a menu command, a toolbar button, or by the occurrence of an event. Since chances are good that most add-ins you create will be visible in the development environment, you’ll want to be able to control when they’re active and when they’re not.

There are five ways to control an add-in’s operation:

  • Create a menu command for it.

  • Create a button for it on a toolbar (such as the Add-In toolbar).

  • Start or stop it with events.

  • Have the add-in present a non-modal form with which the user interacts.


Command Bars: A Replacement for Menus and Toolbars

Even though menus are differentiated from toolbar buttons because they look different from each other, to Visual Basic they are in essence the same kind of object. Internally, Visual Basic considers menus, menu commands, toolbars, and toolbar buttons as command bars.

A command bar unifies the concept of menus and toolbars into a single common visual and programmatic object. As command bars, menus contain menu commands which can have icons and captions and exhibit button-like behavior while remaining in the familiar menu format. Some toolbar buttons have dropdown arrows, similar to ComboBox controls. A command bar object can contain other command bar objects, depending on its type.

While a discussion of the command bar object model is beyond the scope of this book, here are a few basics to help you place your add-in where you want it.

There are three types of command bars:

Popup | A Popup command bar is equivalent to a menu item on a menu bar.
ComboBox | A ComboBox command bar is similar to a ComboBox control. That is, a toolbar button with a dropdown arrow next to it (like the Add Project toolbar button). When you click the arrow, it displays more menu commands with icons.
Button | A Button command bar is equivalent to a standard toolbar button. That is, a button with an icon displayed on it.

Getting to Command Bars Through Code

The concept of command bars is important to you really only as a programmer, since the interface acts more or less the same as it did in previous versions of Visual Basic. Programmatically, however, it is quite different. You can gain access to and explore the various command bar objects in the Object Browser by selecting the Microsoft Office 8.0 Object Library check box in the References dialog box.

Since menus and toolbars are in the same object library, they’re referred to through the CommandBarControl object. For example, to declare the basic object for use in your code, you would enter:

Dim mcbMenuCommandBar As Office.CommandBarControl

Every toolbar, menu, or context menu in the development environment is a command bar. It helps to understand command bars if you think of them as containers that hold other command bars. Therefore, a menu bar command bar can contain several menu items, each of which is itself a command bar, and each menu item can contain several menu commands, each of which is also a command bar.

Getting Your Bearings Among the Command Bars

This model makes placing your add-in in the development environment easy. You simply refer to the command bar in which you wish your add-in to appear. You refer to the command bars from top to bottom and left to right. In the example below, we expose our new add-in as a command on the Tools menu. Here’s an example of how to do this:

Set mcbMenuCommandBar = _
VBInst.CommandBars(1).Controls(8).CommandBar. _
Controls.Add(1, , , 3)

The code above installs a popup command bar before the third menu command from the top. What's in this code?

  • The first reference to command bars is CommandBars(1). (Note that because Item is the CommandBars collection’s default property, the code here is the same as if we used CommandBars.Item(1).) The first reference to command bars is the toolbar itself. The first toolbar is always the menu bar. So when we say CommandBars(1) we’re talking about the menu command bar.

  • The second reference is Controls(8). This means the eighth control from the left on that command bar, which is the Tools menu.

  • The next two references are CommandBar.Controls. This means that we’re affecting the Tools command bar.

  • The last reference is Add(1, , , 3). Using the Add method, we add a new command bar (in this case a menu command) to the Tools menu. You can view the parameters for the Add method by examining the CommandBarControls object in the Object Browser.

Working with the Model's Flexibility

Note, however, that command bar controls can be moved by users to any location within a menu or toolbar, which means you cannot rely on them to always remain in the same numeric position. Additionally, if you support localized versions of Visual Basic, you cannot rely on the command bar control captions to always remain the same.

However, there is a method that allows you to always obtain a specific menu item, regardless of its location. The name of the CommandBar object of the menu item is not localized; thus, you can always specify this name in the top-level CommandBars collection to refer to a menu item. For example, to obtain a reference object to the command bar representing the Add-Ins menu, you could use the following statement:

Set cmdBar = VBInst.CommandBars("Add-Ins")

This works regardless of the location of the menu item or the caption name. You obtain the CommandBar object of a particular menu item, then use its Controls collection to add an item to the menu.


Creating a Menu Command for Activating Add-Ins

In some cases, you may want to provide access to your add-in through a menu command. In this example, we’ll place a menu command for our new add-in on the Tools menu.

The following procedure is designed to build on example code presented in “Creating a Basic Add-In.”

To place a menu command for an add-in on the Tools menu

  1. Make sure that “Microsoft Office 8.0 Object Library” is selected in the References dialog box. This allows you access to the Office command bar objects.

  2. Add the following code to the General Declarations section of the class module:
    Public VBI As VBIDE.VBE
' VBI is assigned a pointer to the current IDE's
' VBA object which is later passed as a parameter
' to the OnConnection procedure. It's retained
' because you need it later for disconnecting the
' add-in. Other procedures may have a need for it
' as well.

Private mcbMenuCommandBarCtrl As _
Office.CommandBarControl
' This will be set to the new command bar control.

Private WithEvents MenuHandler As CommandBarEvents
' This is the event handling procedure for
' the click event of the new command bar control.
  3. The following procedure is called when the add-in is connected in the Add-In Manager. It adds a new menu command to the Tools menu called “My New Add-In.” In the IDTExtensibility_OnConnection procedure, remove the MsgBox line and add the following:
    Private Sub IDTExtensibility_OnConnection _
(ByVal VBInst As Object, _
ByVal ConnectMode As VBIDE.vbext_ConnectMode, _
ByVal AddInInst As VBIDE.AddIn, _
custom() As Variant)

' Save the current instance of Visual Basic.
Set VBI = VBInst
' Add a menu command to the Tools menu.
Set mcbMenuCommandBarCtrl = _
VBI.CommandBars("Tools").Controls.Add(before:=3)
' Place a separator bar before the new
' menu command.
mcbMenuCommandBarCtrl.BeginGroup = True
' Set the title for the add-in.
mcbMenuCommandBarCtrl.Caption = "My New Add-In"
' Copy an icon bitmap to the clipboard.
Clipboard.SetData _
LoadPicture("c:\windows\triangles.bmp")
' Copy the icon from the clipboard to the menu
' command's icon.
mcbMenuCommandBarCtrl.PasteFace
' Connect the event handler to receive the
' events for the new command bar control.
Set MenuHandler = _
VBI.Events.CommandBarEvents _
(mcbMenuCommandBarCtrl)
' Place a separator bar after the new
' menu command.
VBI.CommandBars("Tools").Controls(4).BeginGroup _
= True
End Sub
  4. The next procedure is called whenever the add-in is disconnected. The mcbMenuCommandBar.Delete line ensures that the menu command in the Tools menu is removed once the add-in is disconnected. Add the following lines to the IDTExtensibility_OnDisconnection procedure. (You can also remove the MsgBox line from the original code if you wish):
    Private Sub IDTExtensibility_OnDisconnection _
(ByVal VBInst As Object, ByVal LoadMode As _
Long, ByVal AddInInst As VBIDE.AddIn, custom() _
As Variant)
' Delete the new menu command from the Tools
' menu.
mcbMenuCommandBarCtrl.Delete
End Sub
  5. The MenuHandler_Click event is called whenever you click the new menu command on the Tools menu. The optional message box function is here only to signal that the Click event was correctly intercepted. Add the following procedure to the add-in’s class module by using the dropdown to insert the event handler:
    Private Sub MenuHandler_Click(ByVal _
CommandBarControl As Object, handled As Boolean, _
CancelDefault As Boolean)
MsgBox "You clicked the new menu command."
End Sub
  6. Save and then compile the project (as an ActiveX component).

  7. Start a new project and activate the add-in through the Add-In Manager.

  8. Look in the Tools menu. Notice that there is now a My New Add-In menu command.

  9. Click the new menu command. You should get a message box confirming that you clicked the menu command.

  10. Disconnect the add-in in the Add-In Manager. Notice that the menu command is removed from the Tools menu.

To summarize, you now have an add-in which:

  • Adds a command to the Tools menu when connected to Visual Basic.

  • Demonstrates that it handles an event when the menu command is clicked.

  • Removes the command from the Tools menu when disconnected.


Creating a Toolbar Button for Add-In Activation

As explained earlier, a toolbar is simply another form of command bar object. So to convert our example add-in from appearing as a menu entry to appearing as a button on a toolbar, make the following code changes:

' This line puts a command bar as a button on the
' toolbar before the 20th item, in this case, the
' form coordinates box.
Set mcbMenuCommandBarCtrl = _
VBInst.CommandBars("Standard").Controls.Add(1, , , 20)

If you want to put the add-in on the Add-In toolbar rather than the Standard toolbar, first activate the Add-In toolbar in the Add-In Manager, then change the reference name of the CommandBar object. The following code adds a button in the first position (leftmost side) of the Add-In toolbar.

' This line puts a command bar as a button on the
' Add-in toolbar.
Set mcbMenuCommandBarCtrl = _
VBInst.CommandBars("Add-Ins").Controls.Add(1)


Starting and Stopping an Add-In Using Events

What should you do if you want to control start and stop an add-in without user intervention?

The answer is to control it through events. Add-ins don’t require menu commands or toolbar buttons to activate them. Clicking a command bar object (such as a menu command or toolbar button) triggers the Click event of that object (in our example, the MenuHandler_Click event).

So to control an add-in without using a UI, simply program your own events in a class module.


Add-In Troubleshooting

If you get an error when creating or activating your add-in, check the following:

  • Make sure that the add-in was created as an ActiveX DLL or ActiveX EXE project.

  • Make sure the add-in component is registered on the machine on which you are going to use it

    For more information about registering your add-in, see "Registering Add-Ins" in this chapter.

  • Make sure that Microsoft Visual Basic Extensibility is selected in the References dialog box. (In Visual Basic, click the Project menu, then click References.) This is because the VBIDE collection and VBA object are part of that type library. If it’s not active, references to extensibility (add-in) objects cannot be resolved properly, and you’ll get errors at compile time.

  • If your add-in uses command bars, make sure that Microsoft Office 8.0 Object Library is selected in the References dialog box. This gives your program access to the command bar objects, collections, properties, and events.

  • Make sure that the project name written to the Vbaddin.ini file add-in entry exactly matches the name of your project.

    You can view or set the project name by clicking the Project menu, then clicking project name Properties. Enter the project name in the Project Name box.

  • Make sure that the Name property of your class module that exposes the IDTExtensibility interface matches the name written to the Vbaddin.ini file entry.

  • Make sure that the Visual Basic run-time DLL and any other necessary files are on file paths that are available to your add-in. (This shouldn’t be an issue if you run the Visual Basic setup program.)

  • Make sure that your add-in’s class module contains procedures for the OnConnection, OnDisconnection, OnStartupComplete, and OnAddInsUpdate events. Add-ins require that these four event procedures be present, even if they contain only a comment. The procedures must contain at least one line of code, otherwise the compiler will remove the empty procedures from the class module, and the add-in will not work correctly.


Accessing DLLs and the Windows API

When you need capabilities that go beyond the core language and controls provided with Microsoft Visual Basic, you can make direct calls to procedures contained in dynamic-link libraries (DLLs). By calling procedures in DLLs, you can access the thousands of procedures that form the backbone of the Microsoft Windows operating system, as well as routines written in other languages.

As their name suggests, DLLs are libraries of procedures that applications can link to and use at run time rather than link to statically at compile time. This means that the libraries can be updated independently of the application, and many applications can share a single DLL. Microsoft Windows itself is comprised of DLLs, and other applications call the procedures within these libraries to display windows and graphics, manage memory, or perform other tasks. These procedures are sometimes referred to as the Windows API, or application programming interface.

DLLs or Automation?

Another way to bring more power into Visual Basic is through Automation (formerly called OLE Automation). Using Automation is simpler than calling routines in a DLL, and it doesn't create the same level of risk that you'll hit when going straight to the Windows API. By using Automation, you can get programmatic access to a wide range of objects exposed by external applications.

For more information For additional information on Automation, see "Programming with Components" in the Programmer's Guide.

Topics

Using a DLL Procedure in Your Application

An introduction to calling a DLL procedure from within a Visual Basic application.
Accessing the Microsoft Windows API

How to use the API Viewer application to find procedures in the Win32api.txt file.
Declaring a DLL Procedure

An overview of syntax, clauses, and arguments that make up a Declare statement in Visual Basic.
Passing Strings to a DLL Procedure

How to work with String data types in DLL procedures.
Passing Arrays to a DLL Procedure

How to work with arrays in DLL procedures.
Passing User-Defined Types to a DLL Procedure

How to work with user-defined types (structures) in DLL procedures.
Passing Function Pointers to DLL Procedures and Type Libraries

An introduction to using function pointers, callback procedures, and subclassing in Visual Basic.
Passing Other Types of Information to a DLL Procedure

How to work with null values, properties, handles, and variants in DLL procedures.
Converting C Declarations to Visual Basic

A table of data type conversions between the Visual Basic and C languages.


Using a DLL Procedure in Your Application

Because DLL procedures reside in files that are external to your Visual Basic application, you must specify where the procedures are located and identify the arguments with which they should be called. You provide this information with the Declare statement. Once you have declared a DLL procedure, you can use it in your code just like a native Visual Basic procedure.

Important When you call any DLLs directly from Visual Basic, you lose the built-in safety features of the Visual Basic environment. This means that you increase the risk of system failure while testing or debugging your code. To minimize the risk, you need to pay close attention to how you declare DLL procedures, pass arguments, and specify types. In all cases, save your work frequently. Calling DLLs offers you exceptional power, but it can be less forgiving than other types of programming tasks.

In the following example, we'll show how to call a procedure from the Windows API. The function we'll call, SetWindowText, changes the caption on a form. While in practice, you would always change a caption by using Visual Basic's Caption property, this example offers a simple model of declaring and calling a procedure.

Declaring a DLL Procedure

The first step is to declare the procedure in the Declarations section of a module:

Private Declare Function SetWindowText Lib "user32" _
Alias "SetWindowTextA" (ByVal hwnd As Long, _
ByVal lpString As String) As Long

You can find the exact syntax for a procedure by using the API Viewer application, or by searching the Win32api.txt file. If you place the Declare in a Form or Class module, you must precede it with the Private keyword. You declare a DLL procedure only once per project; you can then call it any number of times.

For More Information For more information on declare statements, see the topic "Declaring a DLL Procedure" later in this chapter.

Calling a DLL Procedure

After the function is declared, you call it just as you would a standard Visual Basic function. Here, the procedure has been attached to the Form Load event:

Private Sub Form_Load()
   SetWindowText Form1.hWnd, "Welcome to VB"
End Sub

When this code is run, the function first uses the hWnd property to identify the window where you want to change the caption (Form1.hWnd), then changes the text of that caption to "Welcome to VB."

Remember that Visual Basic can't verify that you are passing correct values to a DLL procedure. If you pass incorrect values, the procedure may fail, which may cause your Visual Basic application to stop. You'll then have to reload and restart your application. Take care when experimenting with DLL procedures and save your work often.

Note Very few API calls recognize the default Variant data type. Your API calls will be much more robust if you declare variables of specific types and use Option Explicit.


Accessing the Microsoft Windows API

You can gain access to the Windows API (or other outside DLLs) by declaring the external procedures within your Visual Basic application. After you declare a procedure, you can use it like any other language feature in the product.

The most commonly used set of external procedures are those that make up Microsoft Windows itself. The Windows API contains thousands of functions, subs, types, and constants that you can declare and use in your projects. These procedures are written in the C language, however, so they must be declared before you can use them with Visual Basic. The declarations for DLL procedures can become fairly complex. While you can translate these yourself, the easiest way to access the Windows API is by using the predefined declares included with Visual Basic.

The file Win32api.txt, located in the \Winapi subdirectory of the main Visual Basic directory, contains declarations for many of the Windows API procedures commonly used in Visual Basic. To use a function, type, or other feature from this file, simply copy it to your Visual Basic module. You can view and copy procedures from Win32api.txt by using the API Viewer application, or by loading the file in any text editor.

Note The Windows API contains a vast amount of code. To find reference information on the procedures and other details included in this API set, refer to the Win32 SDK, included on the Microsoft Developer Network Library CD.

Using the API Viewer Application

The API Viewer application enables you to browse through the declares, constants, and types included in any text file or Microsoft Jet database. After you find the procedure you want, you can copy the code to the Clipboard and paste it into your Visual Basic application. You can add as many procedures as you want to your application.

The API Viewer application

To view an API file

  1. From the Add-Ins menu, open the Add-In Manager and load API Viewer.

  2. Click API Viewer from the Add-Ins menu.

  3. Open the text or database file you want to view.
    • To load a text file into the viewer, click File \ Load Text File and choose the file you want to view.

    • To load a database file, click File \ Load Database File.
  4. Select the type of item you want to view from the API Types list.

Note You can have the API Viewer automatically display the last file you viewed in it, when it is opened, by selecting View \ Load Last File.

To add procedures to your Visual Basic code

  1. Click the procedure you want to copy in the Available Items list.

  2. Click Add. The item appears in the Selected Items list.

  3. Indicate the scope of the item by clicking Public or Private in the Declare Scope group.

  4. To remove an entry from the Selected Items list box, click the item and click Remove.

  5. To remove all entries from the Selected Items list box, click Clear.

To copy the selected items to the clipboard

  1. Click Copy. All of the items in the Selected Items list will be copied.

  2. Open your Visual Basic project and go to the module in which you want to place the API information.

  3. Position the insertion point where you want to paste the declarations, constants, and/or types, and then choose Edit \ Paste.

Converting Text Files to Jet Database Files

To optimize speed, you can convert the Win32api.txt file into a Jet database file, because it is much faster to display the list when opening a database than when opening a text file.

To convert a text file to a jet database file

  1. Start the API Viewer application.

  2. Click File \ Load Text File and open the .txt file you want to convert.

  3. Click File \ Convert Text to Database.

  4. Choose the file name and location for your database file, then click OK.

Loading an API File Automatically from the Command Line

You can specify a text or database file on the command line for Apilod32.exe so that the file is automatically loaded when you start API Viewer. Use the following syntax to load the file you choose when you start the API Viewer application:

Apilod32.exe {/T|/D} filename

Argument | Description
/T | API Viewer will load the file as a text file. /T must be uppercase.
/D | API Viewer will load the file as a database file. /D must be uppercase.
Filename | The path of the file you want to open.

There must be a space between /T or /D and the filename argument. An error message will be displayed if the file is not found. If you specify a file that is not a database or text file, an error message will be displayed when you try to load the file.

Tip You can view a prompt that shows the parameters of the command line syntax by using a DOS window to navigate to the directory in which the API viewer application is installed, then typing apiload /?.

Viewing the Win32api.txt file with a Text Editor

You can also load the Win32api.txt file in a text editor, such as Microsoft Word or WordPad, to locate the procedures you want to use. Again, you just copy the procedures from the file to a Visual Basic module to use them in your application.

Tip Don't load the Win32api.txt file into a module. This is a large file, and it will consume a lot of memory in your application. You will generally use only a handful of declarations in your code, so selectively copying the declarations you need is much more efficient.

Using Procedures from Other Sources

If you are attempting to call a procedure in a DLL that is not part of the operating system, you must determine the proper declaration for it. The topic "Declaring a DLL Procedure" explains the syntax of the Declare statement in detail.

Note If you use Visual C++ (or a similar tool) to create DLLs that will be called by Visual Basic, use the __stdcall calling convention. Do not use the default calling convention (_cdecl).


Declaring a DLL Procedure

Even though Visual Basic provides a broad set of predefined declares in the Win32api.txt file, sooner or later you'll want to know how to write them yourself. You might want to access procedures from DLLs written in other languages, for example, or rewrite Visual Basic's predefined declares to fit your own requirements.

To declare a DLL procedure, you add a Declare statement to the Declarations section of the code window. If the procedure returns a value, write the declare as a Function:

Declare Function publicname Lib "libname" [Alias "alias"] [([[ByVal] variable [As type] [,[ByVal] variable [As type]]...])] As Type

If a procedure does not return a value, write the declare as a Sub:

Declare Sub publicname Lib "libname" [Alias "alias"] [([[ByVal] variable [As type] [,[ByVal] variable [As type]]...])]

DLL procedures declared in standard modules are public by default and can be called from anywhere in your application. DLL procedures declared in any other type of module are private to that module, and you must identify them as such by preceding the declaration with the Private keyword.

Procedure names are case-sensitive in 32-bit versions of Visual Basic. In previous, 16-bit versions, procedure names were not case-sensitive.

For More Information See "Declare Statement" in the Language Reference.

Specifying the Library

The Lib clause in the Declare statement tells Visual Basic where to find the .dll file that contains the procedure. When you're referencing one of the core Windows libraries (User32, Kernel32, or GDI32), you don't need to include the file name extension:

Declare Function GetTickCount Lib "kernel32" Alias _
"GetTickCount" () As Long

For other DLLs, the Lib clause is a file specification that can include a path:

Declare Function lzCopy Lib "c:\windows\lzexpand.dll" _
(ByVal S As Integer,    ByVal D As Integer) As Long

If you do not specify a path for libname, Visual Basic will search for the file in the following order:

  5. Directory containing the .exe file

  6. Current directory

  7. Windows system directory (often but not necessarily \Windows\System)

  8. Windows directory (not necessarily \Windows)

  9. Path environment variable

The following table lists the common operating environment library files.

Dynamic Link Library | Description
Advapi32.dll | Advanced API services library supporting numerous APIs including many security and Registry calls
Comdlg32.dll | Common dialog API library
Gdi32.dll | Graphics Device Interface API library
Kernel32.dll | Core Windows 32-bit base API support
Lz32.dll | 32-bit compression routines
Mpr.dll | Multiple Provider Router library
Netapi32.dll | 32-bit Network API library
Shell32.dll | 32-bit Shell API library
User32.dll | Library for user interface routines
Version.dll | Version library
Winmm.dll | Windows multimedia library
Winspool.drv | Print spooler interface that contains the print spooler API calls

Working with Windows API Procedures that Use Strings

When working with Windows API procedures that use strings, you'll need to add an Alias clause to your declare statements to specify the correct character set. Windows API functions that contain strings actually exist in two formats: ANSI and Unicode. In the Windows header files, therefore, you'll get both ANSI and Unicode versions of each function that contains a string.

For example, following are the two C-language descriptions for the SetWindowText function. You'll note that the first description defines the function as SetWindowTextA, where the trailing "A" identifies it as an ANSI function:

WINUSERAPI
BOOL
WINAPI
SetWindowTextA(
   HWND hWnd,
   LPCSTR lpString);

The second description defines it as SetWindowTextW, where the trailing "W" identifies it as a wide, or Unicode function:

WINUSERAPI
BOOL
WINAPI
SetWindowTextW(
   HWND hWnd,
   LPCWSTR lpString);

Because neither function is actually named "SetWindowText," you need to add an Alias clause to the declare to point to the function you want to reference:

Private Declare Function SetWindowText Lib "user32" _
Alias "SetWindowTextA" (ByVal hwnd As Long, ByVal _
lpString As String) As Long

Note that the string that follows the Alias clause must be the true, case-sensitive name of the procedure.

Important For API functions you use in Visual Basic, you should specify the ANSI version of a function, because Unicode versions are only supported by Windows NT — not Windows 95. Use the Unicode versions only if you can be certain that your applications will be run only on Windows NT-based systems.

Passing Arguments by Value or by Reference

By default, Visual Basic passes all arguments by reference. This means that instead of passing the actual value of the argument, Visual Basic passes a 32-bit address where the value is stored. Although you do not need to include the ByRef keyword in your Declare statements, you may want to do so to document how the data is passed.

Many DLL procedures expect an argument to be passed by value. This means they expect the actual value, instead of its memory location. If you pass an argument by reference to a procedure that expects an argument passed by value, the procedure receives incorrect data and fails to work properly.

To pass an argument by value, place the ByVal keyword in front of the argument declaration in the Declare statement. For example, the InvertRect procedure accepts its first argument by value and its second by reference:

Declare Function InvertRect Lib "user32" Alias _
"InvertRectA" (ByVal hdc As Long, _
lpRect As RECT) As Long

You can also use the ByVal keyword when you call the procedure.

Note When you're looking at DLL procedure documentation that uses C language syntax, remember that C passes all arguments except arrays by value.

String arguments are a special case. Passing a string by value means you are passing the address of the first data byte in the string; passing a string by reference means you are passing the memory address where another address is stored; the second address actually refers to the first data byte of the string. How you determine which approach to use is explained in the topic "Passing Strings to a DLL Procedure" later in this chapter.

Nonstandard Names

Occasionally, a DLL procedure has a name that is not a legal identifier. It might have an invalid character (such as a hyphen), or the name might be the same as a Visual Basic keyword (such as GetObject). When this is the case, use the Alias keyword to specify the illegal procedure name.

For example, some procedures in the operating environment DLLs begin with an underscore character. While you can use an underscore in a Visual Basic identifier, you cannot begin an identifier with an underscore. To use one of these procedures, you first declare the function with a legal name, then use the Alias clause to reference the procedure's real name:

Declare Function lopen Lib "kernel32" Alias "_lopen" _
(ByVal lpPathName As String, ByVal iReadWrite _
As Long) As Long

In this example, lopen becomes the name of the procedure referred to in your Visual Basic procedures. The name _lopen is the name recognized in the DLL.

You can also use the Alias clause to change a procedure name whenever it's convenient. If you do substitute your own names for procedures (such as using WinDir for GetWindowsDirectoryA), make sure that you thoroughly document the changes so that your code can be maintained at a later date.

Using Ordinal Numbers to Identify DLL Procedures

In addition to a name, all DLL procedures can be identified by an ordinal number that specifies the procedure in the DLL. Some DLLs do not include the names of their procedures and require you to use ordinal numbers when declaring the procedures they contain. Using an ordinal number consumes less memory in your finished application and is slightly faster than identifying a procedure in a DLL by name.

Important The ordinal number for a specific API will be different with different operating systems. For example, the ordinal value for GetWindowsDirectory is 432 under Win95, but changes to 338 under Window NT 4.0. In sum, if you expect your applications to be run under different operating systems, don't use ordinal numbers to identify API procedures. This approach can still be useful when used with procedures that are not APIs, or when used in applications that have a very controlled distribution.

To declare a DLL procedure by ordinal number, use the Alias clause with a string containing the number sign character (#) and the ordinal number of the procedure. For example, the ordinal number of the GetWindowsDirectory function has the value 432 in the Windows kernel; you can declare the DLL procedure as follows:

Declare Function GetWindowsDirectory Lib "kernel32" _
Alias "#432" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long

Notice that you could specify any valid name for the procedure in this case, because Visual Basic is using the ordinal number to find the procedure in the DLL.

To obtain the ordinal number of a procedure you want to declare, you can use a utility application, such as Dumpbin.exe, to examine the .dll file. (Dumpbin.exe is a utility included with Microsoft Visual C++.) By running Dumpbin on a .dll file, you can extract information such as a list of functions contained within the DLL, their ordinal numbers, and other information about the code.

For More Information For more information on running the Dumpbin utility, refer to the Microsoft Visual C++ documentation.

Flexible Argument Types

Some DLL procedures can accept more than one type of data for the same argument. If you need to pass more than one type of data, declare the argument with As Any to remove type restrictions.

For example, the third argument in the following declare (lppt As Any) could be passed as an array of POINT structures, or as a RECT structure, depending upon your needs:

Declare Function MapWindowPoints Lib "user32" Alias _
"MapWindowPoints" (ByVal hwndFrom As Long, _
ByVal hwndTo As Long, lppt As Any, _
ByVal cPoints As Long) As Long

While the As Any clause offers you flexibility, it also adds risk in that it turns off all type checking. Without type checking, you stand a greater chance of calling the procedure with the wrong type, which can result in a variety of problems, including application failure. Be sure to carefully check the types of all arguments when using As Any.

When you remove type restrictions, Visual Basic assumes the argument is passed by reference. Include ByVal in the actual call to the procedure to pass arguments by value. Strings are passed by value so that a pointer to the string is passed, rather than a pointer to a pointer. This is further discussed in the section "Passing Strings to a DLL Procedure."


Passing Strings to a DLL Procedure

In general, strings should be passed to APIs using ByVal. Visual Basic uses a String data type known as a BSTR, which is a data type defined by Automation (formerly called OLE Automation). A BSTR is comprised of a header, which includes information about the length of the string, and the string itself, which may include embedded nulls. A BSTR is passed as a pointer, so the DLL procedure is able to modify the string. (A pointer is a variable that contains the memory location of another variable, rather than the actual data.) BSTRs are Unicode, which means that each character takes two bytes. BSTRs typically end with a two-byte null character.

Figure 1.2 The BSTR type (each box represents two bytes)

The procedures in most DLLs (and in all procedures in the Windows API) recognize LPSTR types, which are pointers to standard null-terminated C strings (also called ASCIIZ strings). LPSTRs have no prefix. The following figure shows an LPSTR that points to an ASCIIZ string.

Figure 1.3 The LPSTR type

If a DLL procedure expects an LPSTR (a pointer to a null-terminated string) as an argument, pass the BSTR by value. Because a pointer to a BSTR is a pointer to the first data byte of a null-terminated string, it looks like an LPSTR to the DLL procedure.

For example, the sndPlaySound function accepts a string that names a digitized sound (.wav) file and plays that file.

Private Declare Function sndPlaySound Lib "winmm.dll" _
Alias "sndPlaySoundA" (ByVal lpszSoundName As String, _
ByVal uFlags As Long) As Long

Because the string argument for this procedure is declared with ByVal, Visual Basic passes a BSTR that points to the first data byte:

Dim SoundFile As String, ReturnLength As Long
SoundFile = Dir("c:\Windows\System\" & "*.wav")
Result = sndPlaySound(SoundFile, 1)

In general, use the ByVal keyword when passing string arguments to DLL procedures that expect LPSTR strings. If the DLL expects a pointer to an LPSTR string, pass the Visual Basic string by reference.

When passing binary data to a DLL procedure, pass a variable as an array of the Byte data type, instead of a String variable. Strings are assumed to contain characters, and binary data may not be properly read in external procedures if passed as a String variable.

If you declare a string variable without initializing it, and then pass it by value to a DLL, the string variable is passed as NULL, not as an empty string (""). To avoid confusion in your code, use the vbNullString constant to pass a NULL to an LPSTR argument.

Passing Strings to DLLs that Use Automation

Some DLLs may be written specifically to work with Automation data types like BSTR, using procedures supplied by Automation.

Because Visual Basic uses Automation data types as its own data types, Visual Basic arguments can be passed by reference to any DLL that expects Automation data types. Thus, if a DLL procedure expects a Visual Basic string as an argument, you do not need to declare the argument with the ByVal keyword, unless the procedure specifically needs the string passed by value.

Some DLL procedures may return strings to the calling procedure. A DLL function cannot return strings unless it is written specifically for use with Automation data types. If it is, the DLL probably supplies a type library that describes the procedures. Consult the documentation for that DLL.

For More Information For information on Automation data types, see the OLE 2 Programmer's Reference, published by Microsoft Press.

Procedures That Modify String Arguments

A DLL procedure can modify data in a string variable that it receives as an argument. However, if the changed data is longer than the original string, the procedure writes beyond the end of the string, probably corrupting other data.

You can avoid this problem by making the string argument long enough so that the DLL procedure can never write past the end of it. For example, the GetWindowsDirectory procedure returns the path for the Windows directory in its first argument:

Declare Function GetWindowsDirectory Lib "kernel32" _
Alias "GetWindowsDirectoryA" (ByVal lpBuffer As _
String, ByVal nSize As Long) As Long

A safe way to call this procedure is to first use the String function to set the returned argument to at least 255 characters by filling it with null (binary zero) characters:

Path = String(255, vbNullChar)
ReturnLength = GetWindowsDirectory(Path, Len(Path))
Path = Left(Path, ReturnLength)

Another solution is to define the string as fixed length:

Dim Path As String * 255
ReturnLength = GetWindowsDirectory(Path, Len(Path))

Both of these processes have the same result: They create a fixed-length string that can contain the longest possible string the procedure might return.

Note Windows API DLL procedures generally do not expect string buffers longer than 255 characters. While this is true for many other libraries, always consult the documentation for the procedure.

When the DLL procedure calls for a memory buffer, you can either use the appropriate data type, or use an array of the byte data type.


Passing Arrays to a DLL Procedure

You can pass individual elements of an array the same way you pass a variable of the same type. When you pass an individual element, it will be passed as the base type of the array. For example, you can use the sndPlaySound procedure to play a series of .wav files stored in an array:

Dim WaveFiles(10) As String
Dim i As Integer, worked As Integer
   For i = 0 to UBound(WaveFiles)
      worked = sndPlaySound(WaveFiles(i), 0)
   Next i

Sometimes you may want to pass an entire array to a DLL procedure. If the DLL procedure was written especially for Automation, then you may be able to pass an array to the procedure the same way you pass an array to a Visual Basic procedure: with empty parentheses. Because Visual Basic uses Automation data types, including SAFEARRAYs, the DLL must be written to accommodate Automation for it to accept Visual Basic array arguments. For further information, consult the documentation for the specific DLL.

If the DLL procedure doesn't accept Automation SAFEARRAYs directly, you can still pass an entire array if it is a numeric array. You pass an entire numeric array by passing the first element of the array by reference. This works because numeric array data is always laid out sequentially in memory. If you pass the first element of an array to a DLL procedure, that DLL then has access to all of the array's elements.

As an example, consider how you can use an API call to set tab stops within a text box There are internal tab stops in multiple-line (but not single-line) text box controls: If the text in the text box contains tab characters (character code 9), the text following the tab character is aligned at the next tab stop. You can set the position of these tab stops by calling the SendMessage function in the Windows API and passing an array that contains the new tab stop settings.

Private Declare Function SendMessageSetTabs Lib _
"user32" Alias "SendMessageA" (ByVal hwnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, _
lParam As Any) As Long
Const EM_SETTABSTOPS = &HCB

Sub ChangeTabs(anyText As TextBox, tabcount As Integer)
Dim i As Integer
Dim alngTabs() As Long
Dim lngRC As Long
ReDim alngTabs(tabcount - 1)
   For i = 0 To UBound(alngTabs)
      alngTabs(i) = (i + 1) * 96
      ' Set value to specify tabs in "dialog units."
   Next i
   ' Call with null pointer to empty existing
   ' tab stops.
   lngRC = SendMessageSetTabs(anyText.hwnd, _
   EM_SETTABSTOPS, 0, vbNullString)
   ' Pass first element in array; other elements
   ' follow it in memory.
   lngRC = SendMessageSetTabs(anyText.hwnd, _
   EM_SETTABSTOPS, tabcount, alngTabs(0))
   anyText.Refresh
End Sub

When you call this procedure, you specify the name of the text box and the number of tab stops you want to use for the indent. For example:

Private Sub Command1_Click()
   ChangeTabs Text1, 4
End Sub

This approach will also work for string arrays. A DLL procedure written in C treats a string array as an array of pointers to string data, which is the same way Visual Basic defines a string array.

For More Information For more information on SAFEARRAYs and other Automation data types, see the Microsoft Press book, OLE 2 Programmer's Reference.


Passing User-Defined Types to a DLL Procedure

Some DLL procedures take user-defined types as arguments. (User-defined types are referred to as "structures" in C and as "records" in Pascal.) As with arrays, you can pass the individual elements of a user-defined type the same way you would pass ordinary numeric or string variables.

You can pass an entire user-defined type as a single argument if you pass it by reference. User-defined types cannot be passed by value. Visual Basic passes the address of the first element, and the rest of the elements of a user-defined type are stored in memory following the first element. Depending on the operating system, there may also be some padding.

For example, several procedures in the operating environment DLLs accept a user-defined type for a rectangle, which has the following structure:

Type RECT
   Left As Long
   Top As Long
   Right As Long
   Bottom As Long
End Type

Two of the procedures that accept a rectangle are DrawFocusRect, which draws a dotted outline around the specified rectangle, and InvertRect, which inverts the colors of the specified rectangle. To use the procedures, place these declarations in the Declarations section of a standard module:

Declare Function DrawFocusRect Lib "User32" Alias _
"DrawFocusRect" (ByVal hdc As Long, _
lpRect As RECT) As Long

Declare Function InvertRect Lib "User32" Alias _
"InvertRect" (ByVal hdc As Long, _
lpRect As RECT) As Long

Dim MouseRect As RECT

Now you can use the following Sub procedures to call the DLLs:

Private Sub Form_MouseDown (Button As Integer, _
Shift As Integer, X As Single, Y As Single)
   ScaleMode = 3
   If Button And 1 Then
      MouseRect.Left = X
      MouseRect.Top = Y
      MouseRect.Right = X
      MouseRect.Bottom = Y
   End If
End Sub

Private Sub Form_MouseUp (Button As Integer, _
Shift As Integer, X As Single, Y As Single)
   ScaleMode = 3
   If Not (Button And 1) Then
      MouseRect.Right = X
      MouseRect.Bottom = Y
      InvertRect hDC, MouseRect
   End If
End Sub

Private Sub Form_MouseMove (Button As Integer, _
Shift As Integer, X As Single, Y As Single)
   ScaleMode = 3
   If Button And 1 Then
      DrawFocusRect hDC, MouseRect
      MouseRect.Right = X
      MouseRect.Bottom = Y
      DrawFocusRect hDC, MouseRect
   End If
End Sub

User-defined types can contain objects, arrays, and BSTR strings, although most DLL procedures that accept user-defined types do not expect them to contain string data. If the string elements are fixed-length strings, they look like null-terminated strings to the DLL and are stored in memory like any other value. Variable-length strings are incorporated in a user-defined type as pointers to string data. Four bytes are required for each variable-length string element.

Note When passing a user-defined type that contains binary data to a DLL procedure, store the binary data in a variable of an array of the Byte data type, instead of a String variable. Strings are assumed to contain characters, and binary data may not be properly read in external procedures if passed as a String variable.


Passing Function Pointers to DLL Procedures and Type Libraries

If you're familiar with the C programming language, function pointers may be familiar to you. If you're not, the concept merits some explanation. A function pointer is a convention that enables you to pass the address of a user-defined function as an argument to another function you've declared for use within your application. By using function pointers, you can now call functions like EnumWindows to list the open windows on the system, or EnumFontFamilies to catalog all of the current fonts. You can also use them to gain access to many other functions from the Win32 API that have not previously been supported in Visual Basic.

For Visual Basic, several limitations apply to the use of function pointers. For details, see "Limitations and Risks with Function Pointers" later in this topic.

Learning About Function Pointers

The use of function pointers is best illustrated with an example. To start, look at the EnumWindows function from the Win32 API:

Declare Function EnumWindows lib "user32" _
(ByVal lpEnumFunc as Long, _
ByVal lParam as Long ) As Long

EnumWindows is an enumeration function, which means that it can list the handle of every open window on your system. EnumWindows works by repeatedly calling the function you pass to its first argument (lpEnumFunc). Each time EnumWindows calls the function, EnumWindows passes it the handle of an open window.

When you call EnumWindows from your code, you pass a user-defined function to this first argument to handle the stream of values. For example, you might write a function to add the values to a list box, convert the hWnd values to window names, or take whatever action you choose.

To specify that you're passing a user-defined function as an argument, you precede the name of the function with the AddressOf keyword. Any suitable value can be passed to the second argument. For example, to pass the function MyProc as an argument, you might call the EnumWindows procedure as follows:

x = EnumWindows(AddressOf MyProc, 5)

The user-defined function you specify when you call the procedure is referred to as the callback function. Callback functions (or "callbacks," as they are commonly called) can perform any action you specify with the data supplied by the procedure.

A callback function must have a specific set of arguments, as determined by the API from which the callback is referenced. Refer to your API documentation for information on the necessary arguments and how to call them.

Using the AddressOf Keyword

Any code you write to call a function pointer from Visual Basic must be placed in a standard .BAS module — you can't put the code in a class module or attach it to a form. When you call a declared function using the AddressOf keyword, you should be aware of the following conditions:

  • AddressOf can only be used immediately preceding an argument in an argument list; that argument can be the name of a user-defined sub, function, or property.

  • The sub, function, or property you call with AddressOf must be in the same project as the related declarations and procedures.

  • You can only use AddressOf with user-defined subs, functions, or properties — you cannot use it with external functions declared with the Declare statement, or with functions referenced from type libraries.

  • You can pass a function pointer to an argument that is typed As Any or As Long in a declared Sub, Function, or user-defined type definition.

Note You can create your own call-back function prototypes in DLLs compiled with Visual C++ (or similar tools). To work with AddressOf, your prototype must use the __stdcall calling convention. The default calling convention (_cdecl) will not work with AddressOf.

Storing a Function Pointer in a Variable

At times, you may need to store a function pointer in an intermediate variable before passing it to the DLL. This is useful if you want to pass function pointers from one Visual Basic function to another. It's required if you are calling a function like RegisterClass, where you need to pass the pointer through an argument to a structure (WndClass), which contains a function pointer as one of its elements.

To assign a function pointer to an element in a structure, you write a wrapper function. For example, the following code creates the wrapper function FnPtrToLong, which can be used to put a function pointer in any structure:

Function FnPtrToLong (ByVal lngFnPtr As Long) As Long
   FnPtrToLong = lngFnPtr
End Function

To use the function, you first declare the type, then call FnPtrToLong. You pass AddressOf plus your callback function name for the second argument.

Dim mt as MyType
mt.MyPtr = FnPtrToLong(AddressOf MyCallBackFunction)

Subclassing

Subclassing is a technique that enables you to intercept Windows messages being sent to a form or control. By intercepting these messages, you can then write your own code to change or extend the behavior of the object. Subclassing can be complex, and a thorough discussion of it is beyond the scope of this book. The following example offers a brief illustration of the technique.

Important When Visual Basic is in break mode, you can't call vtable methods or AddressOf functions. As a safety mechanism, Visual Basic simply returns 0 to the caller of an AddressOf function without calling the function. In the case of subclassing, this means that 0 is returned to Windows from the WindowProc. Windows requires nonzero return values from many of its messages, so the constant 0 return may create a deadlock situation between Windows and the Visual Basic, forcing you to end the process.

This application consists of a simple form with two command buttons. The code is designed to intercept Windows messages being sent to the form and to print the values of those messages in the Immediate window.

The first part of the code consists of declarations for the API functions, constant values, and variables:

Declare Function CallWindowProc Lib "user32" Alias _
"CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
   ByVal hwnd As Long, ByVal Msg As Long, _
   ByVal wParam As Long, ByVal lParam As Long) As Long

Declare Function SetWindowLong Lib "user32" Alias _
"SetWindowLongA" (ByVal hwnd As Long, _
ByVal nIndex As Long, ByVal dwNewLong As Long) As Long

Public Const GWL_WNDPROC = -4
Global lpPrevWndProc As Long
Global gHW As Long

Next, two subroutines enable the code to hook into the stream of messages. The first procedure (Hook) calls the SetWindowLong function with the GWL_WNDPROC index to create a subclass of the window class that was used to create the window. It then uses the AddressOf keyword with a callback function (WindowProc) to intercept the messages and print their values in the Immediate window. The second procedure (Unhook) turns off subclassing by replacing the callback with the original Windows procedure.

Public Sub Hook()
   lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, _
   AddressOf WindowProc)
End Sub

Public Sub Unhook()
   Dim temp As Long
   temp = SetWindowLong(gHW, GWL_WNDPROC, _
   lpPrevWndProc)
End Sub

Function WindowProc(ByVal hw As Long, ByVal uMsg As _
Long, ByVal wParam As Long, ByVal lParam As Long) As _
Long
   Debug.Print "Message: "; hw, uMsg, wParam, lParam
   WindowProc = CallWindowProc(lpPrevWndProc, hw, _
   uMsg, wParam, lParam)
End Function

Finally, the code for the form sets the initial hWnd value, and the code for the buttons simply calls the two subroutines:

Private Sub Form_Load()
   gHW = Me.hwnd
End Sub

Private Sub Command1_Click()
   Hook
End Sub

Private Sub Command2_Click()
   Unhook
End Sub

Limitations and Risks with Function Pointers

Working with function pointers can be unforgiving. You lose the stability of Visual Basic's development environment any time you call a DLL, but when working with function pointers, it can be especially easy to cause the application to fail and to lose your work. Save often and back up your work as necessary. Following are notes on some areas that require special attention when working with function pointers:

  • Debugging. If your application fires a callback function while in break mode, the code will be executed, but any breaks or steps will be ignored. If the callback function generates an exception, you can catch it and return the current value. Resets are prohibited in break mode when a callback function is on the stack.

  • Thunks. Thunking is the way that Windows enables relocatable code. If you delete a callback function in break mode, its thunk is modified to return 0. This value will be correct most of the time — but not all of the time. If you delete a callback function in break mode and then type it again, it's possible that some callees will not know about the new address. Thunks aren't used in the .exe — the pointer is passed directly to the entry point.

  • Passing a function with the wrong signature. If you pass a callback function that takes a different number of arguments than the caller expects, or mistakenly calls an argument with ByRef or ByVal, your application may fail. Be careful to pass a function with the correct signature.

  • Passing a function to a Windows procedure that no longer exists. When subclassing a window, you pass a function pointer to Windows as the Windows procedure (WindowProc). When running your application in the IDE, however, it's possible that the WindowProc will be called after the underlying function has already been destroyed. This will likely cause a general protection fault and may bring down the Visual Basic development environment.

  • "Basic to Basic" function pointers are not supported. Pointers to Visual Basic functions cannot be passed within Visual Basic itself. Currently, only pointers from Visual Basic to a DLL function are supported.

  • Containing errors within a callback procedure. It is important that any errors within a callback procedure not be propagated back to the external procedure that initially called it. You can accomplish this by place the On Error Resume Next statement at the beginning of the callback procedure.


Passing Other Types of Information to a DLL Procedure

Visual Basic supports a wide range of data types, some of which may not be supported by the procedures in certain dynamic-link libraries. The following topic describes how to handle some of the special cases you may find when using Visual Basic variables with DLL procedures.

Passing Null Pointers

Some DLL procedures may sometimes expect to receive either a string or a null value as an argument. If you need to pass a null pointer to a string, declare the argument As String and pass the constant vbNullString.

For example, the FindWindow procedure can determine if another application is currently running on your system. It accepts two string arguments, one for the class name of the application, and another for the window title bar caption:

Declare Function FindWindow Lib "user32" Alias _
"FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long

Either of these arguments can be passed as a null value. Passing a zero-length string ("") does not work, however, as this passes a pointer to a zero-length string. The value of this pointer will not be zero. You instead need to pass an argument with the true value of zero. The easiest way to do this is by using the constant value vbNullString for the appropriate argument:

hWndExcel = FindWindow(vbNullString, "Microsoft Excel")

Another way to handle this situation is to rewrite the declare to substitute a Long data type for the argument that you want to pass as null, and then call that argument with the value 0&. For example:

Declare Function FindWindowWithNull Lib "user32" -
Alias "FindWindowA" (ByVal lpClassName As Long, _
ByVal lpWindowName As String) As Long

hWndExcel = FindWindow(0&, "Microsoft Excel")

Passing Properties

Properties must be passed by value. If an argument is declared with ByVal, you can pass the property directly. For example, you can determine the dimensions of the screen or printer in pixels with this procedure:

Declare Function GetDeviceCaps Lib "gdi32" Alias _
"GetDeviceCaps" (ByVal hdc As Long, _
ByVal nIndex As Long) As Long

You can also pass the hDC property of a form or the Printer object to this procedure to obtain the number of colors supported by the screen or the currently selected printer. For example:

Private Sub Form_Click ()
Const PLANES = 14, BITS = 12
   Print "Screen colors ";
   Print GetDeviceCaps(hDC, PLANES)* 2 ^ _
   GetDeviceCaps(hDC, BITS)
   Print "Printer colors ";
   Print GetDeviceCaps(Printer.hDC, PLANES) * _
   2 ^ GetDeviceCaps(Printer.hDC, BITS)
End Sub

To pass a property by reference, you must use an intermediate variable. For example, suppose you want to use the GetWindowsDirectory procedure to set the Path property of a file list box control. This example will not work:

ReturnLength = GetWindowsDirectory(File1.Path,_
Len(File1.Path))

Instead, use the following code to set the property:

Dim Temp As String, ReturnLength As Integer
Temp = String(255, 0)
ReturnLength = GetWindowsDirectory(Temp, Len(Temp))
Temp = Left(Temp, ReturnLength)
File1.Path = Temp

Use this technique with numeric properties if you want to pass them to DLL procedures that accept arguments by reference.

Using Handles with DLLs

A handle is a unique Long value defined by the operating environment. It is used to refer to objects such as forms or controls. The operating environment DLL procedures make extensive use of handles — handles to windows (hWnd), handles to device contexts (hDC), and so on. When a procedure takes a handle as an argument, always declare it as a ByVal Long. DLL functions that return a handle can be declared as Long functions. Handles are identifier (ID) numbers, not pointers or numeric values; never attempt mathematical operations on them.

The hWnd property of forms and nongraphical controls and the hDC property of forms and picture box controls supply valid handles that you can pass to DLL procedures. Like any other property passed to a DLL procedure, they can be passed only by value.

Passing Variants

Passing an argument of type Variant is similar to passing any other argument type, as long as the DLL procedure uses the Automation VARIANT data structure to access the argument data. To pass Variant data to a argument that is not a Variant type, pass the Variant data ByVal.


Converting C Declarations to Visual Basic

The procedures in DLLs are most commonly documented using C language syntax. To call these procedures from Visual Basic, you need to translate them into valid Declare statements and call them with the correct arguments.

As part of this translation, you must convert the C data types into Visual Basic data types and specify whether each argument should be called by value (ByVal) or implicitly, by reference (ByRef). The following table lists common C language data types and their Visual Basic equivalents for 32-bit versions of Windows.

C language data type | In Visual Basic declare as | Call with
ATOM | ByVal variable As Integer | An expression that evaluates to an Integer
BOOL | ByVal variable As Long | An expression that evaluates to a Long
BYTE | ByVal variable As Byte | An expression that evaluates to a Byte
CHAR | ByVal variable As Byte | An expression that evaluates to a Byte
COLORREF | ByVal variable As Long | An expression that evaluates to a Long
DWORD | ByVal variable As Long | An expression that evaluates to a Long
HWND, HDC, HMENU, etc. (Windows handles) | ByVal variable As Long | An expression that evaluates to a Long
INT, UINT | ByVal variable As Long | An expression that evaluates to a Long
LONG | ByVal variable As Long | An expression that evaluates to a Long
LPARAM | ByVal variable As Long | An expression that evaluates to a Long
LPDWORD | variable As Long | An expression that evaluates to a Long
LPINT, LPUINT | variable As Long | An expression that evaluates to a Long
LPRECT | variable As type | Any variable of that user-defined type
LPSTR, LPCSTR | ByVal variable As String | An expression that evaluates to a String
LPVOID | variable As Any | Any variable (use ByVal when passing a string)
LPWORD | variable As Integer | An expression that evaluates to an Integer
LRESULT | ByVal variable As Long | An expression that evaluates to a Long
NULL | As Any or ByVal variable As Long | ByVal Nothing or ByVal 0& or vbNullString
SHORT | ByVal variable As Integer | An expression that evaluates to an Integer
VOID | Sub procedure | Not applicable
WORD | ByVal variable As Integer | An expression that evaluates to an Integer
WPARAM | ByVal variable As Long | An expression that evaluates to a Long


Building Internet Applications

Visual Basic offers several possibilities for how you can move your applications onto the Internet or a corporate intranet.

Chapters

Introduction to Internet Applications

Introduces the developmental approach for creating applications for the Internet using Dynamic HTML and Internet Information Server technology.
Developing DHTML Applications

Explains how to create, test, and deploy client-based Internet applications using Dynamic HTML.
Developing IIS Applications

Explains how to create, test, and deploy server-based Internet applications using Internet Information Server.
Downloading ActiveX Components

Explains how to package and deploy ActiveX controls, components, and user documents on the Web, through Internet Component Download.


About Internet Applications

Introduction to Internet Applications

Microsoft® Visual Basic® lets you create applications for the Internet or a corporate intranet in several different ways. Using the new Internet programming features in Visual Basic, you can make use of your skills as a Visual Basic programmer to produce dynamic, powerful Internet applications with little effort. You can produce applications that run on the client or the server, present HTML pages inside a forms-based application, or use ActiveX components on the Web. In addition, you can quickly and easily set up your applications for Internet component download and deployment.

Topics

What is an Internet Application?

Introduces the concepts of Visual Basic Internet applications for the client and the server.
Understanding Internet Basics

Introduces basic Internet concepts, such as client/server interactions and HTML pages.
A History of Development on the Internet

Presents a brief overview of methods of developing applications for the Internet and explains how Visual Basic's Internet applications fit in.
Advantages of Visual Basic Internet Applications

Explains the unique advantages of creating Visual Basic Internet applications.
Security and Internet Applications

Discusses security concerns for Internet applications.
System Requirements for Internet Applications

Explains what must be present on the development and end-user computers for Visual Basic Internet applications.
Other Internet Technologies in Visual Basic

Explains how to use ActiveX components on the Internet.


What is an Internet Application?

Simply put, an Internet application is an interactive, compiled application that can be accessed through a corporate intranet or through the Internet. Internet applications can perform complex business processes on either the client or the server. In a server-based Internet application, the application uses the HTTP Internet protocol to receive requests from a client, typically a Web browser, process associated code, and return data to the browser.

In Visual Basic Internet programming, you can add active content to Web pages with little effort. Visual Basic Internet applications link Visual Basic code to one or more HTML pages and handle events raised in those pages by interacting with programs on either a client or a server. There are two types of Internet applications in Visual Basic: server-based IIS applications and client-based DHTML applications.

The user interface in a Visual Basic Internet application can be a series of HTML pages, a mix of HTML pages and Visual Basic forms, or a Visual Basic form that makes use of special components such as the WebBrowser control to take advantage of some of the power of the Internet. Regardless of the type of user interface, the application handles events, calls methods, and sets and retrieves properties based on elements in the HTML page.

The Web pages that make up the user interface for Visual Basic Internet applications are generally produced by a Web designer, rather than a developer. If you prefer to create your own HTML pages you can, but you do not have to. Instead, you can focus on your talents as a Visual Basic programmer by writing Visual Basic code that interacts with and uses HTML. Using your skills as a developer, you can easily produce dynamic, powerful applications for the Web.

For example, using Visual Basic Internet technology, you might create an application that can be used entirely on the browser to let salespeople track their status on the road, then link up to the central server when they return to the office. Or, you might create a sophisticated database-driven system that presents a catalog of selections to your end users, through their browser. Users could choose products from the catalog and your application would run associated Visual Basic code to retrieve product detail from a database recordset and send that information to the user.

For More Information More about Internet protocols is available on the World Wide Web.


Understanding Internet Basics

You can program for the Web, using your skills as a Visual Basic programmer, no matter what your level of experience with Internet technology. If you are new to the Internet or unfamiliar with its technology, Visual Basic allows you to quickly and easily produce functional applications. If you are more experienced with Internet technology, you can work at a more advanced level.

From one perspective, Internet technology simply provides another area for your development efforts. When you deploy Internet applications on the Web, you may go about it differently — incorporating HTML pages with your Visual Basic code, providing security features, and so on — but you're still calling methods, setting properties, and handling events. In this way, all of your knowledge as a Visual Basic developer can be carried into the Internet arena.

From another perspective, applying Internet technology enables you to extend your development skills in exciting new ways. For example, writing Visual Basic code that manipulates HTML pages allows you to decrease deployment costs, reduce client maintenance problems, and reach the broad audience of the Internet.

Internet Clients and Servers

A common way to think about Internet development is in terms of client/server relationships. In this case, the client is the browser, and the server is the Web server. Most interactions on the Internet or an intranet can be thought of in terms of requests and responses. The browser makes a request to the Web server (usually to display a page the user wants to see) and the Web server returns a response (usually an HTML page, an element, or an image) to the browser.

Internet vs. Intranet

The Internet encompasses two categories: the Internet and the intranet. The Internet is a global, distributed network of computers operating on a protocol called TCP/IP. An intranet is also a network of computers operating on the TCP/IP protocol, but it is not global. Generally, intranets are restricted to a particular set of users and are not accessible by the outside world. For example, many corporations use a corporate intranet to provide information to their employees, and run another Internet site for external users. Users within the company can access both the intranet sites and the Internet, but users outside the company can access only the company's Internet sites.

HTML Pages

HTML (HyperText Markup Language) is a language that allows you to display documents in a Web browser. You use HTML to create .htm files that are displayed in a browser. When you create an Internet application in Visual Basic, your user interface is usually made up of HTML pages rather than forms. In many ways, an .htm file (which allows you to display HTML pages) is similar to a Visual Basic .frm file (which allows you to display a Visual Basic form).

Note While the user interface is generally made up of HTML pages, it can also contain a mix of Visual Basic forms and HTML pages.

An .htm file is a text document that contains a series of tags that tell the browser how to display the file. These HTML tags supply information about the page's structure, appearance, and content. The following figure shows the relationship between page in the browser and its HTML tags:

HTML Page and Source HTML

In addition to describing the structural relationships among page elements, some HTML tags also contain attributes. Attributes provide details about a particular tag. For example, the tag that inserts an image onto a page contains an attribute that specifies the name of the file to insert. The tag is shown below.

HTML Tags and Attributes

Internet Object Models

You use the concepts of object-oriented programming in your Visual Basic Internet applications just as you do in forms-based Visual Basic applications. In Visual Basic Internet applications, you use Internet-related object models to access and manipulate information and controls on your HTML pages.

There are two types of Visual Basic Internet applications: IIS applications and DHTML applications. In IIS applications, you make use of the Active Server Pages (ASP) object model to retrieve information from the user, send information to the browser, and maintain information about the current session. In DHTML applications, you use the Dynamic HTML (DHTML) object model to manipulate the elements on an HTML page.

The important point to remember is that you access the information on your HTML pages through objects, regardless of whether the objects themselves are ASP or DHTML. The object models are explained in much greater detail in the chapters describing each type of application.

For More Information See "A History of Development on the Internet" for more information on the differences between IIS and DHTML applications. See the "Developing DHTML Applications" chapter for more information on using Dynamic HTML objects. See the "Developing IIS Applications" chapter for more information on using ASP objects. See the MSDN™ Web site at http://www.microsoft.com/msdn for details on using HTML and Internet technologies.


A History of Development on the Internet

Visual Basic programming technology represents a new set of approaches to Web technology, focused on the Visual Basic developer. This section offers a brief history of the Web's evolution from linked static content to the dynamic, compiled environment of Visual Basic Internet applications.

Static Content

The first applications on the Internet consisted of static pages that delivered their content to the browser and did not react dynamically to any actions the user performed. Although this model provides ready access to nicely formatted pages of information for your employees or potential customers, it provides limited interaction between the user and the Web server — and the static pages have to be manually edited to update their content.

Dynamic Content through Gateway Programming

The development of gateway interfaces such as Common Gateway Interface (CGI), Internet Server Application Programming Interface (ISAPI), and others allowed users to add dynamic content to the Web. With dynamic content, a browser can send a request for information. The server, instead of returning a static page, runs a script or application and returns HTML that reflects up-to-date, accurate information.

The disadvantage of gateway programs is that they are difficult to create and change. Gateway programs are not integrated into HTML files; in fact, they require an entirely different design process than do HTML files. In addition, all processing in a gateway program happens entirely on the server. This can increase server load and lead to backlogs in processing.

Scripting

Scripting enables dynamic content by embedding executable scripts directly in an HTML page. Rather than querying the server for an executable, the browser can process scripts as it loads the HTML page. These scripts can be processed on either the client or the Web server. The most common languages for client-side scripting are VBScript and JavaScript. A common framework used for server-side scripting is called Active Server Pages, or ASP.

In the ASP scripting model, HTML development and scripting development are part of the same process. This translates into tangible benefits, enabling Web providers to provide interactive business applications rather than merely publishing content. For example, a travel agency can go beyond just publishing flight schedules; it can use ASP scripting to enable customers to check available flights, compare fares, and reserve a seat on a flight.

With ASP scripting, you can use any scripting language for which an appropriate scripting engine is available. ASP supplies scripting engines for Microsoft Visual Basic Scripting Edition (VBScript) and Microsoft JScript. You can incorporate sophisticated functionality using ActiveX components to process data and generate useful information.

Visual Basic Internet Applications

Visual Basic Internet technology takes the process one step further by allowing you to link standard Visual Basic code to a user interface of HTML pages. This provides an extremely powerful mechanism for associating programming functionality with your Web pages, while allowing you to create dynamic, interactive content. With Visual Basic Internet technology, you can perform complex business operations while avoiding some of the intricacies of gateway programming or scripting. You can also enjoy all the benefits of working in Visual Basic, such as using class modules, controls, and designers, and debugging your code.

There are two main types of Visual Basic Internet applications: IIS applications and DHTML applications:

  • IIS applications, named for Microsoft Internet Information Server, reside on a Web server and are used to process incoming requests from a browser. IIS applications process the requests, run associated Visual Basic code, and return responses to the user. All processing for an IIS application happens on the server.

  • DHTML applications, named for Dynamic HTML, let you write Visual Basic code that handles events on any element of an HTML page. Most processing in a DHTML application can happen on the browser computer, although the application can make calls to the server if necessary.

DHTML applications require the end user to have Microsoft Internet Explorer version 4.0 or later, while IIS applications are browser- and operating system—independent to the end user. Because of this, IIS applications are the appropriate choice when you want to develop an application that reaches a broad audience. DHTML applications may be the more appropriate choice for intranet applications or applications that need to support remote or offline usage.

There are also other types of applications you can create in Visual Basic that make use of the Internet. For example, you can create ActiveX components that can be used on the Internet. These include ActiveX controls that can be used on Web pages, ActiveX documents that can run in a browser, and ActiveX code components that can be deployed on a Web server for behind-the-scenes processing. In addition, you can use the WebBrowser control to use HTML pages within a forms-based application.

For More Information See "Other Internet Technologies in Visual Basic" for more information on incorporating ActiveX components into your Internet applications. For information on using the WebBrowser control, see the Internet Client SDK on the MSDN Library CD, under the heading "SDK Documentation."


Advantages of Visual Basic Internet Applications

There are many ways to develop applications for the Internet. Prior to Visual Basic 6.0, developers relied on CGI programs, on ActiveX functionality, or on Active Server Pages to create dynamic, interactive Internet applications. Internet application development in Visual Basic offers several key advantages over those approaches:

  • You can greatly reduce the cost of deployment per user. End users of an IIS Internet application can run the application using only a browser; no special software aside from the appropriate browser needs to be installed on their computers. End users of a DHTML Internet application need only the appropriate browser and the Visual Basic run-time on their computers. These necessary pieces are installed automatically.

  • You can leverage your knowledge of Visual Basic and use the Visual Basic programming environment. You do not have to learn scripting or manipulate HTML tags to develop a highly functional Web-based application.

  • You can separate designing the user interface from writing the code for a form or page. In previous Web-based applications, developers had to insert their script directly into an HTML document that was also used to generate the user interface. The end result was code that could be difficult to read and maintain.

  • In IIS applications, you can reuse pages in different contexts. Unlike HTML, where information to move from page to page must be embedded in the HTML itself, navigation information for a page in a Visual Basic Internet application is stored separately from the page itself. This allows you to reuse the page in several places in your application, changing the navigation each time depending on the context.

  • You can lessen download time and therefore reduce your network load, because the system does not need to download large components to run the application.

  • You can easily maintain application state, such as the user's name or account number, between client requests. Depending on the type of application, you can manage state on the client, the server, or both.

  • You can debug your IIS and DHTML applications using Visual Basic's standard debugging tools. Script and CGI-based Internet applications, on the other hand, can be very difficult to debug.


Security Implications in Internet Applications

Security and Internet Applications

This section briefly introduces security issues for your Visual Basic Internet applications.

Security for DHTML Applications

DHTML applications are affected by security in the following ways:

  • You may encounter zone security in Internet Explorer 4.x when you attempt to download your DHTML application to a client. For an explanation of security zones, see the Internet Client SDK, available on your MSDN CD.

  • When you deploy your DHTML application, you must make sure the deployment files are properly signed and licensed. For more information, see the "Downloading ActiveX Components" chapter.

Security for IIS Applications

IIS applications face the same restrictions listed for DHTML applications in the section above. In addition, IIS applications have additional security options based on the fact that all IIS applications use an Active Server Page (ASP) as the entry point into the application. You can secure an ASP page in several ways:

  • You can set permissions on the virtual directory on your Web server that contains the ASP page.

  • You can set file access permissions, if you are using the Windows NT File system (NTFS) to determine which users can access the ASP page.

  • You can use client certificates through two protocols known as Secure Sockets Layer (SSL) 3.0 or through Private Communications Technology (PCT). A client certificate is an encrypted number the browser sends to the server when it requests the ASP page.

For More Information See "An Introduction to Webclasses" in "Developing IIS Applications" for more information on the relationship between IIS applications and ASP pages.


System Requirements for Internet Applications

System requirements vary depending on whether you're developing DHTML applications or IIS applications.

DHTML Application Requirements

System requirements for DHTML applications vary for the development computer and the end-user's computer.

The Development Computer

In order to create and test client intranet applications, you must have Internet Explorer 4.01 Service Pack 1 or later installed on your computer. In addition, you may want the HTML editor of your choice and other programs you use to create Web pages, such as image editors, and any supplemental programs your DHTML application will use, such as database programs.

The End-User Computer

End-users of a DHTML application must have Internet Explorer 4.0 or later. The first time end users download a DHTML application, they must download the Visual Basic run-time if it is not already on their computer. For subsequent applications, they do not need to download this file again. In addition, if the application uses ActiveX components, the end user must download those as well. You should be certain to include all necessary components in your download files.

IIS Application Requirements

System requirements for IIS Applications vary for the development computer, the deployment computer, and the end-user's computer.

The Development Computer

To create and test IIS applications, you need to have Internet Explorer 4.0 and a Web server installed on your development computer that is capable of running Active Server Pages. You can use any of the combinations in the table below for development purposes:

Operating System | Web Server
Windows NT Server® 4.0 or later | Internet Information Server 3.0 or later, with Active Server Pages
Windows NT Workstation 4.0 or later | Peer Web Services 3.0 or later, with Active Server Pages
Windows 95 or later | Personal Web Server 3.0 or later, with Active Server Pages

Important If you are using Windows NT Server or Windows NT Workstation, you must have Service Pack 3.0 installed.

The Deployment Computer

For deployment purposes, you must have Microsoft Internet Information Server, along with any supplemental system such as database applications that are needed by your application.

The End-User Computer

End users of an IIS application must have a browser installed. Although Internet Explorer 4.0 or later is recommended, any browser can act as the front end of an IIS application.

For More Information See the "Developing DHTML Applications" chapter for more information on creating, testing, and deploying intranet applications. See the "Developing IIS Applications" chapter for more information on creating, testing, and deploying IIS applications.


Using ActiveX Components on the Internet

Other Internet Technologies in Visual Basic

In addition to DHTML applications and IIS applications, there are other types of Internet functionality you can add to your projects in Visual Basic, including:

  • ActiveX components that can be used on HTML pages, either as part of an IIS or DHTML application or not. ActiveX components include controls, code components, and ActiveX documents. Using some or all of these solutions, you can create highly functional Internet solutions for your business needs.

  • Internet extensions for Visual Basic applications. You can use special controls that provide some Internet-related functionality. These include the Internet Transfer control, the WebBrowser control, and the WinSock Control.

ActiveX Controls

ActiveX controls enliven and add power to your HTML pages. In addition to creating your own controls, you can use the Internet controls provided with the Professional and Enterprise editions of Visual Basic to extend standard applications with Internet technology. For example, you can use ActiveX controls on a Web page to add customized menus to your Web pages, add a scrolling banner you can use to present advertisements or important announcements, or add animated effects.

Using HTML and VBScript, you can include ActiveX controls on an HTML page, set their properties, call their methods, and handle their events.

For More Information See the "Building ActiveX Controls" chapter in the "Creating ActiveX Components" book of the Component Tools Guide for information on developing ActiveX controls for your Visual Basic applications.

ActiveX Code Components

You can use ActiveX code components (.dll or .exe files) to add functionality to an HTML page on either the client or server. Code components on the client can offer an increase in speed, since users' commands do not need to be routed back to the server. Server components also have the benefit of being able to show user interface elements.

For More Information See the "Creating an ActiveX DLL," "Creating an ActiveX EXE Component," or "Building Code Components" chapters in the "Creating ActiveX Components" book of the Component Tools Guide for information on developing ActiveX components for your Visual Basic applications.

ActiveX Documents

ActiveX documents integrate tightly with the other elements of your Internet or intranet site. You can use ActiveX documents in conjunction with DHTML applications, IIS applications, or HTML pages that are not associated with a Visual Basic Internet application. You can deploy these components so that users can navigate transparently between ActiveX documents and other pages in your application or Web site.

You might use ActiveX documents within your Internet applications for two reasons:

  • If you need to use the Visual Basic programming model rather than the programming model used in DHTML or IIS applications.

  • If you want to have control over the whole frame of the browser window, rather than just a part of it. When you display a Web page in the browser, the HTML you write to display the page lets you specify the appearance of only the page itself. You cannot write HTML to control the menu, the scroll bar, or any other part of the browser window. When you use an ActiveX document, you can control additional pieces of the window: You can add menus, toolbars, scroll bars, and other items. For example, if you want one page of your DHTML application to contain a custom menu, you might use an ActiveX document.

As is the case with the elements of an HTML page, ActiveX documents that are not installed on users' computers can be automatically downloaded when users navigate to them — and upgraded automatically if the versions on the server are more recent.

For More Information See the "Building ActiveX Documents" chapter in the "Creating ActiveX Components" book of the Component Tools Guide for information on creating an ActiveX document.

Internet Extensions for Standard Applications

The Professional and Enterprise Editions of Visual Basic also include controls designed specifically to encapsulate Internet-related technology. These include:

  • Internet Transfer Control — Packages three common Internet protocols: HyperText Transfer Protocol (HTTP), File Transfer Protocol (FTP), and Gopher.

  • WinSock Control — Allows you to connect to a remote computer and exchange data using either the User Datagram Protocol (UDP) or the Transmission Control Protocol (TCP).

In addition, a WebBrowser control that is available when you install Internet Explorer 4.x allows you to run Web pages as part of a Visual Basic form.

For More Information For information about the Internet Transfer control and the WinSock control, see "Using ActiveX Controls" in the Component Tools Guide. For information on using the WebBrowser control, see the Internet Client SDK on the MSDN Library CD, under the heading "SDK Documentation."


Developing DHTML Applications

A DHTML application is a Visual Basic application that uses a combination of Dynamic HTML and compiled Visual Basic code in an interactive, browser-based application. A DHTML application resides on the browser machine, where it interprets and responds to actions the end user performs in the browser.

In its simplest form, a DHTML application can be a single HTML page that uses Visual Basic code and the Dynamic HTML object model to instantly respond to actions that occur on the page. This could involve responding to user-initiated actions such as mouse movements and clicks, or responding to actions the browser itself performs, such as opening the page or loading a picture. These are just a few of the things you could do with a more complicated DHTML application:

  • Retrieve data from the page and use it to query a database.

  • Update the page's appearance and behavior.

  • Create HTML elements and insert them onto a page in response to user requests.

DHTML applications use Visual Basic code to perform much of the same processing you might have previously done with script, CGI processing, and other methods of Internet application development — and much of it can be done without transferring processing to the server.

DHTML applications are named for Dynamic HTML, a technology extension of Internet Explorer 4.x that allows developers and end users to interact with Web pages in new ways. DHTML applications must be run on Internet Explorer 4.x.

This chapter assumes that you are familiar with some basic Internet concepts that are covered in "Introduction to Internet Applications." See it for more information on Internet technologies in general, including HTTP, the Internet development environment, and models of Web browser and Web server interaction.

Topics

What is a DHTML Application

Introduces and defines DHTML applications, explains their advantages, and presents the structure of a typical application.
Dynamic HTML in Visual Basic

Presents information about how Dynamic HTML is used in DHTML applications and introduces the object model.
Design Considerations for DHTML Applications

Presents considerations to keep in mind when creating your DHTML applications.
DHTML Application Development Process

Presents detailed instructions on how to create DHTML applications.
Beginning Your DHTML Application Project

Presents instructions on how to start a DHTML application.
Designing Pages for DHTML Applications

Explains how to create the user interface for DHTML applications, either by importing an existing HTML page or creating your own in Visual Basic.
Writing Code Using Dynamic HTML

Presents information on how to write code for a DHTML application using the Dynamic HTML object model.
Managing State in DHTML Applications

Explains how to manage state in a DHTML application.
Testing Your DHTML Application

Covers how to test and debug your DHTML applications.
Building Your DHTML Application

Discusses compiling your DHTML applications.
Deploying Your DHTML Application

Presents information on how to deliver your DHTML application to the Web server and to your users.

Sample Applications

DhShowMe.vbp

Demonstrates several techniques for incorporating Dynamic HTML functionality into the Web pages you create for DHTML applications and into your Visual Basic code. The pages in this application demonstrate how you can use Dynamic HTML to change the styles, layout, and elements on your Web pages, and how you can use the object model to handle events at multiple levels of the page.

You will find DhShowMe.vbp listed in the Samples directory.
PropBag.vbp

Demonstrates how to use the GetProperty and PutProperty functions to save state between your Web pages in a DHTML application. You will find PropBag.vbp listed in the Samples directory.


What is a DHTML Application?

A DHTML application is a group of HTML pages that work together to perform a business process. You write Visual Basic code to handle events that occur when these pages are viewed in the browser. You can respond to events that occur on any element on the page — from clicking a button to loading an image to passing the mouse over a certain part of the page.

Most of the processing associated with a DHTML application occurs on the client computer, although the application can make calls to the server. However, performing the majority of processing on the client (in this case, the browser) allows your applications to respond quickly to user actions without making time-consuming trips to the server. In addition, corporations can lower the cost of training and support associated with their applications by presenting commonly used information and applications through the browser.

The end user views a DHTML application with Internet Explorer 4.0 or later. DHTML applications are designed to work most optimally on intranets. An intranet is a Web site or series of Web sites that belong to an organization and can be accessed only by the organization’s members. Many corporations use an intranet, rather than the Internet, to offer their employees easy access to corporate information, such as customer order information, sales data, or performance figures, while preventing outside access to that data.

In addition to writing Visual Basic code to hook up to a Web page, you can use a special designer in Visual Basic to create Web pages that act as the user interface of your application. You can also pull an existing Web page into Visual Basic and modify it. This allows you to tailor the process to your preferences — if you are familiar with HTML and want to create your own pages you can do so using the DHTML Page designer. If not, you can have an experienced Web developer create the pages and simply add them to your application.

Note See "Developing Internet Applications" for more information on Internet technologies in general, including HTTP, the Internet development environment, and models of Web browser and Web server interaction.

Important You must use Internet Explorer version 4.01 or later if you want to create or run a DHTML application in Visual Basic.

Web Pages vs. Forms

DHTML applications are structured differently than forms-based Visual Basic applications. In a DHTML application, the user interface consists of a series of HTML pages rather than forms. An HTML page is like a form in that it contains all the visual elements that make up your application's user interface. You can place the some of the same items into a page as you do a form, including text, buttons, checkboxes, and radio buttons.

An HTML page is stored in an .htm file that is analogous to a .frm file, in that it is used to render and display the form to the end user. You can create these pages yourself, or a Web designer or a developer can create the pages and you can then link the finished pages into your project.

The following table sums up the differences between forms-based applications and Web-based applications:

 | Forms-based application | Web-based application
User interface | Visual Basic forms | HTML pages
UI elements (for example, text boxes, buttons, images) | controls | elements
File format | .frm files | .htm or .html files, or generated from Visual Basic code
Creator | developer | Web designer or developer
Run time | Visual Basic run-time DLL, msvbvm60.dll | Web browser or Web browser control, with msvbvm60.dll

Structure of DHTML Applications

DHTML applications are made up of the following pieces:

  • One or more HTML pages.

  • Visual Basic code that handles the events generated from the HTML pages.

  • A run-time component that hosts the page in the Web browser or Web browser control.

  • A project DLL that contains your Visual Basic code and is accessed by the run-time component, generated automatically when you debug or compile.

There is a one-to-one relationship between the designers and the HTML pages in your project. For each page in your application, there is a page designer.


Advantages of DHTML Applications

Building a DHTML application in Visual Basic provides several advantages over other methods of Internet development. DHTML applications give you:

  • Dynamic HTML. When you create a DHTML application, you have full access to the richness of Dynamic HTML, integrated with the power of Visual Basic code and controls. See "Dynamic HTML in Visual Basic" for more information about the Dynamic HTML features you can access.

  • Lessened server load. DHTML applications conserve server resources because each request or user action does not have to be routed through the Web server.

  • Fewer refreshes, faster responses. When an end user's actions initiate changes to a typical Web page, the browser must refresh the page from the server. In a DHTML application, the browser can process user data, make changes to the page's layout and appearance, and process code all without refreshing the page.

  • Dynamic interaction. Visual Basic code on a Web page can directly manipulate any element on the page and create and manage new elements on the fly, allowing for truly dynamic user interfaces.

  • Improved state management. Typically, HTML pages are stateless — that is, no information about an HTTP request is maintained after the response is received from the server. Visual Basic DHTML applications allow you to store state between requests, without using the server. Therefore, multiform or multipage applications are possible without requiring server interaction, complex URL-based state, or cookies.

  • Offline capability. For a DHTML application, users can browse to and use a DHTML application on their corporate intranet. Later, when disconnected, the same users can still make use of their Web-based application through the browser’s cached storage.

  • Code security. When you embed scripts within an HTML page, anyone can access your page, read the script, and make changes to it. Using Visual Basic to develop your DHTML application, your code is compiled, is not part of the HTML page itself, and cannot be tampered with as easily.

For More Information For more information about HTTP, see "Understanding Internet Basics" in "Developing Internet Applications."


Key Events in DHTML Applications

Your DHTML application goes through several main events during its lifetime. There are three objects involved in the life of the application:

  • The BaseWindow object represents an instance of the browser and is used to display the Document object.

  • The Document object represents the HTML page the end user views in the Web browser or Web browser control. You use events in the Document object to access the Dynamic HTML object model and handle user actions in the browser. The Document object, in turn, contains the DHTMLPage object.

  • The DHTMLPage object is contained by the document object, and represents a run-time utility that hooks up the page to its Visual Basic code when the application runs. The DHTMLPage object provides functionality such as a load, unload, initialize, and terminate events for your HTML pages. You cannot access the DHTMLPage object directly through the Document object.

The following figure shows how these objects work together:

Page Designer Top-Level Objects

The DHTMLPage object manages the lifetime of your HTML pages with the following sequence of events:

  1. The Initialize event occurs early in the loading process, whenever the run-time DLL for the application is created or recreated by the system. It is always the first event in a DHTML application. When the Initialize event is fired, not all objects on the page have been loaded, so object references may not be valid. Because of this, you should not use the Initialize event to reference and set properties for elements on the page.

  2. The Load event occurs later in the loading process, after the Initialize event. If your page is loading asynchronously, the Load event is fired after the first element on the page has been created. If your page is loading synchronously, the Load event is fired after all of the elements have been created. You can use the Load event to set information on the page.

  3. The Unload event occurs when the end user moves to another page referenced by the DHTMLPageDesigner object or closes the application. During unload, all of the objects on the HTML page still exist. This is therefore a good time to do any state management, cleanup, or other processing that needs to reference items on the page.

  4. The Terminate event occurs when the HTML page is about to be destroyed. None of the objects on the page exist in memory during the terminate event, so you cannot use this event to reference items on the page for state management or other processing.

For More Information See "Document Object Model" in the "Dynamic HTML" section of the Internet Client SDK for a full list of the properties, methods, and events available for the Document object. For information on how to use the Dynamic HTML object model in Visual Basic, see "Writing Code with Dynamic HTML" in this chapter.


Dynamic HTML in Visual Basic

Dynamic HTML is an extension of HTML that allows Web authors and developers to create pages that dynamically update their content and interact with the user without relying on scripts or server-side processing. When you create a DHTML application, the resulting pages use the Dynamic HTML technology in Internet Explorer 4.x. Dynamic HTML's object model allows you to interact with HTML pages in new ways — for example, manipulating their properties, methods, and events, and exercising finer control over the layout of the pages.

A summary of key features of Dynamic HTML is presented in the following topics:

  • Element Appearance in DHTML Applications

  • Element Positioning in DHTML Applications

  • Event Bubbling in DHTML Applications

HTML Elements As Objects

An HTML page consists of a series of objects that are called elements. Elements on a page can include simple items such as headings, paragraphs of text, and images, or more complicated items such as form fields, or tables and their rows, columns, and cells. On the surface, the page’s elements correspond very closely to the items in a Visual Basic form. However, in past versions of HTML you could not access properties and events on all of these objects: only certain elements were available to be programmed.

With Dynamic HTML, you can now access and manipulate every element on a Web page. This allows you to treat an HTML page much like a form — you can change the attributes for any element, write code or script that performs actions on it, and access its methods and events.

For More Information See "The Dynamic HTML Object Model in Visual Basic" in this chapter for more information on the objects available in DHTML applications. See "Dynamic HTML" in the Internet Client SDK for more information on features of Dynamic HTML. See "Programming with Objects” in the Visual Basic Programmer's Guide for more information on the basics of object programming.


Element Appearance in DHTML Applications

Dynamic HTML allows you to create richer, more interactive user interfaces by allowing you to:

  • Use additional measurement units, for finer control over the positioning of page elements.

  • Create elements that wrap around other elements, nest within them, or act as blocks.

  • Have finer control over tables.

  • Support additional types of graphics , including .mpg files (movie clips) and .png files (portable network graphics designed to address some of the shortcomings of .gif files), and other media types registered with the browser.

In addition, you can make use of dynamic styles and dynamic content to fine-tune the appearance of your page.

Dynamic Styles

Originally, HTML was intended to provide a way to separate the structure and content of a Web page from its presentation. That is, the HTML code that defined a page's content and structure was not intended to also define the physical appearance of elements on the page. In earlier version of HTML, this separation was not possible. The only way to allow users to control the formatting of page elements was to introduce tags such as bold <B> and italic <I> and a variety of attributes that controlled such things as font style and size.

With Dynamic HTML, the original intent of HTML is realized in that you can use cascading style sheets (CSS) to format your page elements. The code controlling the appearance of page elements can be stored in a style sheet. This style sheet is kept separate from the HTML code controlling the page's content and structure.

A style sheet is a collection of properties that control the appearance of elements on a Web page. Style sheets can apply a style to a group of elements, or to a single element. In addition, you can apply multiple styles to each element on the page. There are three main types of styles you can use in your DHTML applications:

  • Inline styles are styles applied to a single element and applied within the element's HTML tag. This is the least flexible way to use styles.

  • Global style sheets are defined at the beginning of an HTML file and set and apply styles for all elements within the page. This is a flexible way to set all styles for a single page.

  • Linked style sheets are stored in a separate file and can apply to multiple pages. This is the most powerful and flexible way to set styles for an application that uses more than one page.

The advantage of using style sheets is that it is very easy to make changes to a page's appearance without writing code. That is, you can change a single line of code in the style sheet — such as increasing the font size of an element — and have that change be reflected in multiple locations throughout your page or in multiple pages. In previous version of HTML, you would have had to make the same change in each location where you wanted to alter the page's appearance. Because of this, it is preferable to avoid using inline styles as much as possible, and store your style information in either a global or a linked style sheet.

Note To define global style sheets in your DHTML applications, you need to edit your application's HTML pages in an external editor, as Visual Basic does not provide a way to define styles sheets within the designer. However, if you use the designer to open a page that references a style sheet, the styles for that document appear in the toolbar.

For More Information For more information on using style sheets in your applications, see "Dynamic Styles" in the "Dynamic HTML" section of the Internet Client SDK.

Dynamic Content

Dynamic content means that you can customize or replace the contents of a page after the page has been loaded, without making a trip to the server. In past version of HTML, any change to a page's contents required the browser to submit a request to the Web server and then wait for a new version of the page to be sent. Customization required more processing time, and it was often difficult to retain context for the page's contents.

Using the properties and methods provided in Dynamic HTML, you can easily add new elements with events or remove existing elements. In addition, you can change portions of existing elements by inserting new content within them.

For More Information For more information on using style sheets in your applications, see "Dynamic Content" in the "Dynamic HTML" section of the Internet Client SDK.


Element Positioning in DHTML Applications

Several features of Dynamic HTML give you detailed control over the position and behavior of elements on an HTML page. You can control where an object is placed, and work with special effects such as animation, transitions and filters, and automatic resizing.

Positioning and Animation

Positioning is the ability to place an HTML element at a specific point in a page by assigning an x- and y-coordinate and a z-plane to that element. This means you can place elements exactly where you want them and achieve special, overlapping effects by defining how elements should be stacked on top of one another.

Because the object model gives you access to styles and style sheets, you can set and change the position of an element as simply as you set and change its color. This makes it especially easy to change the position of elements based on how the user is viewing the page, or to animate the elements. For animation, all you need is to slightly modify the position of an element at some regular interval.

For More Information For more information on positioning effects, see "Positioning" in the "Dynamic HTML" section of the Internet Client SDK.

Automatic Resizing and Scrolling

In a standard Visual Basic application, you have to write code that tells the system how to respond when a user resizes a form. In a Web page built with Dynamic HTML, the Web browser automatically handles the resize event and adjusts the user interface accordingly. In Dynamic HTML, percentages can be used as sizes for elements. In addition, you can anchor elements at a particular place in the flow of text. This enables a page to resize flawlessly.

In addition, you can now create bottomless, scrollable forms. Forms in Visual Basic do not scroll and have a set size. HTML pages, however, always include a scroll bar and can be considered bottomless because the user can continue scrolling as long as there are elements to view.

Filters and Transitions

Internet Explorer 4.0 enables you to specify filters and transitions using cascading style sheet, or CSS, properties. Filters are effects (such as text drop shadows) that can be applied to content on Web pages. Transitions are effects that can be applied when changing the display of an element—switching from one image to another, for example. Both transitions between pages and transitions on specific elements within a page are supported.

In addition to the set of standard filters and transitions included in Internet Explorer 4.0, the standard filters and transitions can be supplemented by additional third-party filters and transitions.

For More Information For more information on filters and transitions, see "Filters and Transitions" in the "Dynamic HTML" section of the Internet Client SDK.


Event Bubbling in DHTML Applications

Unlike standard Visual Basic programming, where each object must have its own unique event handler, objects in Dynamic HTML can share event handlers. When an event occurs on a child object, the event can travel up the chain of hierarchy within the page until it encounters an event handler to process the event. This process is called event bubbling.

The following figure shows the process of event bubbling.

Event Bubbling Process

In this picture, the diagram displays a portion of the structure of an HTML page within a DHTML application. The top level is the Document object, which contains the HTML page. The Body element is the top-level tag in the HTML page. Within the body is a table, and within the table is a hyperlink. In a sense, the hyperlink can be said to be the child of the table, which is in turn a child of the body tag, and so forth.

When the hyperlink event fires, Visual Basic can bubble that event up the hierarchy until it finds an event handler. If it does not find an event handler in the hyperlink, it looks in the table, then the body, then the DHTMLPage object, and finally in the document. The event stops bubbling the first time it finds an event handler.

Note The hierarchy through which events bubble up is determined by the position of elements in the HTML stream. This order does not necessarily correspond to the order of items you view on the screen. For example, in the picture above, the Hyperlink element appears beneath the table in the HTML stream. If you use position attributes on the Hyperlink to move it so that it appears elsewhere when it is displayed in the browser, its events will still bubble up to the table because its position in the HTML stream is unchanged.

Event bubbling can reduce the amount of code you need to write. For example, suppose you want a second set of options to become available when a check box is selected. Rather than writing a click event procedure for each check box on your form, you can write the procedure on the form itself. When one of the check boxes is selected, the event will automatically bubble up to the parent form and be processed there.

You can cancel event bubbling on an element if you do not want the element’s events to bubble up the hierarchy. You stop bubbling by setting the cancelBubble property of the event object to true in any event handler. After the handler returns, the event stops bubbling and comes to an immediate end.

For More Information For more detailed information on how to bubble events or cancel bubbling, see "Understanding the Event Model" in the "Dynamic HTML" section of the Internet Client SDK.


Design Considerations for DHTML Applications

There are several factors you should keep in mind when creating a DHTML application. These include deciding how you want your application to load in the Web browser or Web browser control, and making decisions about the layout and structure of the pages you create. You must make sure that the elements on your page are properly named and that your pages do not contain any references that would prevent the application from locating related files.

General Considerations

  • Follow principles of good Web design. The HTML pages you develop for your application should follow principles of good Web design that are described in most HTML style references. For example, you should remember that end users sometimes view Web pages with the pictures turned off. This can make your application difficult or impossible to use if you embed key information in images. Keep this in mind as you design your interface, and provide backup mechanisms for any crucial information presented in a picture.

  • Only elements with an ID attribute can be programmed. If you create your HTML pages outside of Visual Basic and then import them into the page designer, be sure to give all of the elements for which you want to handle events a unique ID attribute so that you can manipulate them easily in your Visual Basic code.

  • All IDs must be unique when you use Dynamic HTML in Visual Basic. In a Visual Basic project, all of the names and IDs you give to forms, their elements, and associated modules must be unique. Dynamic HTML used outside of Visual Basic does not have this same restriction — multiple items on an HTML page created outside of Visual Basic can be given the same ID. If you use a page with duplicate IDs in Visual Basic, the page designer appends numbers to the duplicate IDs to make them unique. When you use an external page, be aware that some IDs may be changed.

  • Use relative URLs. Your application and its HTML pages can be deployed onto a Web server with a different directory structure than the one on the development computer. Because of this, it is best to use relative URLs in your HTML pages rather than absolute URLs. Absolute URLs indicate the exact drive and directory in which your HTML page will expect to find any related images or other files it references. Relative URLs give the name of the file to locate and indicate its location in relation to your project directory, specifying how many directories up or down to move to find the reference.

  • Use style sheets whenever possible. Rather than accessing the style property of an individual element to set an element's physical appearance, use global or linked style sheets as much as possible. Global or linked style sheets allow you to take full advantage of the flexible nature of Dynamic HTML. For more information on styles in Dynamic HTML, see "Element Appearance in DHTML Applications," earlier in this chapter.

  • Not all properties cascade. In Dynamic HTML, most properties that you set for a page can cascade down to the page’s children through style sheets. For example, if you set a font for the page, paragraphs, buttons, and other elements with text will inherit that font setting and use the same font. When you set a background color, however, it does not cascade down to any of the object’s children.

  • Some objects cannot be moved once you place them on the page. Some Visual Basic controls, such as the common dialog or the sysinfo control, are invisible at run time. If you add one of these objects to your HTML page, you cannot select it and move it around within the page after you initially draw it. You can, however, select the control in the treeview and either delete it or access its properties.

  • You cannot use the page designer in a single-threaded DLL. To create a DHTML application, you must set your project to be apartment-threaded. You set this on the General tab of the Project Properties dialog box.

Asynchronous Loading Considerations

When the system loads a Visual Basic form, the entire form is loaded and then displayed. When the system loads an HTML page into a Web browser or Web browser control, some parts of the page that load more quickly are displayed first, while the browser finishes loading and displaying other portions of the page. This process is called asynchronous loading and presents some unique challenges for a Web application. Users may have access to some parts of your user interface before other, dependent elements appear. For example, if the code behind a command button references an event on an image control, an error can occur if the user selects the button before the image is loaded.

Note By default, asynchronous loading is turned off for your HTML pages. If you want to change this setting, you can set the AsyncLoad property for page to True.

To lessen the effects of asynchronous loading in your application:

  • When possible, leave AsyncLoad set to False. To prevent the browser from displaying any part of the page until the entire page is loaded, set the AsyncLoad property on the page designer to False. This makes the browser wait to run any Visual Basic code until all elements and events are loaded. False is the default setting of this property.

  • When you must use asynchronous loading, check for the existence of an object before you reference it. When you must use asynchronous loading, it is best to not write code that crosses object boundaries by referencing one object on the page from within another object. There are times, however, when crossing object boundaries will be necessary. In these cases, include code that checks to see whether the object exists, or provide error handling that tells the system how to react when the referenced object does not exist.

  • Don’t rely on backward references to avoid trouble. Backward referencing means using references to only those elements on the page that appear above the current item. The assumption is that the page will load in an orderly, top-to-bottom fashion. This is not the case. Elements on an HTML page can load in a random order.


DHTML Application Development Process

The process of creating a DHTML application is similar to creating any other project in Visual Basic. The overall process is presented below. Details for each step in the process can be found in later topics in this chapter.

These are the steps you perform to create a DHTML application. For more information on the individual steps in the process, see the following sections.

  1. Start a new project and select DHTML Application as the project type.

  2. From the Project Explorer window, open the designer.

  3. If you are designing your user interface from scratch, add HTML elements and ActiveX controls to your page and arrange them as desired.

  4. If you want to edit an existing page, use the Properties dialog box to reference an external HTML file, then make any necessary changes to the page's contents and appearance.

  5. Add code for any elements on the page for which you want to handle user actions.

  6. If necessary, add other pages to the project, add elements to them, and write code.

  7. Test and debug the application by running the project and viewing the document in Internet Explorer 4.01 Service Pack 1 or later.

  8. Compile the project.

  9. Deploy the application using the Package and Deployment Wizard.


Beginning Your DHTML Application Project

When you begin a DHTML application, you select a special type of project called a DHTML Application project. A DHTML Application project is an ActiveX DLL project that automatically sets the correct references you need to access the designer for your HTML pages and the appropriate toolbox tab and controls.

Visual Basic automatically inserts a designer into the project and displays it on the screen when you open this project type. The designer corresponds to a single HTML page and acts as the base on which you create its user interface. If you want to include more than one HTML page in your application, you can insert additional designers into your project.

The application you create runs as an in-process component in the Web browser or Web browser control. For more information on in-process components, see the "In-Process and Out-of-Process Components" section in the Component Tools Guide.

To begin your DHTML application

  1. Start a new project and select DHTML Application as your project type.

  2. In the Project Explorer window, open the Designers folder, then double-click on DHTMLPage1 to display it in the main window.

To add additional DHTML pages to your project

  1. On the Project menu, click Add DHTML Page and Visual Basic adds a new page designer to your project.

    Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

  2. In the DHTMLPage Properties dialog box, choose how you want to save your HTML pages, then click OK.

Save Options for Pages and Designers

When you add pages to the project, you must make decisions about how your page will be saved and whether you will create a new page or edit an existing HTML page from your hard drive. There are two ways you can save your page:

  • Save it within the designer file. This method allows you to easily share files between developers or move your development project from machine to machine without worrying about paths to any external pages. However, saving your page in this way prevents you from using the Launch Editor feature to edit it in an external HTML or text editor.

  • Save the page to a location on your computer. This method creates external HTML files that are referenced in the designer's SourceFile property by an absolute path. When you save to an external file, you can use the Launch Editor feature to edit the page in an external HTML or text editor. However, moving your file from machine to machine may cause the path you entered for your source file to become invalid.

The SourceFile property for the designer reflects the save method you choose. When you save within the designer, the SourceFile property is blank. When you specify an external file, the SourceFile property reflects the full path to the file you selected.

The save options you choose apply only until the project is built. At that time, the system generates external HTML files for the pages in your project and stores them in the location you specify.

Specifying save options for a page

To access the DHTMLPage Properties dialog box

  • Select the page designer and then click the Properties toolbar icon.

To save your HTML page within the designer files until the project is built

  • Click Save HTML inside the designer in the .DHTMLPage Properties dialog box.

To create a new page and save it to a location on your computer

  1. Click Save HTML in an external file in the .DHTMLPage Properties dialog box.

  2. Click New, enter a file name for your page, and click OK.

To import an existing page and save it to a location on your computer

  1. Click Save HTML in an external file in the .DHTMLPage Properties dialog box.

  2. Click Open, choose the file you want to edit, and click OK.

To import an existing page but save it within the designer

  1. Click Save HTML in an external file in the .DHTMLPage Properties dialog box.

  2. Click New, choose the file you want to import, and click OK.

  3. Click Apply to import the page.

  4. Click Save HTML within the designer.

  5. Click OK to return to the page designer.

    Visual Basic sets the value of the SourceFile property to reflect the choices you made.

Note You can edit the SourceFile property directly to make the same choices. Enter a path and file name in the Properties window to import a file or save to an external file, or clear the contents of the SourceFile property to save the page within the designer.


Designing Pages for DHTML Applications

There are three ways you can create the user interface of your DHTML application:

  • You can create pages from scratch, adding HTML elements from the toolbox and arranging them as desired.

  • You or a Web developer can create pages in an external HTML editor, then you can reference them in the page designer.

  • You can use a combination of both methods.

Page Design: Visual Basic vs. HTML Editors

When you create pages in Visual Basic, you add HTML elements to the designer and arrange them on the page. You do not need to know any HTML tags to build a page in this way, because Visual Basic creates the tags and attributes for each element for you. This method of design is quick and easy and allows you to create functional pages without learning the details of HTML.

However, Visual Basic is not designed to be an advanced HTML editor and does not contain some of the features you will find in many HTML editors. For example, you cannot view the source code for your page, and you cannot easily work with advanced features such as animation. It is best to create your pages in Visual Basic when your application's user interface will be relatively simple and you do not want to work in the HTML source code.

If you are more familiar with HTML or you want to incorporate more advanced features into your pages, you should create your pages in an external editor and then import them into Visual Basic. Creating pages externally gives you several advantages:

  • You can use the editor of your choice, with any advanced features it includes. This may give you finer control over the appearance and layout of the page.

  • You can access the source code for the HTML page and perform fine-tuning that you cannot do in the page designer.

  • You can have a graphic designer or another person create the pages if you prefer not to create them yourself.

You can use a variety of external programs to create your HTML pages, including editors such as Microsoft FrontPage®, a word processing program such as Microsoft Word, or Notepad.

In addition to using Visual Basic or an external program to create your pages, you can use a combination of both methods. You can create the preliminary page layout in Visual Basic, then open the page in an editor to do any fine-tuning, or you can create the page in an editor and rearrange and fine-tune elements in Visual Basic.


Using Existing HTML Pages in the Page Designer

You can use existing HTML pages for your application’s user interface rather than designing the pages in Visual Basic. This is useful if you prefer not to design the user interface yourself, or if you want to design it using another tool and then bring it into your Visual Basic application. Using an external program can give you access to the source HTML, rather than just a visual representation of the page, and may give you more advanced functionality when you design the page. You can design your interface in any HTML editor, word processor, or text program.

To use an existing HTML page, you reference it as the source for the page by setting the SourceFile property. Visual Basic loads the file and displays it in the rightmost pane of the designer window. You can then tailor the page's appearance, if necessary, by changing properties or adding, deleting, or rearranging elements on the page.

When the designer loads the page, it also checks to see what elements exist on the page. It displays all elements in the leftmost pane of the designer window, in the treeview.

To reference an existing HTML page

  1. If necessary, add a new designer to your project by clicking Add DHTML Page from the Project menu.

    Note Remember that there is a one-to-one relationship between pages and designers. Each time you want to add a new page to your application, you must first add a designer from the Project menu.

  2. In the DHTMLPage Properties dialog box, click Open, then choose the HTML page you want to import and click OK.

    Visual Basic loads the HTML page when you close the dialog box and displays it in the designer window.

For More Information See "Understanding the Page Designer Treeview" later in this chapter for more information on the treeview and what it displays in the page designer.


Creating New HTML Pages in the Page Designer

You can design HTML pages in Visual Basic, using the DHTML Page designer. The designer allows you to create your application's user interface without explicitly writing any HTML code. Instead, you add HTML elements to the designer's drawing surface, arrange them as desired, and set properties that control their appearance. Visual Basic writes the HTML code for each element behind the scenes, storing it either in an HTML file whose location you specify, or saving it within the designer itself.

It is best to design your pages within Visual Basic when you are creating a relatively simple user interface. Visual Basic does not give you direct access to the source HTML, and it does not contain some of the advanced features you will find in HTML editing programs. If your interface is more complex or your prefer greater control over the HTML source code, you can create your pages in an external program and then link them to your project.

Visual Basic provides a set of HTML elements that you can add to the page when you design HTML pages in Visual Basic. These include the most common HTML elements, such as buttons, text boxes, option buttons, checkboxes, and images. You cannot use intrinsic Visual Basic controls such as the text box or checkbox on your pages; instead you must use the special HTML controls. These controls, called elements, appear automatically on an HTML tab in the toolbox when you add a page to your project.

There are four ways you can add elements to your page:

  • You can select the element from the toolbox, then draw it on the detail pane.

  • You can double-click the toolbox icon for the element.

  • You can drag and drop the element from the toolbox onto the designer's detail pane.

  • You can type text directly onto the designer's detail pane to create paragraphs, headings, and other text elements, then set properties to change them to the appropriate style.

In addition to the HTML elements in the toolbox, you can add any ActiveX control to your page. For example, you can use Visual Basic to define an ActiveX control, add it to your toolbox, and insert it on any HTML page.

Note Some Visual Basic controls, such as the common dialog or the sysinfo control, are invisible at run time. If you add one of these objects to your HTML page, you cannot subsequently select it in the drawing surface and move it around within the page. To manipulate one of these controls after you have added it to the page, select it in the treeview.

For More Information For an explanation of the relationship between the elements on an HTML page and the HTML source code, see "Understanding Internet Basics" in "Introduction to Internet Applications."

To create an HTML page from scratch

  1. If necessary, add a new designer to your project by clicking Add DHTML Page from the Project menu.

  2. Add the elements you need to the page:
    • To add text, labels, or headings to your page, click on the page, press ENTER until you reach the location at which you want the text to appear, and then type the text.

    • To add an HTML element to your page, click the appropriate icon from the HTML tab of the toolbox and then draw the element on the page.

    • To add an ActiveX control to your page, switch to the standard tab of the toolbox, select the ActiveX control, and draw the control on the page.

      Note You may need to use the Components dialog box to add the ActiveX control you want to your project before you will see it in the toolbox.

  3. Resize and position your elements as you would for any standard Visual Basic project. See "Positioning Elements," later in this chapter, for information on moving HTML elements.

  4. Set any default properties to control the appearance of the page.
    • To set properties for the page, such as background color, select the Body element in the Properties window list, then set properties.

    • To set properties for an element, select the element in the designer, then set its properties.

Tip Some HTML elements have a very large number of properties available. It is simplest to work on the Categorized panel when you access the Properties window for these elements. The primary properties (such as ID) appear under the Misc category, while properties dealing with appearance and styles appear under the Style category.


Positioning HTML Elements

After you add elements to your page, you may want to rearrange and fine-tune their positioning. There are several things you should know about positioning elements on the page:

  • Move text elements with the keyboard. Text elements such as headings, paragraphs, and labels are positioned independently of other elements on the page. You can move text elements by using the ENTER or BACKSPACE keys. Moving a text element in this way does not affect the other elements around it — that is, all of the text elements on your page will move down a line if you press ENTER at the top of the page, but buttons and other visual elements will not be moved.

    Tip You can also move align text elements using the toolbar alignment icons.

  • Move nontext elements with the mouse. You can move elements such as buttons, images, and checkboxes by clicking on them, selecting the gray border around the element, and moving the element to its new location.

    Note You must select the border of an element in order to move or delete it. When you select the body of an element, the border shows a series of diagonal lines. When you select the border of an element, the border shows gray fill. You can move an element when the gray fill is visible.

  • Elements without a run-time interface cannot be repositioned once they are added to the page. Some Visual Basic controls, such as the common dialog or the sysinfo control are invisible at run time. If you add one of these to the page, you cannot select it or reposition it. You can delete the item by selecting it from the treeview and using the shortcut menu option to remove it.

  • Elements in an HTML page can be layered. In addition to arranging your elements on the page in two-dimensional space, you can layer elements on top of and behind each other by using the Order toolbar options.

  • There are two positioning modes: absolute and relative. These modes determine how the system interprets your actions as you add elements to the page. It is important to understand the ramifications of choosing between these two modes.

Absolute and Relative Positioning

When you are designing or editing pages in the designer window, you can work in one of two modes: absolute mode or relative mode:

  • In absolute positioning mode, all items you drag onto or move on the page are placed at the exact location where you release the cursor. Visual Basic writes information to the HTML for each element that indicates the exact position of the item.

  • In relative positioning mode, each new item you drag onto the screen is placed in the next available cursor position. If you have not used the cursor to add any paragraph <P> tags to the page, the elements you drop on the page are positioned in the next available space, moving left to right across the screen, regardless of where you drop the element on the page.

The following screen shows how Visual Basic positions three buttons that are dropped at varying locations on the drawing surface in absolute mode:

Buttons Positioned in Absolute Mode

If you were not working in absolute mode and dropped those three buttons onto the screen, they would be positioned as shown in the following screen:

Buttons Positioned in Relative Mode

Absolute and relative mode have advantages and disadvantages. Absolute mode allows you finer control over the layout of your page but is not as flexible as relative positioning. With relative positioning, the page can easily adjust to end users with different viewing environments. For example, if the browser screen is small or the computer's resolution is much lower than that on the development machine, only a portion of your absolutely positioned page may appear to the end user. In relative positioning, the page would dynamically wrap its elements and adjust to the browser's screen space.

To change from absolute mode to relative mode, you select or deselect the Absolute Positioning icon on the toolbar. This sets the mode for the entire page.

Note You can also turn on Absolute Positioning for select items on the page, rather than the whole page itself, by using the Absolute Positioning toolbar icon. Selecting this button affects both the selected element and all subsequently created elements on the page unless you deselect it. It does not affect elements already placed on the page.

Layering Elements

You can layer elements on top of and behind each other using z-order properties. Z-order assigns a sequence number to elements that indicates how elements are layered. For example, the following screen shows three layered buttons.

Buttons Arranged in Layers

In this screen, three buttons have been arranged so that they overlap. Button1 is in the foreground, with Button2 placed behind it and Button3 placed in the back.

You can manipulate the layering of elements that occupy the same space by selecting the element you want to move forwards or backwards and selecting the Order icon on the toolbar for the page.

Z-ordering in DHTML applications is recorded differently than z-ordering in forms. On an HTML page, the body element, or page surface, is considered to have a z-order value of zero. Items that are layered on top of the body have a positive z-order value, and items that are layered behind the body have a negative z-order value.

Locking Elements

It is easy to accidentally move an element on your HTML page by clicking on it and moving the mouse. You can prevent an element from being moved by locking it. Any absolutely positioned element can be locked in place with the Lock icon on the toolbar.


Grouping HTML Elements

You group sections of an HTML page when you want to perform an action on multiple elements. For example, you might group a series of paragraphs in order to apply a single style to the entire section, instead of to each paragraph individually. You use the HTML tags DIV and SPAN to group your elements in this way:

  • DIV is used to group a series of elements into a larger group. For example, you can group multiple paragraphs and headings or several elements on a form together using the DIV tag. DIV is used to offset a group of elements; that is, when you insert a DIV tag in a page, the DIV tag adds a paragraph break after its closing tag. DIV should not be used within a paragraph of text for this reason.

  • SPAN is used within an element to group a part of it that is too small to capture with regular HTML tags. For example, you can use the SPAN tag to group a few sentences from within a paragraph, so you can perform actions that affect only that text. SPAN does not make formatting changes to the document as does the DIV tag, and is therefore better suited for use within paragraphs and in other elements where you do not want to introduce more white space.

You can add DIV and SPAN tags to the page designer by highlighting the elements you want to group, then choosing the DIV or SPAN icon from the toolbar. Visual Basic automatically inserts the appropriate opening and closing tags in the HTML for your page. The resulting DIV and SPAN elements appear in the treeview, and you can assign IDs and write code for them.

Grouping elements in DIV and SPAN tags can be an important part of your coding efforts, because it allows you to manipulate precise portions of the page. The following HTML code shows an example of DIV and SPAN tags used to manipulate parts of a Web page:

<DIV id="client-fullname" class="client">
   <SPAN class="client-lastname">Last name:</SPAN>
   <SPAN class="client-firstname">First name:</SPAN>
</DIV>

In this example, a DIV tag with the ID "client-fullname" groups the fields that specify the client's first and last name. Each label is part of a SPAN tag. If you wanted to perform an operation on all of the fields on the form, you could use the DIV tag to do so. If you wanted to perform an operation on just one item in the form, you could use the appropriate SPAN tag.

To add a DIV or SPAN tag to your page

  1. Select the elements on your HTML page that you want to enclose in a DIV or SPAN tag.

    Note You can highlight the elements in either the drawing surface or in the treeview.

  2. Click the Wrap Selection in <DIV>…</DIV> toolbar icon to add a DIV tag, or the Wrap Selection in <SPAN>…</SPAN> toolbar icon to add a SPAN tag.

    Visual Basic inserts opening and closing tags around your selection.

  3. If you want to program the DIV or SPAN elements, add a unique ID for each element by selecting it in the treeview panel, then setting its ID property in the Properties window.

For More Information For more information on DIV and SPAN tags, see the W3C documentation at http://www.w3.org.


Creating Hyperlinks in the Page Designer

A hyperlink is an HTML feature that lets you make a piece of text or an image into a jump that, when selected, opens another Web page or moves to another location within the current page. You can create hyperlinks easily when you are designing an HTML page in the page designer.

There are two ways to create a hyperlink:

  • If you want to turn existing text into a hyperlink, you can use the Make Selection Into Link toolbar icon.

  • If you want to create a new hyperlink, you can use the Hyperlink icon on the HTML panel of the toolbox.

To create a hyperlink from existing text

  1. Highlight the text or image on your HTML page that you want to make into a hyperlink.

    Note You can highlight the elements in either the drawing surface or in the treeview.

  2. Click the Make Selection Into Link icon on the toolbar.

    The page designer inserts opening and closing tags around your selection and formats it as a hyperlink.

To create a new hyperlink

  • Drag and drop the Hyperlink from the toolbox onto the detail panel, then position and format it accordingly.

    Important A hyperlink element is part of the text stream on your HTML page and must be repositioned using the keyboard rather than the mouse. To move a hyperlink element, place your cursor before the hyperlink and press ENTER.


Working with Tables in the Page Designer

You can easily create tables when designing an HTML page from scratch or working with an existing page. The designer toolbar provides options that allow you to easily insert a table of the size and shape you need. In addition, you can add, delete, and manipulate the rows, columns, and cells in existing tables.

Note You can use the Show Borders icon in the toolbar to display or hide the borders of a table for which the Border property is set to zero. When a tables Border property is zero, no borders appear in the designer, and the table will be borderless when displayed in the browser for the end user. You can use the Show Borders icon to temporarily view the borders in the designer, making it easier to manipulate the table.

To add a table

  • Click the Table Operations dropdown icon from the toolbar, then click Insert Table.

    Note By default, Visual Basic inserts a table with two columns and two rows. You can position your cursor in any cell and type in contents for that cell, or you can fill in table rows programmatically.

To add a row or column to the table

  • Position your cursor in the row above which you want to add a column or row, then click the Table Operations dropdown icon from the toolbar and click Insert Row or Insert Column.

To delete a row or column from the table

  • Place your cursor in the row or column, then click the Table Operations dropdown icon from the toolbar and click Delete Row or Delete Column.

To add table cells

  • Position your cursor in the cell to the left of which you want to add a new cell, then click the Table Operations dropdown icon from the toolbar and click Insert Cell.

To delete table cells

  • Place your cursor in the cell, then select Delete Cell from the Table Operations submenu.


Working with Lists in the Page Designer

There are two types of lists you can add to a DHTML page: a List element and a Select element. The List element is a scrolling list. The Select element creates a combo box much like the one you add to Visual Basic forms.

Adding items to HTML lists works very differently from the process of adding items to a list box or combo box on a Visual Basic form. You cannot use the List property or the AddItem method to add items to an HTML element. Instead, there are two ways you can populate your list:

  • To populate the list at design time, you can edit the HTML in an external editor and enter the appropriate values.

  • To populate the list at run time, you can use a method of the Document object in Dynamic HTML.

When you first add a list to an HTML page in the designer, the HTML for the list element looks like this:

Element Original HTML When Element is Created
List
<select name="List1" id="List1" value="List1">
     <option value="List">
</select>
Select
<select name="Select1" id="Select1" value="Select1">
     <option selected value="Select">Select
</select>

Note If you edit your HTML using FrontPage, you may see closing </option> tags in the code shown above. Either usage is acceptable.

To populate a list element

  1. Click the Launch Editor button from the designer toolbar.

  2. In your HTML or text editor, view the source code for the list element.

  3. Copy the <option value="list"> line and paste it between the opening and closing <select> tags once for each list item you want to add. For example, if you are adding four list items, there would be four instances of <option value="list"> in your HTML code.

  4. Type the list item you want to add after each <option> tags. Your resulting HTML will look something like this:
    <select name="List1" id="List1" value="List1">
<option value="List">First list item
<option value="List">Second list item
<option value="List">Third list item
</select>
  5. Save your changes and return to the designer. When prompted to refresh the changed file, click OK.

To populate a select element

  1. Click the Launch Editor button from the designer toolbar.

  2. In your HTML or text editor, view the source HTML code for the list element.

  3. Copy the <option selected value="Select"> line and paste it between the opening and closing <select> tags once for each list item you want to add. For example, if you are adding four list items, there would be four instances of <option selected value="Select">.

  4. Edit the tags you have copied so that only one line says "option selected value="Select". The other lines should only say "option value="Select". For example, if you have added three lines, your HTML would now look like this:
    <select name="Select1" id="Select1" value="Select1">
<option selected value="Select">Select
<option value="Select">Select
<option value="Select">Select
</select>
  5. Type the list item you want to add after each <option> tags, replacing the word "Select." Your resulting HTML will look something like this:
    <select name="Select1" id="Select1" value="Select1">
<option selected value="Select">Default list item
<option value="Select">Second list item
<option value="Select">Third list item
</select>
  6. Save your changes and return to the designer. When prompted to refresh the changed file, click OK.

Populating a List or Select Element at Run Time

You can also populate list elements at run time. For example, suppose you have an HTML page with a list, a text field, and a button. You want the end user to be able to enter a value in the text field, then press a button to make that value appear in the list. The following picture shows the HTML page:

HTML Page with List Elements

The button code can use a method of the DHTML Document object called CreateElement to add list items to the list. The code would be as follows:

Private Function Button1_onclick() As Boolean
   Dim e as HTMLOptionElement
   Set e = Document.createElement("OPTION")
   e.Text=TextField1.Value
   e.Value="ListItemValue"
   List1.Options.Add e
End Function

Note The CreateElement method can be used only for Select elements, such as the list and select elements in the toolbox, and for IMG elements. You cannot use it to create a new element of any other type.


Understanding the Page Designer Treeview

The designer window for your DHTML applications consists of two parts — the detail panel, on the right and the treeview panel, on the left. The detail panel shows the HTML page as it will appear in the browser and is the area you use to draw and manipulate your page's appearance. The treeview is used to show the elements that make up the HTML page and their relationship to the overall object model for the application. When you first begin a project for a DHTML application, the treeview contains three elements — the Document, the Body, and a paragraph, or <P>, tag.

After you reference an existing HTML page or begin creating your own in the page designer, the treeview displays a listing for each tag on the HTML page. This includes all visible elements on the page, such as buttons, text, images, checkboxes, and options boxes, and a series of tags, such as text formatting tags for bold, italic, and underline, organizational tags such as DIV and SPAN, and other tags.

Although all elements on a page are displayed in the treeview, you can only write code for those elements that contain an ID attribute. The treeview displays in bold the elements with ID attributes. For each element, the page designer lists the ID, if one exists; the type of element; and in some cases the beginning of the content for that element. If an element does not have an ID, you can assign one to it in the Properties window.

The treeview provides three valuable shortcuts:

  • When you select an item in the treeview, Visual Basic highlights the appropriate item in the detail panel and in the Properties window. This can help you determine at a glance the correspondence between elements in the treeview and on the page. Note that this works in reverse as well: When you select an element on the detail panel or in the Properties window, Visual Basic highlights the corresponding element in the treeview and scrolls the treeview accordingly.

  • You can double-click an element in the treeview to open the Code Editor window for that element. The system selects the default event for the element.

  • For ActiveX controls that do not have a visible user interface at run time (such as the common dialog control), the treeview is the only way you can select and manipulate these elements after adding them to your page.

For More Information See "Making Elements Programmable" in this chapter for more information on how to work with element IDs.


Launching an HTML Editor from the Page Designer

If you have saved your HTML page to an external file, you can launch an HTML editor from within Visual Basic and view your page within it. You might do this if you want to fine-tune the appearance of your page, or if you want to work in the HTML source code. When you launch an external editor from Visual Basic, Visual Basic saves your current project and any changes you have made to the page, then opens the page in the default HTML editor for your system. If you do not have an HTML editor installed, the system opens Notepad and displays the HTML source.

Note You can set the default editor that Visual Basic will open on the Advanced panel of the Options dialog box.

You can launch an external editor for only those files that were saved to a location on your hard drive, rather than stored within the designer. If you want to edit a file you initially stored within the designer, you must specify a save location for it by filling in the SourceFile property for the page.

Note If you specify a different source file after one has already been set, Visual Basic prompts you to save your current HTML page before switching to the newly specified file. This will save your changes to the old file name before opening the new file.

Any changes you make to the page in an external editor appear in Visual Basic as soon as you save in the editor. However, changes you make in Visual Basic may not automatically be updated in the editor, depending on the editor you choose to use. Be careful to avoid conflicts when you have an HTML page open in both Visual Basic and another program. In general, it is best to make changes to the document in only one location at a time. The steps for editing a document should be:

  1. Launch the editor from Visual Basic.

  2. Edit your HTML.

  3. Save your changes and close the editor.

  4. Choose "Yes" at the prompt in Visual Basic asking if you want to refresh the page.

Note In addition to watching for changes when you launch an external editor, Visual Basic watches for changes to the page's source file any time you have the page open in Visual Basic. If the source file changes, a prompt appears that asks if you want to reload your page.

To edit your HTML page outside of Visual Basic

  • Click the Launch Editor button in the designer window. The default HTML editor appears.

    Important If you select the Launch Editor button when the SourceFile property has not been set, the system will not launch the editor. Set the SourceFile property, then click the Launch Editor button again. Be aware that this will change the save options for your page. For more information, see "Beginning your DHTML Application Project" earlier in this chapter.


Writing Code Using Dynamic HTML

When you create a DHTML application in Visual Basic, you use the Dynamic HTML object model to access and manipulate anything within the page. The HTML elements in the page are available as individual objects, which means you can examine and modify an element and its attributes by reading and setting properties and by calling methods.

The object model also makes user actions — such as clicking, double-clicking, or right-clicking an element, pressing keys, and loading forms — available as events. You can intercept and process these and other events by creating event procedures in your Visual Basic code.

For a Visual Basic programmer, there are many aspects of the Dynamic HTML object model that are unfamiliar. The following sections present an overview of some of the key differences between Visual Basic’s programming model and the Dynamic HTML object model. The following table explains some of the areas where you may encounter differences and lists the sections to which you can go for more information.

If you need help | See
Understanding the object model | "The Dynamic HTML Object Model in Visual Basic"
Understanding what Visual Basic events the events in Dynamic HTML correspond to | "Events in Dynamic HTML"
Making elements on your page programmable | "Making Page Elements Programmable"
Working with styles | "Setting and Retrieving Properties for a Web Page" or "Dynamic HTML in Visual Basic"
Working with text elements in your pages | "Handling Text in the Page Designer"
Retrieving data from a form or other elements within an HTML page | "Retrieving Data from an HTML Page"
Navigating among pages | "Navigating in DHTML Applications"
Creating elements on the fly and inserting them, with events, onto the HTML page | "Dynamically Creating Elements and Events"
Storing and retrieving state information | "Managing State in DHTML Applications"


The Dynamic HTML Object Model in Visual Basic

Using the Dynamic HTML object model, you can access and manipulate anything within your application's HTML pages. The HTML elements in a page are available as individual objects, meaning you can examine and modify elements and their attributes by reading and setting properties and by calling methods. The text in elements is available through the elements' properties and methods.

The object model also makes user actions, such as pressing a key and clicking the mouse, available as events. You can intercept and process these and other events by creating event procedures in your Visual Basic code.

The following figure shows the major objects in the Dynamic HTML object model.

Dynamic HTML object model

Note The BaseWindow object used by Visual Basic in the picture above corresponds to the topmost Window object in the Dynamic HTML object model.

The two main objects in the object model, for the purposes of Visual Basic, are the BaseWindow object and the Document object.

  • The BaseWindow object represents an instance of the browser and is used to display the Document object.

  • The Document object represents the HTML page you view in the browser. You use events in the Document object to access the Dynamic HTML object model and handle user actions in the browser.


Events in Dynamic HTML

Most events in the Dynamic HTML object model are similar to the events in the Visual Basic programming model. Event names, however, are often slightly different. All event names in Dynamic HTML are preceded by the word "on." You can locate most common events by looking for them under this prefix. For example, the Visual Basic click event corresponds to onclick, load corresponds to onload, and so on.

For More Information See "Document Object Model" in the "Dynamic HTML" section of the Internet Client SDK for a full list of the properties, methods, and events available in an HTML page through the Dynamic HTML object model.

Visual Basic Events vs. Dynamic HTML Events

The sections below list some of the more commonly used events in Visual Basic, their counterparts in Dynamic HTML, and any additional information needed for you to use them.

Keyboard Events

Keyboard events in Dynamic HTML correspond very closely to keyboard events in Visual Basic. The following table lists the common Visual Basic keyboard events and their Dynamic HTML counterparts:

Visual Basic Event | DHTML Event | Comments
keydown | onkeydown | Fires when a key is pressed.
keypress | onkeypress | Fires when a user's keyboard input is translated to a character.
keyup | onkeyup | Fires when a user releases a key.

When a keyboard event occurs, the keycode property of Dynamic HTML's event object contains the Unicode keycode of the corresponding key. The altKey, ctrlKey, and shiftKey properties specify the state of the ALT, CTRL, and SHIFT keys. You can change which key is associated with the event by either changing the value of the keycode property or returning an integer value. You can cancel the event by returning zero or false.

Mouse Events

Mouse events in Dynamic HTML correspond closely to mouse events in Visual Basic. The following table lists the common Visual Basic mouse events and their Dynamic HTML counterparts:

Visual Basic Event | DHTML Equivalent | Comments
click | onclick | In addition to occurring when a user clicks on an element, this event also fires when the user presses ENTER on an element that can receive focus, such as a button.
doubleclick | ondblclick | This event works similarly to its Visual Basic counterpart.
mousedown, mouseup, mousemove | onmouseout, onmousedown, onmouseup, onmousemove, onmouseover | When moving between elements, the onmouseout event fires first to indicate that the mouse has left the original element. Next the onmousemove event fires, indicating that the mouse has moved. Finally, onmouseover fires to indicate that the mouse has entered the new element.

When a mouse event occurs, the button property of the event object identifies which mouse button (if any) is down. The x and y properties specify the location of the mouse at the time of the event. For the onmouseover and onmouseout events, the toElement and fromElement properties specify the elements the mouse is moving to and from.

Focus and Selection Events

Focus and selection events in Dynamic HTML differ slightly from their Visual Basic counterparts. In particular, focus events are called only from certain elements, and selection and dragging are handled differently. The following table lists the common Visual Basic focus and selection events and their Dynamic HTML counterparts:

Visual Basic Event | DHTML Equivalent | Comments
gotfocus | onfocus | Fires when the user moves to an element capable of receiving input, such as a button or form element.
lostfocus | onblur | Fires when you move out of an element that is capable of receiving input.
selchange | onselectstart onselect | The onselectstart event fires when a selection is first initiated — for example, when the user clicks a character or object in the document. Onselect fires when the user changes the selection — for example, by moving the mouse over a portion of the document while holding down the mouse button.
dragdrop, dragover | ondragstart | Fires when the user first begins to drag the selection. The default action is to prepare the selection to be copied to another element.

Note Onblur and onfocus fire whether you move between elements in the page, between frames, or even between applications on the desktop. For example, if a page element has focus and the user switches to another application, the onblur event fires for that element. When the user switches back, onfocus fires.

Other Events

Visual Basic Event | DHTML Equivalent | Comments
change | onchange | Onchange fires when the user tabs off or presses ENTER on an element, moving out of it. In Visual Basic, the change event fires as soon as the user performs any action within the control.
close | -- | No direct equivalent exists for this event.
error | onerror | Fires when an error occurs loading an image or other element, or when a scripting error occurs.
initialize | onready statechange | Fires when the page changes from initializing to interactive, and from interactive to loaded. A page is interactive as soon as the user can interact with it by scrolling or clicking on anchors or elements. A page is loaded when all the content has been downloaded.
load | onload | Fires after the document is loaded and all the elements on the page have been completely downloaded.
paint | -- | No direct equivalent exists for this event.
resize | onresize | This Dynamic HTML event functions similarly to its Visual Basic counterpart; however, you do not need to write code to handle resizing of an HTML page as you do a Visual Basic form. Resize in HTML happens automatically.
scroll | onscroll | Fires whenever the scroll box for the page or any element within it is repositioned.
terminate | -- | No direct equivalent exists for this event. You can perform terminate-type actions in the onunload event. A terminate event does exist for the DHTMLPage object. This object is not part of the Dynamic HTML object model, and is unique to Visual Basic.
unload | onunload | Fires immediately prior to the document being unloaded (when navigating to another document).

Events Unique to Dynamic HTML

In addition, there are some events in Dynamic HTML that do not have equivalents in Visual Basic. A few of the more interesting of these are shown in the following table:

DHTML Event | Usage
onabort | Fires when the user aborts the download of an image or page element by pressing the Stop button.
onreset | Fires when the user selects a Reset button on a form.
onsubmit | Fires when the user selects a Submit button on a form. Can be used to perform validation of data on the client before sending it to the server.

For More Information See "Events" in the "Document Object Model References" section of the Internet Client SDK for more information on Dynamic HTML events.


Making Page Elements Programmable

Elements on a page in a DHTML application are capable of being handled programmatically — that is, you can respond to events for the elements and write code that tells the system what to do when an event is fired. You choose which elements can be programmed by ensuring that those elements for which you want to write code have unique IDs. Only elements with IDs can be programmed.

Note Some elements are automatically assigned an ID when you add them to a page. For those that are not, you must add the ID yourself by entering a value for the ID property.

An ID is an attribute of an HTML tag that provides a unique identifier for an element on the page. For example, you may have multiple paragraph elements in your document, each marked with the standard <P> HTML tag. Without an ID, there is no way to tell one of these elements from another. Suppose you want to perform an operation on a single paragraph in your page, such as turning all of the text in that paragraph blue, but you do not want to apply that change to any other paragraphs in the page. Without an ID attribute, you cannot specify that the operation should affect any one <P>, or paragraph. However, if you assign a unique ID to the paragraph you want to change, you can reference just that paragraph in your code.

Note For Visual Basic, all of the IDs on a page must be unique. This is not true of HTML pages outside of Visual Basic, so it is possible that you may import a page that contains duplicate IDs. Visual Basic will automatically append numbers to the end of any nonunique IDs during the import process.

After you add an ID to an element, it appears in bold in the treeview. This shows you at a glance the elements that are currently programmable in your page. You can double-click on any programmable element in the treeview or detail panel to move to the Code Editor window for that element.

Name vs. ID

In forms-based Visual Basic applications, the Name property acts as the main identifying property for controls on the page. For HTML elements, the ID property is the value that uniquely identifies the element in code. In most cases, the Name property does not play an important role in the life of elements on an HTML page.

There is one situation in which the Name property is useful for HTML page elements: making a set of option buttons mutually exclusive. For example, suppose you have three option buttons that are intended to identify how the user is going to pay for an order: with cash, with a credit card, or with a check. In a forms-based application, you would make these elements part of a control array, thus allowing the user to select only one of them. In a DHTML application, you use the Name property to produce the same result. For each option button you want to group together, you set the Name property to the exact same value. For example, you might set the Name property for these three options to "PaymentOptions." This makes the option buttons function as a group — when one is selected, the other two are automatically deselected.

Note Even though you assign the same Name property to the three option buttons in this example, they must still have unique ID values. The value of the ID property must always be unique in Visual Basic.

To make an element programmable

  1. In either the treeview or the detail panel, click the element for which you want to assign an ID.

  2. In the Properties window, enter a value for the ID property, then press ENTER.

    The system adds the element to the Object list in the Code Editor window, and makes the name of the element bold in the treeview.

To access the Code Editor window for a programmable element

  • Double-click on the element in the designer's treeview or detail panel.


Enabling the Default Action for an Element

Many elements on an HTML page have a default action. For example, the default action for a hyperlink is to launch the specified Web page when clicked. When you write Visual Basic code for an object on the page, you must include a statement that enables the default action to occur after your procedure is processed.

In Internet Explorer 4.x, when you attach an event to any object, the system returns a True or False value when the event is fired. A return value of True allows the default action to occur, and a value of False prevents the action from occurring. Visual Basic always defaults booleans to False, so by default, any event procedure the system runs prevents the default action from occurring. To avoid this problem, you must include a line of code that returns a value of True in order for the action to occur.

For example, suppose you have the following hyperlink on your HTML page:

<a href = "http://www.microsoft.com">Go to Microsoft</a>

You want to write additional code that intercepts this request and performs a procedure before the link is followed to the Microsoft site. The following code shows the correct way to do this:

Private Function Hyperlink1_onclick() as Boolean
   ‘ Your event procedure code here

   ‘ Send a true value to the system to request default processing
   Hyperlink1_onclick = True
End Function

If you leave out the line of code that sends the True value, the system will process the code you’ve written, but will not open the Web site specified in the hyperlink tag.


Setting and Retrieving Properties for a Web Page

Using Visual Basic and Dynamic HTML, you can easily change the styles of elements on the pages that make up your application's user interface. You do by manipulating the properties of page elements.

Setting and retrieving properties for elements on an HTML page works differently than it does for controls on a form. In a form, you access the properties of a control directly by referencing the name of the control and the name of the property you want. For most elements in a Web page, you must use a secondary object known as the Style object to access properties for an element.

Working with Physical Properties on Page Elements

When you want to set a physical property such as background color, you use a collection called Style to access the appropriate property. The following table shows how you set and retrieve properties in standard Visual Basic applications and in DHTML applications. In this example, properties are being set and retrieved for a command button with the ID "Button1."

Setting and Retrieving Property Values

Setting a Property Retrieving a Property Value
In Visual Basic
Button1.BackColor = vbBlue

Dim Color as String
Color = Button1.backcolor
In Dynamic HTML
Button1.style.backgroundColor = "blue"

Dim Color as String
Color = Button1.style.backgroundColor

Note Color names in DHTML applications are different from the color names you use in forms-based Visual Basic applications. When you specify a color for a page element, you must either specify an RGB value (such as #5F9EA0) or a color label as specified by Internet Explorer 4 (such as "cadetblue"). A full list of the color names supported by Internet Explorer 4 can be found in the Color Table topic in the Dynamic HTML section of the Internet Client SDK.

Working with Physical Properties on the Document Object

The only element within an HTML page to which the Style object syntax does not apply is the document itself. You do not use the style property to set the background color or other physical properties for the document. Instead, you access those properties directly as shown in the following code:

Document.bgcolor = "lightyellow"
Document.fgcolor = "slateblue"

The following code shows how you retrieve the RGB value for the background color for the Document object:

Dim Color as string
Color = Document.bgcolor

Note The fgcolor property sets the color for the page's text elements only.

Common Styles for HTML Elements

The style properties you use to set the physical appearance of an HTML element are named differently than their corresponding properties in Visual Basic. The following table lists a few of the more frequently used styles and explains their use.

DHTML Style | Purpose
backgroundcolor | Setting the background color of all elements except the Document object, which represents the body of the page. For the page itself, the property is called bgcolor.
border | Setting a border around any element. All elements on an HTML page can have a border, including paragraphs of text.
color | Setting the foreground color for all elements except the Document object, which represents the body of the page. For the page itself, the property is called fgcolor.
font | Setting the font for the element. A series of related properties (fontfamily, fontsize, fontstyle, etc.) are used to fine-tune the appearance of the font.
margin | Controlling the distance between the border of the element and the edge of the page. This property can be set to control all of the element's margins, or you can set the top, bottom, left, and right margins independently through a series of related properties.
padding | Controlling the distance between the inner text of the element and its border. This property can be set to control the padding space on all sides of the elements, or you can set the padding of the top, bottom, left, and right sides independently through a series of related properties.
textdecoration | Formatting text within an element. You use this property to make the text blink, or to display it with a strikethrough, underline, or overline.

For More Information For more information on the Style collection, any of the properties listed above, or other style-related properties in Dynamic HTML, see "Dynamic HTML" in the Internet Client SDK, available on your MSDN CD.


Retrieving Data from an HTML Page

You can retrieve data from elements on an HTML page using Dynamic HTML. This allows you to retrieve and manipulate data without making trips to the server for processing. Using properties of the objects on the page, you can gather data, perform calculations on it in your Visual Basic code, and display responses without ever transferring processing from the client to the server, which can increase your response time to user actions and requests.

For example, suppose you are working with an application that allows users to query books from a database by entering the author's name, the title of the book, or the call number of the book in text fields on a search page. The text fields are named Author, Title, and CallNo. When a query is submitted from the application's search page, the application must retrieve the value from these text fields.

The following code shows how you could use variables to retrieve the values from these fields:

Private function cmdSearch_onclick() As Boolean
   'Create variables to contain the search criteria.
   Dim sAuthor as String
   Dim sTitle as String
   Dim sCallNo as String

   'Retrieve criteria from the page.
   sAuthor=Me.author.Value
   sTitle=Me.title.Value
   sCallNo=Me.callno.Value

   'Code here to process the data and return the query.
End Function

This code uses the Value property of the HTML text fields to retrieve the fields' data. Your code would open a database connection, create a recordset, and return the appropriate records. This data could then be sent to the user.


Handling Text in the Page Designer

Text replacements are a frequent operation in many Visual Basic applications. Often you want to change the text displayed in a form element in response to a user event. In forms-based Visual Basic applications, you usually do this by setting the Text property of the control in question.

For the HTML pages in a DHTML application, text replacements are handled differently than they are on forms. It is important to remember that a standard text element in an HTML page consists of both a set of tags and of text that is embedded between them. For example, the following is the HTML for a typical heading element on a page:

<H3 ID=Subhead1>My Heading</H3>

The <H3> and </H3> tags tell the browser how to format and display the text "My Heading." The ID=Subhead1 portion is an attribute that assigns a unique ID to the heading and makes it programmable.

When you want to make changes to text on an HTML page, you must specify whether you are replacing it with straight text or text that includes additional HTML tags. Additionally, you must indicate whether the original tags for the element should be affected by the replacement. You do this with two sets of properties: innerText and outerText, and innerHTML and outerHTML.

Changing Text Within HTML Tags

To change text within HTML tags, use the innerText or innerHTML properties. These properties both replace the existing text without changing the tags around the text. They differ in how the system processes them:

  • innerText provides changes that the system inserts as straight text, without performing any parsing.

  • innerHTML provides text replacements and additional HTML tags that must be parsed and inserted between the original tags.

The following table shows how you would use the innerText and innerHTML properties to change an element:

Using innerText | Using innerHTML
Original HTML for the element | <H3 ID=Subhead1>My Heading</H3> | <H3 ID=Subhead1>My Heading</H3>
Replacement code
Subhead1.innerText="Heading One"

Subhead1.innerHTML="<I>Heading One</I>"
Resulting HTML | <H3 ID=Subhead1>Heading One</H3> | <H3 Subhead1><I>Heading One</I></H3>
Element displayed in browser
Heading One
Heading One

Replacing Text and Tags

To change both the text and the original tags around an element, you can use two properties called outerText and outerHTML. Both properties replace the text enclosed in the HTML for a specific element and the element tags themselves. Like innerText and innerHTML, they differ in terms of how the system processes them:

  • outerText provides changes that the system inserts as straight text, without performing any parsing. This text replaces the entire original element, including its HTML tags.

  • outerHTML provides text replacements and additional HTML tags that must be parsed and inserted in place of the original tags.

The following table shows how you would use the outerText and outerHTML properties to replace the original element with text alone or text and tags:

Using outerText | Using outerHTML
Original HTML for the element | <DIV><H3 ID=Subhead1>My Heading</H3></DIV> | <H3 ID=Subhead1>My Heading</H3>
Replacement code
Subhead1.outerHTML=Heading Two

Subhead1.outerHTML=
<P ID=Newpar><U>Heading Two</U></P>
Resulting HTML | Heading Two | <P ID=Newpar><U>Heading Two</U></P>
Element displayed in the browser
Heading Two
Heading Two

If you use OuterHTML to change the ID for an element, any Visual Basic code you wrote for events on the old element ID will no longer work. You must either copy your old code to the appropriate procedures for the new element ID, or leave the ID unchanged.

Note In addition to replacing text, you can append text to an existing element with the insertAdjacentHTML property.

For More Information See "Properties" in the Document Object Model section of the Internet Client SDK for more information on how to use innerText, outerText, innerHTML, outerHTML, and insertAdjacentHTML.


Navigating in DHTML Applications

You can write code in your DHTML application to allow the end user to move from one page to another. If you have multiple page designers in your application, you can move from one page to another within your application, or you can navigate to an external Web site outside of the application.

There are two main ways you can allow for navigation:

  • Program a hyperlink to move to the desired location by setting its HREF property.

  • Program another element, such as a button, to move to a new page when activated.

Navigating with Hyperlinks

You can easily provide navigation to another page by including a hyperlink element in your page. You add the hyperlink to your page, then set a property called HREF that tells the system where to go when the link is selected.

After you add the hyperlink to your page, you indicate the location to which it should jump by setting the HREF property for the element. You can type in a full or relative location to another page on the Web site, whether that page is part of your application or not.

Note It is best to use relative references to the pages to which you want to jump unless those pages are located on external Web sites. If they are pages that you intend to deploy with the application, make sure that your directory structure on the development machine mimics the structure on the site to which you will deploy, then use a relative path to the file. For Web pages on external sites, use a direct reference to the exact location.

To navigate using a hyperlink

  1. In either panel of the designer window, click the hyperlink you want to use to navigate.
    • If you need to add a new hyperlink to your page, drag the Hyperlink icon from the toolbox to the page.

    • To convert existing text to a hyperlink, highlight the text and click the Make Selection into Link icon from the designer toolbar.

    • If you want to use an existing hyperlink on the page, select the hyperlink and make sure that its ID property is set.
  2. In the Properties window, enter a full or relative path to another page in the HREF property.

    Note To use an absolute reference, enter your link in the following format:

    http://www.server.com/directory/page.htm

    Test your link by running the project.

Navigating Programmatically

In addition to using the HREF property on the Hyperlink element, you can programmatically allow any other element to navigate to another page. The most common example of this would be to create a button which, when clicked, moves users backwards or forwards in your application.

Note While it is possible to use other elements in addition to the button or the hyperlink to move from page to page, it may not be intuitive to your users that elements are being used in this way. Use principles of good design when determining what navigational features you want to include in your application.

After you add the element to your page, you can write code for it that tells the system how to respond when the element is activated. In this case, your code will tell the browser to navigate to another page.

The following code shows an example of how you would use a button to move from the current page in your application to another called DHTMLPage2:

Private Function Button1_onclick() As Boolean
BaseWindow.navigate "Project1.DHTMLPage2.html"
End Function

In this code the navigate method of the BaseWindow object is used to move to the desired location. Project1.DHTMLPage2.html is the name assigned to the page when the project is compiled.

To navigate using an element other than a hyperlink

  1. In the page designer, click the element you want to use to navigate.
    • If you need to add a new element to your page, drag the appropriate icon from the toolbox to the page.

    • If you want to use an existing element on the page, select the element in either panel of the designer window.
  2. Verify that the element has an ID, by checking the contents of the ID property in the Properties window. If the element does not have an ID, add a value for this property.

  3. Access the Code Editor window for the element, and select the appropriate event from the Event list.

  4. Write your code as shown in the example above.

  5. Compile your project and test.


Dynamically Creating Elements and Events

In addition to laying out your page elements in the page designer, you can create HTML elements and insert them on the page from code. You might do this if you want to create a page in response to a user's actions. For example, you might dynamically create a page and its contents in response to a query. You create and insert an HTML element by writing HTML in your code, using the DHTML object model's insertAdjacentHTML method.

Example: Retrieving Customer Information

Suppose you are building an application in which a search button launches a database query that returns a customer's last name and order number. You want to create page elements on the fly to contain your query results.

The first thing the code must do in this example is retrieve the user's search criteria from two text fields on the page. These fields have the IDs "custLN," for the last name for which the user wants to search, and "orderNO" for the order number. In this case, you could use two variables to retrieve the search criteria from these fields:

Private Function cmdSearch_onclick() As Boolean
   Dim sLastName As String
   Dim sOrderNum As String
   Dim divCustInfo as HTMLDivElement
   'Retrieve the search criteria and store it in the variables
   SLastName = Me.custLN.Value
   SorderNum = Me.orderNO.Value
End Function

Example: Creating Elements and Replacing Contents

After you retrieve the values from the query page, you will perform the query and send the results to the user by creating and populating a DIV tag on a results page. There are a series of steps you need to perform in this process:

  1. Retrieve the query results from a database.

  2. Check to make sure the DIV to contain the results does not already exist. If the query has previously been run, the DIV tag may already exist on the page. If the query has not been run before, you must create the DIV tag in your code.

  3. If necessary, create the DIV tag and other necessary elements.

  4. Insert your results set into the DIV tag on the results page.

The following code shows how you would check for the DIV tag as part of the function begun in the previous section.

Private Function cmdSearch_onclick() As Boolean
   'Code here to Dim variables and retrieve search criteria

   'Check for a DIV tag called custinfo.
   Set divCustInfo = Me.Document.All("custinfo")

   'Create three sets of HTML elements if the DIV tag is not
   'found: a separator line, a DIV with text, and a programmable
   'DIV to contain the query results.
   If divCustInfo Is Nothing Then
      Me.Document.body.insertAdjacentHTML "BeforeEnd", _
        "<DIV><HR SIZE=2></DIV>"
      Me.Document.body.insertAdjacentHTML "BeforeEnd", _
        "<DIV CLASS=custhead>Customer Information</DIV>
      Me.Document.body.insertAdjacentHTML "BeforeEnd", _
        "<DIV id=custinfo>"

      'Run the query using a CustomerQuery function and insert the
      'results in the custinfo DIV tag.
      If sLastName <> "" Then
         Me.Document.body.insertAdjacentHTML "BeforeEnd", _
           CustomerQuery(sLastName)
      EndIf

      'Create and insert the closing tag for the programmable DIV
      'element.
      Me.Document.body.insertAdjacentHTML "BeforeEnd", "</DIV>"

   'If the DIV tag already exists, run the CustomerQuery function and
   'replace the element with the query results.
   Else
      DivCustInfo.innerHTML = CustomerQuery(sLastName)
   EndIf
End Function


Managing State in DHTML Applications

Typically, Internet applications are stateless, in that the protocol that passes requests and responses between the browser and Web server is not capable of maintaining information between each request. Therefore, the protocol does not "remember" any information from previous requests when it receives a new request. This is also generally true of DHTML applications, in that the browser does not usually store detailed information between actions.

Because the protocol itself cannot maintain state, you must use other means to store information you want the application to remember between requests. In a DHTML application, you use the GetProperty and PutProperty functions to store and retrieve data. These functions store data as long as the end user's Web browser window is open.

Note The GetProperty and PutProperty functions are part of the modDHTML module that is added to your project when you create a new project using the DHTML Application template.

You store information using the PutProperty function. In this function, you identify the name of the property in which you want to store information, and the value to store. The browser stores the indicated data in a client-side cookie. The PutProperty function exposes cookie functionality in a way that is simpler to use than directly coding cookies in your event procedures.

You retrieve information using the GetProperty function. In this function, you identify the name of an existing property that you want to retrieve from the client-side cookie. You can assign this value to fields on the page, or use it in your calculations.

Example: Saving and Retrieving State

For example, suppose you want to build a simple application that will store and retrieve data across page boundaries. The application will consist of two pages: one to store values, and one to retrieve them.

The first page in the application contains a text field into which the end user can enter a value, and a button that stores the value to the cookie. The code for the button, called StoreButton1, would look like this:

Private Function StoreButton1_onclick() As Boolean
   PutProperty BaseWindow.Document, "Property1", TextField1.Value
End Function

The second page in the same application contains a button that, when clicked, retrieves the property from the cookie. The code for that button, called "GetButton1", would look like this:

Private Function GetButton1_onclick() As Boolean
   MyValue.innerText = "The value of the property is " & _
      GetProperty(BaseWindow.Document, "Property1")
End Function

This function uses the innerText property of Dynamic HTML to replace the contents of a DIV tag, called "MyValue," with a sentence which contains the retrieved property.


Testing Your DHTML Application

When you debug a DHTML application, you have the full Visual Basic development environment at your disposal. You can use all the tools available in Visual Basic — breakpoints, watch variables, debug statements, and so on — to debug your project.

Note A file named VB6DEBUG.DLL enables you to debug your DHTML application across processes. Do not delete this file or you will not be able to test your application.

The settings in the Debugging tab of the Project Properties dialog box determine whether the system waits for you to tell it what to do when you go into run mode or automatically starts a page designer you specify, displaying its page in the browser. You can also indicate whether to open a new instance of Internet Explorer or use an existing instance.

When Visual Basic runs the application, it inserts an OBJECT tag into the body of each HTML page used in your application. The OBJECT tag is used to download the HTML page when the application is deployed. The tag includes a classID used to include of find the page in the Windows registry, an ID used to reference the page in the run-time DLL, and a CODEBASE tag used to tell the browser where to find the page if it’s not already on the client’s computer.

For More Information See "Debugging Your Code and Handling Errors" in the Programmer’s Guide for more information on how to test and debug your applications.


Building Your DHTML Application

DHTML applications are compiled the same way any Visual Basic project is compiled — by choosing Make from the File menu. A DHTML application must be built as an in-process component, or a DLL file.

Project Files in DHTML Applications

Like standard Visual Basic projects, DHTML application projects have code modules and visual designers — in this case, the DHTML Page designer. Visual Basic creates a page designer for each HTML page in your application. For each page designer, Visual Basic stores the following:

  • A .dsr file. .Dsr files are plain text files that contain the source code for the designer and references to the HTML pages they contain.

  • A generated .dsx file that contains binary information about the designer.

  • The HTML pages associated with an application are saved to the location you specify.

Any files associated with the HTML pages are also a part of your project, in that they must be deployed with the project. For example, if your HTML page references a series of images, the image files themselves must be deployed with the project. See "Deploying Your DHTML Application" later in this chapter for more information.

Compiled Files in DHTML Applications

When you compile a DHTML application, Visual Basic creates a DLL containing the page designer and its code. The HTML pages and their associated files are stored separately from the DLL and must be deployed with it.

During compile, Visual Basic creates several additional files. These include your project file, a .vbw file that contains layout information on your project, an .exp file, and a library file.

To build your DHTML application

  1. If the project is still in run mode, click the End button to return to design mode.

  2. Set the BuildFile property to the full path and file name to which you want to build the project.

    Note By default, the BuildFile property is set to the value of the SourceFile property.

  3. On the File menu, click Make Project.dll to open the Make Project dialog box. Click OK to make the DLL file.


Deploying Your DHTML Application

You package and deploy your DHTML application using the Visual Basic Package and Deployment Wizard. The Package and Deployment Wizard packages your project .dll files and all associated files into a "cabinet" or .cab file. The wizard can then deploy this cabinet file and its associated support files to a location you indicate on a Web server.

Note The .cab files you generate for your DHTML applications should be digitally signed prior to deploymentSee "Digital Signing for ActiveX Components" in "Downloading ActiveX Components" for an explanation of how to sign your files.

Files to Deploy

The application files that must be deployed include:

  • The .dll files for the project — including the Visual Basic run-time DLL and the .dsr and .dsx files for the project. These are automatically packaged into the .cab file when you run the Package and Deployment Wizard.

  • The HTML page or pages associated with the project. The Package and Deployment Wizard does not package these into the .cab file, but it copies them to the indicated location on the Web site when you deploy the .cab file.

  • Any files referenced by the HTML pages, such as images. The Package and Deployment Wizard does not automatically recognize these dependencies, but you can add them to the list of additional files to deploy.

The files end users need to download for a DHTML application vary depending on whether or not they have previously downloaded an application or not. If the end user is downloading for the first time, he must download the Visual Basic run-time DLL and the page designer run-time DLL in addition to the files associated with the application. If he has previously downloaded a DHTML application, the user does not need to download the run-time components again.

The Deployment Process

These are the steps in deploying your DHTML application to the Internet:

  1. Debug and compile your application.

  2. Use the Package and Deployment Wizard to build a .cab file that contains the necessary files for your application.

  3. Digitally sign your .cab file and rebuild.

  4. Use the Package and Deployment Wizard to deploy your application to the Web server you want to use.

  5. Manually copy any files associated with your application’s HTML pages, such as images, to the necessary location on your Web server.

    Note You can also use the Package and Deployment Wizard to perform this step.

  6. Test the pages in your application to make sure that all the links to associated files still work. If not, you may have to adjust the location of your files on the Web server to match the URLs in the HTML pages.

For More Information See "Distributing Your Applications" in the Programmer's Guide for instructions on using the Package and Deployment Wizard to distribute your DHTML application.


Developing IIS Applications

An IIS application is a Visual Basic application that uses a combination of HTML and compiled Visual Basic code in a dynamic, browser-based application. An IIS application resides on a Web server, where it receives requests from a browser, runs code associated with the requests, and returns responses to the browser.

In its simplest form, you can use an IIS application to intercept a user request and return an HTML page to the browser. These are just a few of the things you can do with more advanced IIS applications:

  • Query databases in response to a user's request, writing information to and from records.

  • Retrieve HTML pages and replace portions of them with dynamic content before sending them to the browser.

  • Dynamically create HTML elements and generate events for them on the fly, at run time.

With these or other advanced uses of IIS applications, you can perform complicated processing based on the actions users perform in the browser. IIS applications can use Visual Basic code to perform much of the same processing you might have previously done with script, CGI processing, and other methods of Internet application development.

IIS applications are named for Microsoft Internet Information Server, the Web server you use to run your IIS applications. IIS applications can run in any browser, on the Internet or an intranet, and therefore make it easy to reach a broad audience.

This discussion assumes that you are familiar with some basic Internet concepts that are covered in "Introduction to Internet Applications." See it for more information on Internet technologies in general, including HTTP, the Internet development environment, and models of Web browser and Web server interaction.

For More Information See "System Requirements for IIS Applications" for more information on the system configuration needed to develop and test IIS applications.

Topics

What is an IIS Application?

Introduces and defines IIS applications, explains their advantages, and presents the structure of a typical IIS application.
An Introduction to Webclasses

Introduces webclasses and their contents.
Design Considerations for IIS Applications

Presents factors to keep in mind when planning your IIS applications.
IIS Application Development Process

Presents detailed instructions on how to create IIS applications.
Webclass Events

Describes the events in an IIS application, how to add them to the application, and how to activate them.
The Object Model for IIS Applications

Explains the Active Server Pages objects you use in your IIS application.
Responding to Events in IIS Applications

Explains how to write code for common tasks in your IIS applications.
State Management in IIS Applications

Explains a variety of methods for storing and retrieving state in an IIS application.
Handling Sequencing in Webclasses

Describes how to set an order of navigation from webitem to webitem in your application.
Building Your IIS Application

Presents information on compiling your IIS applications.
Debugging Your IIS Application

Presents information on how to test and debug your IIS applications.
Deploying Your IIS Application

Describes how to deliver your IIS application to the Web server and to your users.

Sample Application

Wcdemo.vbp

Demonstrates several key features of webclass programming in Visual Basic. In this application, you can see examples of how to fire template and custom events, how to dynamically generate pages in response to user requests, how to use custom webitems, and how to use the URLData property to store state information between requests. Wcdemo.vbp is listed in the Samples directory.


What is a IIS Application?

What is an IIS Application?

An IIS (Internet Information Server) application is a Visual Basic application that lives on a Web server and responds to requests from the browser. An IIS application uses HTML to present its user interface and uses compiled Visual Basic code to process requests and respond to events in the browser.

To the user, an IIS application appears to be made up of a series of HTML pages. To the developer, an IIS application is made up of a special type of object called a webclass, that in turn contains a series of resources called webitems. The webclass acts as the central functional unit of the application, processing data from the browser and sending information to the users. You define a series of procedures that determine how the webclass responds to these requests. The webitems are the HTML pages and other data the webclass can send to the browser in response to a request.

IIS Applications vs. ASP Applications

IIS applications bear a superficial resemblance to Active Server Pages applications. Both types of applications present dynamic Web sites and perform their processing on the server rather than the client. However, each has its unique advantages. Active Server Pages are for script developers interested in authoring Web pages, and offer the unique capability of intermingling script with HTML. IIS applications are for Visual Basic developers building Web-based applications, rather than Web pages. IIS applications allow for complicated business processing and easy access from almost any browser or platform.

IIS Applications vs. DHTML Applications

An IIS application is also similar to another type of Internet application you can create in Visual Basic — a DHTML application. Like IIS applications, DHTML applications also allow you to respond to events in an HTML page. However, there are several key differences between the two types of applications:

  • Dependency — DHTML applications are intended for use on intranets, and are dependent on Internet Explorer 4.0 or later, while IIS applications can be used on the Internet or an intranet. End users of an IIS application do not need a specific operating system or browser.

  • Object model — DHTML applications use a different object model than IIS applications to access and work with the elements on an HTML page. While IIS applications use the Active Server Pages object model, DHTML applications use the Dynamic HTML object model.

  • Location of processing — IIS applications are designed to perform most of their processing on the Web server, but DHTML applications perform most of their processing on the browser machine. You do not create any Web server components when you create a DHTML application.

For More Information See "A History of Development on the Internet" in Chapter 1, "Developing Internet Applications," for more information on the differences between IIS applications and DHTML applications.


Advantages of IIS Applications

Building a server-side Internet application in Visual Basic gives you several advantages over other methods of Internet development, including:

  • Reduced cost of deployment per user. End users of an IIS application can run the application using only a browser; no special software needs to be installed on their computers for the application to work.

  • A familiar development environment and model. You can leverage your knowledge of Visual Basic by using the Visual Basic programming environment and standard, compiled Visual Basic code. In addition, you can add classes, modules, or any Visual Basic ActiveX component to your project.

  • Access to a broad audience. IIS applications work with a wide variety of browsers and operating systems, so you can easily reach a wide audience.

  • An object model that gives you direct access to the resources of the Internet Information Server. The Active Server Pages framework provides an object model that allows you to directly manipulate the objects at the core of IIS. This allows you to retrieve information from a browser, send information to it, and perform complex operations on the contents of a Web page. For more information on the object model, see "The Object Model for IIS Applications."

  • Reusable components. Once you have created a webclass, you can easily access it in another webclass. For more information, see "Navigating Between Webclasses."

  • Separation of code and HTML. Unlike scripting, your code is not embedded in the HTML document, so you can separate the process of designing the application's user interface from writing, testing, and debugging its code.

  • State management across multiple interactions with the client. You can manage state using objects or a database, or you can shuttle state between the client and the server. For more information, see "State Management in IIS Applications."

  • Streamlined processing. You do not have to create the HTML template files your application sends to the browser, if you do not want to. In Visual Basic Internet application development, the process of designing your user interface is separated from the process of developing and coding your application. You can have a designer create the template files you want to use.


System Requirements for IIS Applications

System requirements for IIS applications vary for the development computer, the deployment computer, and the end-user's computer.

The Development Computer

To create and test IIS applications, you need to have Internet Explorer 4.0 and a Web server installed on your development computer that is capable of running Active Server Pages. You can use any of the combinations in the table below for development purposes:

Operating System | Web Server
Windows NT Server 4.0 or later | Internet Information Server 3.0 or later, with Active Server Pages
Windows NT Workstation 4.0 or later | Peer Web Services 3.0 or later, with Active Server Pages
Windows 95 or later | Personal Web Server 3.0 or later, with Active Server Pages

Important If you are using Windows NT Server or Windows NT Workstation, you must have Service Pack 3.0 installed.

Important The version of Personal Web Server installed with Microsoft FrontPage 98 does not run Active Server Pages and will not work with IIS applications. If you are using Personal Web Server, you must install Personal Web Server for Windows 95 or Windows NT Service Pack 3.0 or later.

The Deployment Computer

For deployment purposes, you should have Internet Information Server (with Active Server Pages) installed on your Web server, along with any supplemental system such as database applications that are needed by your application.

The End-User Computer

End users of an IIS application must have a browser installed. Although Internet Explorer 4.0 or later is recommended, any browser can act as the front end of an IIS application.

Note If your webclass is set to stay alive between requests, the end user must have a browser that supports cookies and cookies must be enabled. For more information, see "Moving State Between the Browser and the Web Server."


Webclasses

An Introduction to Webclasses

A webclass is a Visual Basic component that resides on a Web server and responds to input from the browser. When you create an IIS application, you create its webclasses using the Webclass Designer. Webclasses typically contain webitems and the code that delivers those webitems to a client.

Note There is a one-to-one relationship between the Webclass Designer and the webclass. If you want to add additional webclasses to your application, you must add additional designers.

A webclass is associated with one and only one client for its entire life cycle. Visual Basic creates a logical instance of the webclass for each client that accesses it. However, for each client, the webclass is capable of maintaining state between requests.

For More Information See "State Management in IIS Applications" for more information about maintaining state in a webclass.

Webclasses and .ASP Files

Each webclass in an IIS application has an associated .asp (Active Server Pages) file that Visual Basic generates automatically during the compile or debug process. The .asp file hosts the webclass on the Web server. In addition, it generates the webclass's run-time component when the application is first started and launches the first event in the webclass's life cycle.

The following figure shows the relationship between .asp files, webclasses, and webclass contents.

Relationship Between .asp Files and Webclasses

As shown in this picture, there is a one-to-one relationship between webclasses and .asp files — each webclass has its own ASP. In turn, a webclass can have many webitems associated with it.

The path to the ASP acts as the base URL for the webclass and its webitems. A URL is a standard way of indicating the location of a document or other item that is available electronically. (For example, http://www.microsoft.com is a URL.) When you want to go to a page on the Web, you enter the URL for the page and the browser retrieves and displays it. You can indicate the URL directly, by typing it into the location line of your browser, or indirectly, by selecting a link that tells the browser the correct URL to which it should go. A base URL refers to the webclass itself, and can act as an entry point into the application.

For example, suppose you have a project called Project 1, that contains a webclass called CustomerInquiry. You specify in the NameInURL property that the ASP should be named Project1_CustomerInquiry. Visual Basic creates the ASP and gives it this name when you compile the project, and stores it in the specified directory, called CustomerSupport, on the Web server you specify: www.mycompany.com. The base URL for your webclass would be the following:

http://www.mycompany.com/CustomerSupport/Project1_CustomerInquiry.asp

To start your IIS application, the user accesses this URL with their browser.

Webclass Contents: Templates and Custom Webitems

A webclass typically contains webitems that it uses to provide content to the browser and expose events. A webitem can be one of two things:

  • An HTML template file — HTML template files are HTML pages that you associate with your webclass. When the webclass receives a request, it can send the HTML pages to the browser for display. Templates differ from regular HTML pages only in that they often contain replacement areas the webclass can process before sending the page to the browser. This allows you to customize your response.

  • A custom webitem — Custom webitems do not have an associated HTML page they can return to the user. Instead, a custom webitem is a programmatic resource that consists of one or more event handlers that are logically grouped together to help organize your Web application. These event handlers are called from the browser, either when the page loads or when a user selects an HTML element. The event handlers can generate a response to the browser or pass processing to another of the webclass's webitems.

Templates and custom webitems both expose events that the webclass processes when certain actions occur in the browser. You can write event procedures for these events using standard Visual Basic code, thus linking the actions that occur on a Web page to Visual Basic processing.

Each webclass can contain multiple templates and webitems. In most cases, you will need only one webclass in your application, but you might want to use multiple webclasses if you want to break up your application into parts that can be reused in other applications.

For More Information See "Webclass Events" for more information on template and custom webitem events. See "Adding HTML Templates to the Webclass" for more information on template webitems. See "Adding Custom WebItems to a Webclass" for more information on custom webitems.

IIS Applications vs. Traditional Visual Basic Applications

IIS applications are structured differently than standard, forms-based Visual Basic applications. In an IIS application, the user interface consists of a series of HTML pages rather than traditional Visual Basic forms. An HTML page is like a form in that it contains all the visual elements that make up your application's user interface. You can place some of the same items into a page as you do a form, including text, buttons, check boxes, and option buttons.

An HTML page referenced in an IIS application is saved to an .htm or .html file that is analogous to a .frm file, in that it is used to render and display the page to the end user. Visual Basic creates the .htm file from the original HTML page when you save or debug your application, or when you use the Edit HTML Template menu command.

In an IIS application, you do not use Visual Basic to create the HTML pages that make up the application's user interface. A Web designer or a developer creates the pages using an HTML editor, a word processing package, or a text editor, and you link the finished pages into your webclass.

The following table sums up the differences between forms-based applications and Web-based applications:

 | Forms-based application | Web-based application
User interface | Visual Basic forms | HTML pages
UI elements (for example, text boxes, buttons, images) | Controls | Elements
File format | .frm files | .htm files
Creator | Developer | Web designer with a developer, or the developer alone
Run time | Visual Basic run time | Web browser

Structure of IIS Applications

An IIS application consists of the following pieces. Many of these are generated for you automatically when you build your project. The pieces include:

  • One or more webclasses, which are generated automatically when you create a webclass project.

  • One or more HTML templates and their events.

  • One or more custom webitems and their events.

    Note A webclass may contains a mixture of templates and custom webitems, only templates, or only custom webitems. You do not necessarily have to have both templates and custom webitems in your webclasses.

  • An .asp (Active Server Pages) file used to host the webclass in Internet Information Server (IIS). The .asp is generated automatically when you create a webclass project, and given the name you specify in the NameInURL property.

  • A webclass run-time component, MSWCRUN.DLL, that helps process requests.

  • A project DLL (generated automatically on compile) that contains your Visual Basic code and is accessed by the run-time component.

The following figure shows how the server portions of an IIS application work together:

Structure of IIS Applications

Like other Visual Basic applications, an IIS application has code modules and a visual designer. IIS application objects are stored in plain text files that contain the source code of the webclass, events and property settings, and the webitems for the webclass. Visual Basic uses the extension .dsr for these files. In addition to the .dsr file, Visual Basic generates a .dsx file that contains a binary version of the application.


Design Considerations for IIS Applications

There are several factors you should keep in mind when creating an IIS application. These include deciding on a consistent directory structure, using paths that will make your deployment work smoothly, and considering the unique navigational considerations of Web applications.

General Considerations

  • Use relative URLs to images and related files. Your application and its HTML pages can be deployed onto a Web server under a different parent directory than the one on the development computer. Because of this, it is best to use relative URLs in your HTML pages rather than absolute URLs. Absolute URLs indicate the exact drive and directory in which your HTML page will expect to find any related images or other files it references. Relative URLs give the name of the file to locate and indicate its location in relation to your project directory, specifying how many directories up or down to move to find the reference.

  • Anticipate your Web server directory structure. During design, think about the Web server directory structure you will use when you deploy your application, and use the same directory structure on your development machine. Your project files — including the designer, its DLL, any template files, and any additional files the templates reference (such as .gifs) — must be stored in the project directory or in subdirectories below it. For more information see "Managing your Project Files."

  • Use generated URLs. Use generated URLs whenever possible to move to other webitems or pages, rather than typing a manual URL (http://www.myserver.com/mypage.htm) into your webclass templates or code. For more information, see "Specifying URLs for Webitems."

  • Gather request resources with BeginRequest. Use the BeginRequest event to gather expensive server-side resources that the webclass should not hold longer than the duration of a request. Release those resources with the EndRequest event.

  • Use ADO data features. When working with databases in your webclass code, use ODBC connection pooling and ADO disconnected recordsets. For more information, search the MSDN library for ActiveX Data Objects.

  • Review your state management options carefully. When planning your application, it is important to read through the information on state management and choose the most appropriate method for your needs. For more information, see "State Management in IIS Applications."

  • Be careful when using wcRetainInstance. When keeping a webclass alive between requests, be aware that Visual Basic creates apartment-model objects that it places into the Session objects. This causes IIS to bind the client to a particular thread. This may cause difficulties for your application. This may also be an issue if you put Visual Basic classes into the Session or Application objects. For more information, see "State Management in IIS Applications."

  • Do not use HTML pages that contain forms with the GET method. If you use an HTML template file that contains forms that use the GET method, you will not be able to successfully connect events and run the application. You need to make sure that all template files you use in your webclass use the POST method for any forms.

Navigational Considerations

It is difficult to predict the exact way in which users are going to interact with a browser-based application. Unlike a forms-based application, where navigation from form to form is generally fixed, users in a browser-based application can move backward and forward at any time, can jump randomly, or can close the application without completing their current process. Because of this inherent flexibility, there are several things you should keep in mind:

  • Close database transactions. Try to avoid holding open database transactions across request boundaries, because there is no guarantee that the user will return to the transaction after the initial request is made. Holding a transaction open on a database consumes expensive resources and locks a part of the database from other users. Instead, consider committing database changes at the end of every request.

  • Allow for open navigation. Structure your application so that users can navigate freely among the application's webitems, rather than assuming a fixed navigation path. You can do this by including navigational buttons and other aids that allow a user to return to the starting point from any place in the application, or including other cues that help the user figure out the appropriate navigational choice from each screen.

  • Anticipate re-submits. Consider how you will handle out-of-sequence navigation caused by use of the browser’s back button and history menu. This can be particularly important in the case of applications that use HTML forms. In these situations, the user might complete a transaction and then use the back button to return to a data entry form thinking they can make a correction and resubmit.

  • Refill data structures when a user moves backwards. Any internal data structures you use in the application must be filled appropriately. For example, suppose your startup screen in the application asks for a user name and password, which you then store in member variables. If a user is in the middle of your application and navigates back to the startup screen, you must reset the variables to their original state.


IIS Application Development Process

The process of creating an IIS application is similar to creating any other project in Visual Basic. The overall process is presented below. Details for each step in the process can be found later in this chapter.

The overall process for creating IIS applications is:

  1. Start a new project and select IIS Application as the project type.

  2. Save the project.

    Note Unlike forms-based Visual Basic applications, you must save an IIS application before you add HTML template webitems to it.

  3. Add as many HTML template webitems and custom webitems to the webclass as needed.

  4. Add any custom events to the project.

  5. Write code for all standard, template, and custom events in the project.

    Note Make sure you write code for the Start event, or your application will not run unless the user specifies a webitem in the base URL. For more information see "Setting the Start Event".

  6. Add other code modules or webclass objects to the project.

  7. Test and debug the application by running the project and viewing the application in the browser. It is recommended that you test all browsers you plan to support before releasing the application to end-users.

  8. Compile the project.

  9. Deploy the application.


Beginning Your IIS Application Project

When you begin an IIS application, you select a special type of project called an IIS Application. An IIS Application project is an ActiveX DLL project type that automatically includes an instance of the appropriate ActiveX designer. The designer, called a WebClass object, acts as the base on which you create your application.

The Webclass designer corresponds to a single webclass in your application, but it can contain multiple HTML templates and webitems. If you want to include more than one webclass in your application, you can insert additional designers into your project.

There are several properties that you should always set for the webclass.

  • StateManagement — This property, available for the webclass object in the Properties window, determines whether your webclass stays alive between browser requests or is destroyed at the completion of each request.

  • Public — Your webclass must be set to public in order for the application to run. This is controlled by the Public property, available for the webclass object in the Properties window. By default, the Public property is set to True.

  • Unattended Execution — This option, available in the Project Properties dialog box for your IIS applications, allows instances of a DLL class to be allocated on any thread. This ability is necessary to IIS in order to avoid having all webclass instances allocated to a single thread.

  • Retained in Memory — This option, available in the Project Properties dialog box for your IIS applications, allows the Visual Basic project to keep its run-time support state permanently loaded on the server's threads, allowing server programs that load Visual Basic projects to run significantly faster.

To begin your IIS application

  1. Start a new project and select IIS Application as your project type.

  2. In the Project Explorer window, open the Designers folder, then double-click on the designer to display it in the main window.

To add additional webclass designers to your project

  • On the Project menu, click Add Webclass and Visual Basic adds it to your project.

    Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

For More Information See "State Management in IIS Applications" for more information on setting the StateManagement property for your webclass. Also see "Public Property" in the Language Reference.


Adding HTML Templates to the Webclass

You add HTML templates to your IIS application to enable your webclass to send HTML pages to the browser in response to user requests. When you add a template to your webclass, you choose an HTML page you want to associate with it. Visual Basic then scans that file and looks for HTML tags that are capable of launching a request to the server. Such tags include form elements, image tags, hyperlinks, and most other tags that contain an URL reference.

Note Although a webclass can contain multiple template webitems, each template webitem can represent only one HTML page. You must add additional templates for each HTML page you want to include in your application.

Tags in the HTML page can contain attributes that launch a server request. For example, in an IMG tag, the SRC attribute is used to specify a URL location from which to retrieve a file. This attribute, and others like it that call the server, can become events in your IIS application. Visual Basic lists each of these tags and attributes in the right panel of the designer window. The name of each tag is taken from the ID attribute for that tag, if one exists. An ID attribute is an optional HTML attribute that you add to the .htm file itself. For each HTML tag in the file, you can assign a unique ID that can be used to identify that tag in script or code.

Note You can use Visual Basic to assign the name of the webclass or its templates, but you cannot use the Properties window to assign an ID to the selected tag.

If your HTML template file contains a tag that does not have an ID, the designer assigns it an ID based on its position in the HTML page. For example, the third <A> tag without an ID would be named Hyperlink3. This ID is not permanent; if you do not connect any of the tag attributes to events or webitems, the ID is not stored in the HTML page. However, when you connect one of the tag's attributes, the ID becomes permanent and is stored in the .dsr file for the page.

You must save your project before you can add a template to it. After you add the template, Visual Basic does one of the following things:

  • If the .htm file you chose for the template was located outside the project directory, Visual Basic makes a copy of the template file and places it in the project directory.

  • If the .htm file you chose for the template came from the project directory or if there is another .htm file there with the same name, the system creates a copy of the .htm file but appends a number to its name. For example, a file called OrderForm.htm would be changed to OrderForm1.htm.

The action of copying the .htm file occurs when you save or debug the project, or when you use the Edit HTML Template menu command.

Important From this point on, the .htm file in the project directory acts as the source file for your project. If you want to make changes to the HTML page's appearance, you do so in this copy of the HTML page.

If your HTML template file references any additional files, such as images, you must copy those files into the project directory or a subdirectory of it. You can do this before you add the template to the webclass or after, but you must do it before debugging or running the project.

In addition, if you import an HTML page for which a similarly named .htm file already exists in the project directory, VB appends a number to the .htm file it creates for the new file. This prevents Visual Basic from overwriting an existing .htm file if you reuse a template in more than one webclass. For example, suppose you have an HTML page called feedbk.htm. The first time you add this to a webclass, Visual Basic generates a file in the project directory with the name feedbk.htm. If you add this same file to another webclass in the application, the new .htm file Visual Basic creates is named feedbk1.htm.

For more Information For more information on the directory structure you should use for your projects, see "Managing your Project Files." For more information on tags and attributes, see "Understanding Internet Basics" in "Developing Internet Applications."

To add an HTML template to an IIS application

  1. In the Webclass Designer window, click the Add HTML Template WebItem button on the Webclass toolbar.

  2. In the Add HTML Template dialog box, select the HTML page you want, then click OK.

  3. If the template file you chose references any additional files, such as .gif or .jpg files, copy those files to the project directory or a subdirectory beneath it.

    Note If the template file contains absolute references to the previous locations of those files on the development computer, you need to make changes to the .htm file to reflect the location the files will have when deployed to the server. See "Managing Your Project Files" for more information on using relative and absolute paths in your template files.

Removing a Template from a Webclass

You can remove a template from a webclass if you no longer want to send that template file to the browser in response to requests. You cannot replace one template with another; instead you must remove the first template file and add a link to a new file. If the template files have the same name, any event procedures you wrote for the former template will be available to the new template.

For example, suppose you are working with a catalog application and want to change the order form your application uses. You delete a template file called OrderForm, then add another HTML page with the same name that contains a different order entry form. All event procedures associated with the original OrderForm file, such as OrderForm_Respond, are immediately associated with the new file.

To remove a template from the webclass

  • Right-click the template webitem and click Delete.

Editing the HTML for a Template

After you have inserted an HTML template file, you can use the HTML editor of your choice to make changes to the page's content and layout. You do so by selecting the Edit HTML Template option from the context menu for a template. Visual Basic opens the .htm file from the project directory. After you finish editing, the designer displays a prompt asking if you want to refresh the file. You must refresh the file if you want to see the changes you made.

By default, Visual Basic displays either your default HTML editor or Notepad when you edit your template. Visual Basic automatically detects your default HTML editor by looking at the system registry.

Note You can determine the program that opens when you edit your template by making an entry in the External HTML Editor field found on the Advanced tab of the Project Options dialog box.

To edit your HTML webitem

  1. In the left panel of the Designer window, right-click the template for which you want to edit the HTML, then select Edit HTML Template.

  2. Make your changes to the .htm file, save, and return to Visual Basic.

  3. If you have made any changes to the event tags in the file, right-click the webitem and select Refresh HTML Template, or respond to the prompt to refresh.

    Visual Basic parses the file.

Saving Changes to Your Template

If you want to update the webclass with changes you have made to the .htm file, you refresh the link between your template and its external file. When you refresh the template, Visual Basic checks to see whether the events currently listed for the template still exist, and whether any new attributes should be added. The program adds any new attributes and processes any changes for existing events. If an event for the template no longer exists in the .htm file, Visual Basic removes it from the webclass. Any procedures you wrote for the event are not removed — you must clean up the event procedures yourself.

If another user makes changes to an .htm file used in your webclass, you receive a prompt asking whether you want to refresh the template. This message appears each time a user saves the .htm file in an editor.

To save changes to the .htm file you've referenced

  • In the left panel of the designer window, right-click the template for which you want to refresh your import, then select Refresh HTML Template.

    Visual Basic parses the file.

For More Information See "Connecting Events in a Webclass" for more information on connecting and disconnecting events for a webitem.


Adding Custom Resources to the Webclass

Adding Custom Webitems to the Webclass

You add custom webitems to your webclass to specify programmatic resources, rather than file-based resources, for your application. Unlike template webitems, which are linked to and manipulate HTML pages, custom webitems are containers that generally group a set of code procedures you want the webclass to be able to access from multiple places in the application.

There are several ways in which custom webitems are useful:

  • Custom webitems can help you produce more modular or structured code. For example, suppose you have several HTML template files in your webclass that all contain a single image, called logo.gif, that is stored in a database. Rather than writing a separate procedure to retrieve and display this image for each IMG tag event in the webclass, you can add a custom event to a custom webitem and define the image retrieval procedure there. Each webitem can then access the custom webitem's event, using the URLFor method, instead of handling the procedure itself.

  • Custom webitems can provide a good way to encapsulate code in your IIS application that produces a frequently used HTML response, such as a standard header or a table.

  • Custom webitems can allow you to send a response to the browser at times when a template file is not a good solution or when a template is not available. For example, if you want to dynamically generate a page in response to a search request, you could do this by using a template file and parsing replacements, or you could generate the entire page within a custom webitem's Respond event.

To add a custom webitem to a webclass

  1. In the Webclass Designer window, click the Add Custom WebItem button on the toolbar. A webitem appears in the treeview, beneath the Custom WebItems folder.

  2. To rename the webitem, use the Properties window to specify a name.

To remove a custom webitem from the webclass

  • Right-click the webitem and click Delete.

For More Information See "Webclass Events" for more information on custom events. See "Specifying URLs for Webitems" for more information on using the URLFor method.


Managing your Project Files

When you edit an HTML template file, save your project, or debug the application, Visual Basic saves the webclass and its associated template .htm files to the directory you specify. When you debug the project, Visual Basic uses this directory as the IIS virtual directory the webclass needs to run. A virtual directory is a directory outside your Web server's home directory that appears to browsers as a subdirectory of the home directory. A virtual directory allows you to publish contents to the Web from directories outside the home directory structure.

Note All of the project files — including the designer and any files the templates reference — should be located either in the project directory or in a subdirectory beneath it. The .htm files for the templates must be in the main project directory.

You can think of your project directory for an IIS application as a "mirror" of the directory structure you will use on the Web server when you deploy the application. You should arrange the files for your project in the directory structure you plan to use. For example, if you plan to use a separate graphics directory on the Web server, you should use a similar subdirectory beneath your project directory.

Relative vs. Absolute URL Paths

After you link a template file to a webclass, you must make sure that the references it contains to images and other files will work correctly when you deploy your application to the server. In most cases, you can ensure this by using relative paths in the URLs that reference these images and other files.

For example, suppose you have an HTML page that references two images on your local drive, using absolute URLs — that is, URL paths that indicate the drive and directory in which to find an associated file. When you deploy the template file to the Web, these references are no longer valid. Instead, you should replace these absolute references with relative URLs — that is, URL paths that indicate the location of an item relative to the current directory, without giving a specific drive or root directory. As long as the directory structure you use on your development computer mimics the directory structure you use on the Web site, your links will resolve correctly after deployment.

There are two situations in which you can safely use absolute URL paths:

  • If your HTML template file references an image or other file that has a permanent home on a Web server, you do not need to replace an absolute reference to this image with a relative image. Absolute references to server locations will resolve correctly after you deploy your application.

  • References to external sites (such as www.microsoft.com) can be referenced with absolute URL paths.

Example: Setting Up Directories for a Simple Webclass

Suppose you have a project called feedback.vbp. The webclass in this project uses a single template file: thankyou.htm. This HTML page references two image files: a corporate logo called logo.gif and a decorative picture called banner.gif. The following table shows the original locations of the files in this project:

File | Original Location
feedback.vbp | c:\vb98\myproject\
thankyou.htm | c:\front page\
logo.gif | c:\front page\images\
banner.gif | c:\front page\images\

After you add the template to the webclass and save the project, Visual Basic creates a copy of the HTML page and stores it in the project directory. The following table shows the changes to your directory structure after adding the template. These changes happen automatically when you add the template.

File | Interim Location
feedback.vbp | c:\vb98\myproject\
thankyou.htm | c:\vb98\myproject\
logo.gif | c:\front page\images\
banner.gif | c:\front page\images\

The final step is to move the .gif files into the project directory or a subdirectory beneath it. Suppose you plan to use a separate graphics directory on the Web server to contain your images. You need to use this same structure on your development computer, so you would create a graphics subdirectory under c:\vb98\myproject\ for your images, then manually copy the files to that location. The following table shows the final directory structure for your file, before deployment. You would make these changes manually by copying the files to the project directory.

File | Final Location
feedback.vbp | c:\vb98\myproject\
thankyou.htm | c:\vb98\myproject\
logo.gif | c:\vb98\myproject\graphics\
banner.gif | c:\vb98\myproject\graphics\

Example: Using Relative Paths

After copying the graphics files to the subdirectory as shown above, you must make sure that your references to these images in the .htm file and in your Visual Basic code use relative paths that accurately reflect the files' location after deployment. For example, in the .htm file any references to the logo graphic should be:

images/logo.gif

This is considered a relative URL because it does not provide the full server and directory path to the file. Instead, it indicates that the file can be found in a graphics subdirectory of the Web server directory from which the current page was drawn.

For More Information See the HTML reference of your choice for more information on relative and absolute paths.


Using Frames with Webclasses

HTML has a feature that allows you to divide the display area in a browser into two or more sections. These sections are called frames. Each frame can display content from a different Web page. For example, suppose you are working with a catalog application. You might use frames to display a list of all items in the catalog in one frame and detail for the selected item in a second frame.

Frames are created from an HTML page that contains tags known as FRAMESET tags. The FRAMESET tags indicate how many frames the page should contain, their size and position, and the HTML pages that should be displayed in each frame. The browser accesses the page containing the frameset information, processes the tags, and shows the resulting information to the user.

If you want to use frames with a webclass application, the simplest way to do it is to use an HTML template file to contain modified frameset information, add it to your webclass, then link webitems or events to each frame. The following HTML code shows how you might create a template file with frameset information.

<FRAMESET frameBorder=0 cols=30%,70% TARGET=Right MARGINHEIGHT=0>
   <FRAME id=Left>
   <FRAME id=Right>
</FRAMESET>

Normally, you would need to include information telling the browser what content to display in each frame. However, in a webclass application, you do not need to do this because you can link webitems or custom events to each frame and use the processing defined for those items to tell the webclass what to do. The following figure shows how the .htm file shown above would display in the page designer prior to connecting any events:

Frame Document in the Webclass Designer

For the frames above, you might link a custom event called ShowList to the first frame, and a custom event called ShowDetail to the left frame. You would define processing for ShowList that writes a list of the items in your catalog to the leftmost frame. You would define processing for ShowDetail that displays detail information for the first item in the list. When the browser opens the frameset template file, it parses these two events and displays the results. The user can then work with the data in the list and detail frames.

For More Information See "Frameset" in the Internet Client SDK for more information on the frameset tags.


Webclass Events

Every webitem in the webclass contains a series of events. There are three kinds of webclass events: standard events, template events, and custom events. The following table summarizes the main differences between these three events.

Event | Originates | Appears | Applies to | Example
Standard event | Predefined. | Only in the Code Editor window's Procedure list. | Templates and custom webitems. | Respond, ProcessTag
Template event | Event candidates are generated by Visual Basic when it parses an HTML template file. | In the Designer window, and in the Code Editor window after connecting. | Templates. | Img1 SRC, Form1
Custom event | Added by the developer. | In the Designer window and in the Code Editor window. | Custom webitems or templates. | VerifyUser, DisplayChart

The end user of an IIS application initiates the process that fires an event by clicking on an element in the browser — for example, a hyperlink — that makes a request to the server. The webclass intercepts this request and takes one of the following actions:

  • If an event is specified in the request, the webclass fires the Visual Basic event by matching the request up to the appropriate webitem and event procedure.

  • If the event specified in the request does not correspond to any existing webclass event, the webclass fires the UserEvent event for the template.

  • If there is no event specified in the request, the webclass fires the Respond event for the template.

  • If no webitem or event is specified in the request, the system launches the ASP for the application and fires the webclass's Start event.

For More Information See "Responding to Events in IIS Applications" for more information on writing code to process your events.


Standard Webclass Events

By default, each template and webitem you add to a webclass has a set of standard events that automatically appear in the Code Editor window. There are three standard events for any webitem:

  • Respond — Sets the default action a webitem takes when activated by a user request.

  • ProcessTag — Processes specific tags in a template file and replaces their content with data you specify. The tags to process are identified by the TagPrefix property for the webitem.

  • UserEvent — Processes webclass events created at run time.

The most commonly used of these is the Respond event. This is the default event for templates and webitems. The webclass fires the Respond event in two situations: when the webitem is first activated, or when it receives a request from the browser that does not correspond to a template event. The webitem can be activated from the client or the server. A client-side activation occurs when a request contains a reference to the next webitem that should be called. A server-side activation occurs when the NextItem property is set. In either scenario, the Respond event processes the request and sends a response to the browser.

For example, if a user accesses a customer inquiry application and submits a query about whether an order has been shipped, the Respond event might retrieve the request data, process the request by reading a database and retrieving the order detail, then write a reply back to the client that contains the retrieved information.

Note Unlike template events and custom webitem events, the Respond event does not appear in the designer's Detail panel. You access it from the code window's Procedure drop-down list along with other standard events that are not exposed through an external file or from a user's action in the designer.

Events in an IIS application are fired by actions that occur in the browser. These actions might be initiated by the user selecting an element that contains a URL, or they might be caused by the browser loading a page. For example, when the browser loads a page containing an IMG tag, it fires the event corresponding to that tag when it attempts to resolve the URL for the image and display it. Alternately, a user can select an item such as a hyperlink and manually fire an event. In both cases, the event is fired because the browser accesses a URL that calls the server.

For More Information See "Transitioning Between WebItems" for more information on the NextItem property. See the "Respond Event," "ProcessTag Event," "TagPrefix Property," and "UserEvent Event" topics in the Language Reference. For information on using the ProcessTag event to process text replacements, see "Performing Text Replacements in a Webclass." For information on using the UserEvent event to process dynamically created events, see "Defining Webclass Events at Run-Time".


Template Webitem Events

Tags within an HTML template file can act as event sources if the browser calls the server when the user selects their corresponding HTML elements. Elements call the server if their HTML tags have attributes that contain a URL. For example, when the user selects a hyperlink, the browser sends a URL request to the server to display another page. The hyperlink tag can therefore be treated as an event source in the webclass, because the webclass can intercept this request and process Visual Basic code for it.

When you add a template, Visual Basic scans the file and makes note of each tag within it that can make a server request. The designer displays the attributes of these tags in the right panel. Most tags have only one attribute that can call the server, but some tags, such as the IMG tag, have multiple attributes that can do so. There are three steps to setting up template webitem events:

  1. You choose which of the tag attributes displayed in the Detail panel you want to treat as events.

  2. You perform a process known as "connecting" for each attribute you want to process.

  3. You write code for each connected event.

Connecting an attribute means that you are activating it, or enabling the attribute to be treated as an event. Until an attribute is connected, it does not appear in the Code Editor window for the webclass and you cannot write procedures for it. After the tag attribute is connected, it appears in the Procedure drop-down list in the Code Editor window with the other standard events. You can select the template event and write code for it like you would any Visual Basic event.

A template event differs from a standard event in two main ways:

  • Template events are generated from an .htm file. A standard event is predefined and appears automatically for every template in the webclass. Because template events are generated from the HTML, you can have a different set of template events for each template file.

  • Template events must be connected before they can be programmed. Standard events are always available in the Code Editor window.

For More Information See "Adding HTML Templates to the Webclass" for information on adding a template file to the webclass and viewing the tags that can act as events. See "Connecting Events in a Webclass" for instructions on connecting tags to events and webitems.


Custom Webitem Events

You can add custom events to webitems in order to provide additional event handling for them. When you create a custom event, you associate it with the template or custom webitem for which it will be fired, then define the code to determine the action Visual Basic takes when the event fires.

Custom events can be fired directly from the browser through the URLFor method. For example, you can access a custom event by using the URLFor method to reference the custom webitem and event name, as shown in the following code:

Response.Write "<A HREF=""" & URLFor(CustomWebItem1,"CustomEventName") _
& """></A>"

When the user selects this hyperlink in the browser, the webclass intercepts the request and fires the specified custom event on the server.

To add custom events to a custom webitem

  1. In the left panel of the designer window, right-click the custom webitem to which you want to add an event.

  2. Click Add Custom Event. An event appears in the right panel of the designer window.

  3. Write code for the event.

For More Information See "Firing Custom Events" and "Specifying URLs for Webitems" for more information on the URLFor method, or see "URLData Property" in the Language Reference.


Firing Custom Events

Unlike template events, which are fired when the user selects an element in the browser, custom events do not automatically correspond to an element in the browser. To fire these events, you must first use the URLFor method to associate the custom event with an element in the browser. The URLFor method does this by creating a URL that references the custom event. When the user selects the element associated with the URL (for example, a hyperlink), the custom event fires.

For example, suppose you add a custom event named RetrieveAuthorBio to your project, and associate it with a custom webitem called AdditionalInfo. The following screen shows the project.

Sample Project

Suppose you want to fire the RetrieveAuthorBio event from another page in your application. The following code shows how you would use the URLFor method to include a link to this event:

Response.Write "<A HREF=""" & URLFor(AdditionalInfo,"RetrieveAuthorBio") _
& "" ">Retrieve</A>"

This code inserts a hyperlink onto the page that, when selected, activates the AdditionalInfo webitem and fires its RetrieveAuthorBio event.

For More Information See "URLFor Method" in the Language Reference for more information on syntax and arguments for this method. See "Webclass Events" for more information on custom events.


The Webclass Life Cyle

The Webclass Life Cycle

Like a form-based application, a webclass is initialized and destroyed, although these events are fired differently than they are for a form. In addition, a webclass has several other events that mark stages in its life, indicating when the browser displays the first webitem and when requests are received and processed.

These are the key events in the life of a webclass:

  1. The Initialize event occurs when an end user accesses the .asp (Active Server Pages) file that acts as the host for your IIS application. In the Initialize event, the webclass is created as an object. This is always the first event in the webclass's lifetime.

    Note If the StateManagement property for the webclass is set to wcNoState, the webclass fires the Initialize event each time it receives an HTTP request. See "State Management in IIS Applications" for more information about setting the StateManagement property.

  2. The BeginRequest event is the next event called after the Initialize event when the user starts the application, and is also fired each time the webclass receives a subsequent request from the browser. This event marks the beginning of processing for an HTTP request. You might use this event to perform processing required for every request the webclass will perform. For example, you could use BeginRequest to retrieve state information from a database or to verify user information before processing a request.

  3. The Start event generally occurs the first time BeginRequest is fired in the application. It is not fired on subsequent requests. This event is generally used to send an initial response to the browser, launching the Respond event for the specified webitem.

    Note If the end user specifies a webitem and event in the initial URL they use to launch the application, the webclass does not fire the Start event. In this case, the specified event would be the first event fired. See "Setting the Start Event" for more information.

  4. The EndRequest event occurs when the webclass has finished processing an HTTP request and has returned a response to the client. The processing of an event might require the webclass to process several templates or webitems and their associated event procedures before a response is returned to the client.

  5. The Terminate event tells the run-time DLL to destroy the instance of the webclass.

    Note If the StateManagement property for the webclass is set to wcNoState, the webclass fires the Terminate event each time it sends a response to the browser. If the StateManagement property for the webclass is set to wcRetainInstance, the webclass fires the Terminate event only after it calls the ReleaseInstance method. See "State Management in IIS Applications" for more information about setting the StateManagement property.

There are no Load and Unload events for an IIS application. Instead, the BeginRequest or EndRequest events can perform some of the same functions as the Load and Unload event did — these events can be used to initialize values your application needs to run, and clean up state and other values at the end of a request. Begin and EndRequest are unique events for a webclass. A webclass may receive multiple requests throughout its life. These events allow the webclass to process each request as a unique entity.


Connecting Events in a Webclass

HTML template files are made up of HTML tags, tag attributes, and text. HTML tags tell the system how to format and display the information on the page. Attributes provide additional detail about a specific item on the page. For example, for a picture in an HTML page, a tag called an IMG tag tells the system to format and display a picture. An attribute of that tag tells the system where to retrieve the picture from on the server.

When Visual Basic first adds a template to your webclass, it parses the file and lists all the tags for which one or more attributes can be treated as events. These attributes are not actual events, however, until they go through a process known as connecting. Prior to connection, it is more accurate to think of the tag attributes listed in the designer as event candidates.

You can connect an attribute in two ways:

  • You can connect the attribute to an event if you want to write code that tells the webclass how to respond when the element is processed by the browser. Elements are processed on the browser when the user selects the element or when the page processes them automatically, such as when it loads the page.

  • You can connect the attribute to a webitem if you want the element to activate a webitem and fire its Respond event when the element is processed in the browser.

For More Information See "Webclass Events" for more information on adding events to a webclass and the types of events a webclass can process. See "Responding to Events in IIS Applications" for more information on writing code to process your events.

Connecting Attributes to Events

The template events in a webclass generally correspond to attributes of HTML tags in the template file. For example, an image event corresponds to an attribute of the IMG tag in the HTML page, and a hyperlink event corresponds to an A tag attribute in the HTML page. However, the association between attributes and events is not automatic — you must connect to events those tag attributes that you want to treat as events.

Connecting an attribute to an event enables the webclass to launch an event procedure when the end user selects the HTML element the tag attribute represents. For example, suppose you are working with a template called FeedbackPage. When you add this template to the webclass, Visual Basic parses the file and discovers that there are two elements within it capable of submitting requests to the server: a Submit hyperlink and a Home hyperlink. It lists the tags and attributes for these two elements in the designer's Detail panel. You can connect the attributes to events if you want them to fire events in your application.

Only connected events can be programmed. That is, after you connect an event, it appears in the Procedure drop-down list in the Code Editor window, and you can write procedures for it. Disconnected events do not appear in this list.

The Webclass Designer indicates the events that have not been connected in two ways:

  • Tags that have not yet been connected for the selected template appear in the Detail panel. After you connect an attribute, Visual Basic also displays the event in the Treeview panel, under the template to which it belongs, and displays the yellow lightning bolt icon for the event.

  • The Target column displays the word "<None>" for any disconnected tags. Once the tag has been connected to an event, the designer displays the name of the event in the Target column.

After you connect an attribute to an event, you can either write code for it in the Code Editor window, rename the event to which it is connected, or disconnect it. You might disconnect an event if you want to connect it to a webitem instead of to an event, or if you do not want it to function as an event. See "Connecting Attributes to a Custom WebItem" below for information on connecting to a webitem rather than an event.

To connect an attribute to an event

  1. In the left panel of the Webclass Designer, click the template that contains the attribute you want to connect.

  2. In the right panel of the Webclass Designer, right-click the attribute you want to connect, then select Connect to Event.

    Visual Basic changes the text in the Target column to indicate that the attribute has been connected, and it displays the event in the tree view beneath the appropriate template.

To disconnect an attribute from an event

  • Right-click the event in the right panel of the Webclass Designer, then click Disconnect.

Connecting Attributes to a Webitem

When you connect an attribute to a webitem rather than an event, you tell the webclass to activate that webitem when the HTML element the tag attribute represents is selected in the browser. When Visual Basic activates the webitem, it fires the webitem's Respond event. You must choose the specific webitem to which you want to connect the attribute.

For example, you might connect the attributes from a template file's IMG tag to a custom webitem that references an image stored on the server. When the user selects the element corresponding to the tag, it makes a call to the server that the webclass intercepts. The webclass tells Visual Basic to launch the associated webitem and fire its Respond event, which in turn retrieves the image from the database and returns it to the browser.

Visual Basic indicates that an attribute has been connected to a webitem in the Target column. Before you connect an attribute to a webitem, the word <None> appears in the Target column. After connection, the Target column displays the name of the webitem to which you connected the tag.

After you connect an attribute to a webitem, you can disconnect it and either leave it disconnected or connect it to another webitem. See "Connecting Attributes to Events" for more information on connecting to events.

To connect an attribute to a webitem

  1. In the left panel of the Webclass Designer, click the template that contains the tag you want to connect.

  2. In the right panel of the Webclass Designer, right-click the attribute you want to connect, then select Connect to Webitem.

  3. In the Connect to WebItem dialog box, select the webitem to which you want to connect your tag attribute, then click OK.

    Visual Basic displays the name of the webitem to which the attribute has been connected in the Target column.

To disconnect an attribute from a webitem

  • Right-click the event in the right panel of the Webclass Designer, then click Disconnect.


Manually Adding Event Notation to a .WCT File

Manually Adding Event Notation to an .HTM File

When Visual Basic saves the .htm file for a webclass template, it makes changes to it. For each tag attribute you connect to an event or a webitem, Visual Basic replaces a portion of the element's HTML with notation unique to the webclass. This allows the webclass to access the HTML page and determine the webitem and event that should be fired when the user selects the element. You can modify the .htm file manually to include this notation if you want to connect events without using the designer.

Note You do not have to perform the process described in the sections below if you are connecting events from within the Webclass Designer; the designer automatically modifies the HTML file.

Manual Notation to Connect an Attribute to an Event

If you want to manually modify an .htm file to include a reference to a webitem and event, you use different notation than what Visual Basic includes automatically. For example, suppose you have an HTML template file that contains the following code:

<form method=post>

If you want to manually hook up this tag to an event, you could modify the HTML as follows:

<form action=www.myserver.com/Project1_MyWebclass.asp?WCI=webitem1 _
?WCE=eventname method=post>

This notation indicates that the form tag should activate the webitem called "webitem1" and launch its FormSubmit event. The notation contains the following parts:

  • www.myserver.com is the server on which your webclass resides.

  • Project1_MyWebclass.asp is the name of the Active Server Pages (ASP) file associated with your webclass. The ASP is created when you run the project, and the name is set equal to the value of the NameinURL property for the webclass.

  • WCI=webitem1 identifies the webitem being referenced.

  • WCE=eventname tells the webclass the event in the webclass to which this tag corresponds. When the user selects the element for this tag in the browser, the webclass can read this notation and determine the event that it should fire.

Manual Notation to Connect an Attribute to a WebItem

When you connect an attribute to a webitem instead of an event, you do not use the WCE=Eventname syntax in your .htm file. Instead, you would use the WCI=Webitemname argument only, as shown in the following code:

<form action=www.myserver.com/Project1_MyWebclass.asp?WCI=webitem1 method=post>

This notation indicates that the form tag should activate the webitem called "webitem1" and launch its default event. The notation contains the following parts:

  • www.myserver.com is the server on which your webclass resides.

  • Project1_MyWebclass.asp is the name of the Active Server Pages (ASP) file associated with your webclass. The ASP is created when you run the project and has the same name as your webclass.

  • WCI=webitem1 identifies the webitem being referenced.

For More Information See "Connecting Events in a Webclass" for more information on connecting attributes to events and webitems.


The Object Model for IIS Applications

IIS applications are hosted by an .asp (Active Server Pages) file and make use of several of the objects in the Active Server Pages object model. The webclass uses these objects to access and manipulate information from an HTML page. The ASP objects that a webclass can use include:

  • Request — Receives requests from end users in the browser.

  • Response — Sends information to the browser in order to display it to the user.

  • Session — Maintains information about the current user session and stores and retrieves state information.

  • Application — Manages state that is shared across multiple webclass instances.

  • Server — Creates other objects and determines server-specific properties that might influence the webclass's processing.

  • BrowserType — Determines the capabilities of the user's browser and makes processing decisions based on that information.

The ASP Request Object

You use the Request object to retrieve information from or about the current user. The Request object gives you access to all of the information passed in any HTTP request. HTTP requests contain information about the current user, any data they entered prior to making the request, and arguments that tell the Web server how to process and respond to the request.

Most frequently, you use the Request object to retrieve information from an HTML form. For example, you might retrieve all of the form elements passed back in a Submit event. The following example shows how you might use the Request object to gather information from the browser:

Private Sub Webitem1_Submit
   'Define variables to hold information retrieved from the request
   Dim first as String
   Dim last as String
   'Retrieve form information and assign it to the variables, using
   'the Request object and its Form collection.
   First = Request.Form("Firstname")
   Last = Request.Form("Lastname")
End Sub

You can use the Request object's associated collections to access information. These collections include:

  • QueryString — Retrieves the values of additional arguments in a URL when a request is passed using the GET method. GET is used by the Web server to retrieve objects and, in some cases, to send information from an HTML form.

  • Form — Retrieves the value of form elements passed in an HTTP request when the request is passed using the POST method. POST is a method used by the Web browser to send the information from an HTML form.

    Note POST is the more common method used to send form information. You can use the Request object to access any of this data. You should not use the GET method in your HTML template files.

  • Cookies — Retrieves the data contained in cookies sent with the form request. Cookies are small parcels of information used to store data about the current user. These can be passed between the browser and the Web server.

  • ServerVariables — Retrieves information such as header values, logon name, or server protocols in use.

  • ClientCertificate — Retrieves information stored in certificate fields when the browser sending the request supports client certificates. Certificates identify a user to the Web server.

For More Information See "Getting Information from a User" in the Active Server Pages Scripting Guide for more information on the Request object and its collections.

The ASP Response Object

You use the Response object to return information to the browser. For example, you might use the object's Respond event to write HTML to the browser. There are several methods you can use with the Response object:

  • Use the Write or BinaryWrite method to send information directly to a browser.

  • Use the Redirect method to direct the user to a different URL than the one the user requested, such as a different webclass or an external page.

  • Use the Cookies collection to set cookie values to return to the browser and store these values for future use. A cookie is a set of information about the user that can be passed between the client and the server, identifying the user to either system.

  • Use the Buffer property to postpone a response while the entire page is processed.

  • Use the AddHeader method to add http headers to a response.

The following example shows how you might use the Response object to write a few lines of HTML code to the browser:

With Response
   'Indicate the beginning of the HTML page
   .Write "<HTML>"
   'Indicate the beginning of the body section
   .Write "<BODY>"
   'Send a line with heading two style followed by
   'a paragraph break
   .Write "<H2>Feedback Form</H2><P>"
   'Send a paragraph of text
   .Write "Please enter your comments below:<P>"
   'other write statements to present form elements
   'and close the HTML document.
End With

For More Information See "Sending Information to a User" in the Active Server Pages Scripting Guide for more information on the Response object and its collections.

The ASP Session and Application Objects

The Session and Application objects both store state information about the webclass's end users. The Session object can store information about a single user in the current session. The Application object can store information about multiple users.

You use the Session object to maintain information about the current user session and to store and retrieve state information. For example, suppose you have an application that uses two main pages: one that summarizes customer information, and one that allows the user to order products. You want to ensure that the customer only sees the information page once, regardless of how many transactions they enter. In order to do this, the webclass must be able to determine whether the user has seen the customer information when it processes each new order. You can do this by using the Session object to retain information about where the user has already been.

You use the Application object to store information about multiple users. For example, suppose you want to keep track of the number of users who access a webclass and access that statistic online. You can do this by storing a count in the Application object and incrementing it each time a user accesses a part of the webclass.

For More Information See "State Management in IIS Applications" for a code example of storing state in the Session and Application objects.

The ASP Server Object

You use the Server object to create objects and determine server-specific properties that might influence the webclass's processing. For example, suppose you have a library of business objects that are used during event processing. One of these objects must be kept alive throughout the session in order to maintain internal state. You can do this using the Server and Session objects.

In this case, you could choose to store the object as a Session property, using Server.CreateObject to create an instance of it. This is necessary so that IIS can detect the threading model of the object and optimize subsequent request processing.

The following code shows how you would do this in your event procedure:

Dim BusObj as SomeBusinessObject
Set BusObj = Server.CreateObject("OurCompany.SomeBusinessObject.1")
'Code here to call methods of the object
Set Session("BusObjInstance") = BusObj

The ASP BrowserType Object

You use the BrowserType object to determine the capabilities of the user's browser and make processing decisions based on that information. For example, suppose you have an application that contains a button called CheckInventory. This button allows the user to check available inventory before placing an order. You provide two versions of the event procedure for this button in your webclass — one using HTML and one using JavaScript. You might do this if the JavaScript procedure provides a better user experience — for example, it might not cause a page transition, where the HTML procedure would.

In this scenario, you could use the BrowserType object to determine which procedure to use by determining if the user's browser supports JavaScript. The following code shows how you would use the BrowserType object to make this choice:

Private Sub OrderForm_ProcessTag(ByVal TagName As String, TagContents As String, SendTags as Boolean)

   'If the browser supports Javascript, generate HTML to call
   'a method on an applet. If it does not, generate HTML to
   'fire an event on the webclass.

   If TagName = "WC@FORMTAGSTART" Then
      If BrowserType.javascript And BrowserType.javaapplets Then
         TagContents = "<FORM onsubmit=""JavaScriptHandler()"">"
      Else
         TagContents = "<FORM ACTION=" & URLFor(OrderForm _
         "OrderForm_Submit") & " METHOD=POST>"
      End If
   End If
   SendTags = False
End Sub


Responding to Events in IIS Applications

Most of the processing that occurs in an IIS application occurs in the event procedures for the application's webitems. You use these events to tell Visual Basic what to do when it receives a request from the browser for a particular element on a page. Visual Basic matches requests it receives to webitem events and runs any code it finds for the event procedures.

You use Visual Basic code to tell the webclass how to process requests. These are just a few of the things you can do in your event procedures:

  • Write the contents of an HTML template to the browser, or generate your own HTML.

  • Redirect to another page in the application and display it, or move to another webitem and process its default event.

  • Open a database connection, create recordsets, and manipulate the data to be returned to the user.

  • Perform replacements on text within your HTML templates.

  • Retrieve user data for processing.


Setting the Start Event

Before you can respond to user requests, you must, in most cases, write code that tells the webclass what to do when the application is first started. You can do this using the Start event.

The Start event occurs when the user first accesses your application by navigating to the .asp file that forms the base URL for your application. For example, if you have a webclass called Project1_OrderEntry.asp on a Web server called www.mycatalog.com, the base URL to fire the Start event for your application would be:

http://www.mycatalog.com/Project1_OrderEntry.asp

In most cases, you should write code for the Start event in order for Visual Basic to successfully run your webclass. You use the Start event to navigate to the first webitem in your application. The following code shows how you might do this:

Private Sub Webclass_Start()
   Set NextItem = Webitem1
End Sub

This code uses the NextItem property to navigate to the first webitem in the webclass, named Webitem1.

There are situations in which the Start event does not fire when the application is first launched. If the user specifies a webitem in the base URL, the webclass does not launch the Start event — instead, it launches the specified event for the webitem or the webitem's default event. For example, if the user knew they wanted to open a webitem called Form1 in the Project1_OrderEntry webclass, they could enter the following base URL to launch the application:

http://www.mycatalog.com/Project1_OrderEntry.asp?WCI=Form1

The notation WCI=Form1 tells the run-time DLL to open a webitem, called Form1 when it starts the application. In this case the Start event does not fire when the run-time DLL starts the application.

For More Information See "The Webclass Life Cycle" for more information on the Start event in IIS applications. See "Transitioning Between WebItems" for more information on the NextItem property.


Writing HTML in Webclasses

Sending HTML to the Browser

When the webclass processes a request from the browser, it must send the browser a response. Generally, that response is a stream of HTML that the browser displays to the user. You can return HTML to the browser in two ways:

  • You can send the contents of an HTML template file directly to the browser using the WriteTemplate method.

  • You can produce the HTML stream with Visual Basic code.

Sending HTML from a Template

The WriteTemplate method is the simplest way to send HTML to the browser in response to a user action. When the webclass launches an event procedure for a template event that contains this method, it sends the template's HTML back to the browser. The resulting page appears to the user.

Note If the template that you want to send to the browser contains any replacement indicators, the webclass fires the ProcessTag event before it sends the response. This happens automatically as part of WriteTemplate method processing.

The following code shows an example of how you would use this method in code:

Private Sub OrderSearch_Respond()
   OrderSearch.WriteTemplate
End Sub

In this code, a webitem named OrderSearch has an HTML template file associated with it. When Visual Basic fires the Respond event for this webitem, it writes the contents of that template to the browser, which displays it to the end user.

Note The WriteTemplate method does not fire the Respond event if called from an event in another webitem. In order to keep the webclass code organized, you will often want to invoke the WriteTemplate method within a Respond event.

The WriteTemplate method can take one optional argument called Template that allows you to specify a different template file to return to the browser. This is helpful when you need to choose from a group of templates with common events. For example, you may have a set of news articles that are formatted similarly and contain a common series of hyperlinks and other tag elements, no matter what the story content. These articles are stored in template files available to a webclass. You can hook up the appropriate tag elements to events in your webclass, but then use the Template argument in a WriteTemplate call to specify the appropriate template file to retrieve for a user request.

For More Information See "Performing Text Replacements in a Webclass" for more information on the ProcessTag event and its processing as part of a Respond event. See "WriteTemplate Method" in the Language Reference for more information on using the WriteTemplate method and its arguments.

Sending HTML from Code

If you know HTML well, you can also write HTML line by line in your event procedures and send it to the browser using the Response object. This is a more complicated way of sending HTML than the WriteTemplate method, but it is useful in situations where you do not have a template to work with (such as if you are working with a custom webitem) or in which you want to generate a customized response.

The following sample shows how you use the Response object's Write method to send HTML to the browser. In this procedure, the webclass retrieves user information from a database and writes it to an HTML table:

Private Sub CustomerList_Respond()
   With Response
      'Write the HTML header information and start a table
      .Write "<HTML>"
      .Write "<BODY>"
      .Write "<TABLE BORDER CELLSPACING=1 CELLPADDING=7>"

      'Fill in cells until the end of file is reached in the recordset
      Do While rs.EOF = False

         'Write the user's name and address to a row in the table
         .Write "<TR>"
         .Write "<TD>" & rs("lastname") & "," & rs("firstname") & "</TD>"
         .Write "<TD>" & rs("address") & "</TD>"
         .Write "<TD>" & rs("city") & "</TD>"
         .Write "<TD>" & rs("state") & "</TD>"
         .Write "<TD>" & rs("zipcode") & "</TD>"
         .Write "</TR>"

         'Move to the next record in the recordset, then loop
         rs.MoveNext

      Loop

      'End the table, then write closing tags for the page.
      .Write "</TABLE>"
      .Write "</BODY>"
      .Write "</HTML>"
   End With
End Sub

A few notes on using the Write method:

  • Items to be sent to the browser must be enclosed in quotes. This includes both text and HTML tags. For example: "</TD></TR>", or "<P>Please enter your name.</P>" If you want to insert a quotation mark as part of your response, you must use two quotation marks together. For example, "" inserts a quotation mark. Three quotation marks (""") insert a quotation mark and open or close another set of quotation marks.

  • You might find it simpler to use the With statement, as shown above, when writing HTML in code.

  • You do not need to break your Write statements as shown above. You can put all of your HTML in a single Write statement, or break them into separate statements for easier readability.

For More Information See "The Object Model for IIS Applications" for more information on using ASP objects in your webclass code.


Retrieving Information from a Form

Retrieving Information from an HTML Form

You can use the Request object to retrieve the information a user enters on an HTML form. Submitting a form can fire an event within your webclass, if you have connected the FORM ACTION tag attribute to an event; if so, you can use this event to gather and manipulate information. You use the Request object's Form collection to retrieve named fields from the form and set their value equal to variables in the webclass. For example, the following code shows how you would retrieve information from a book request form in an order entry application:

Private Sub BookList_Search()
   ' declare variables for form information
   Private sTitle as String
   Private sAuthor as String
   Private sPublisher as String

   ' retrieve form arguments and assign to variables
   sTitle = Request.Form("title")
   sAuthor = Request.Form("author")
   sPublisher = Request.Form("publisher")

   ' further code here to process this information,
   'open the database, find the book, and return it.
End Sub

When you add a template that contains a form to a webclass, you might notice that the form elements, such as buttons and text areas, do not appear in the Detail panel and event candidates. You can have the webclass respond to events that occur on these form elements by connecting the parent Form for the elements to an event. Then write code for the parent form that uses the Request object's Form collection to determine which element was selected.

For More Information See "The Object Model for IIS Applications" for more information on using ASP objects in your webclass code.


Performing Text Replacements in a Webclass

Text replacements are useful in situations where you are generating some parts of your HTML page on the fly. For example, you might want to personalize a Web page with the user's name, as entered on a previous page in your application. You could insert a replacement indicator in the .htm file, then replace it at run time with the user's name.

In a more complex example, suppose you want to populate a table with the results of a database query. You can use replacement indicators in the HTML for the table to mark where information should go, then replace these indicators with the retrieved information after your query runs.

The process of performing text replacements involves scanning a webclass template file for special indicator tags and replacing them or their contents with custom content. The webclass processes these indicators automatically when it responds to the WriteTemplate method in an event procedure. WriteTemplate sends the contents of an HTML template file to the browser, after any replacements are finished.

The tags processed on the server have the following format:

  • A tag prefix, usually a set of characters used to indicate to the webclass that a replacement operation is necessary. For example, you might use WC@ as your tag prefix. The tag prefix is the same for each location in the template where a replacement operation must occur. You define the tag prefix by setting the TagPrefix property for each template. WC@ is the default tag prefix in Visual Basic.

  • A tag name, usually a descriptive word, that identifies the replacement area. This is usually different for each location in the template where a replacement operation must occur. If you want to perform the same replacement in several locations, use the same tag name.

  • Tag contents, which represent the current contents of the tag before replacement. You can use arguments in the ProcessTag event to replace either the tags and their content, or just the content.

Together, these elements provide the webclass with the information it needs to process the tag. You insert these indicators at each location in the file where you want the system to process replacements.

For More Information See "Sending HTML to the Browser" for more information on the WriteTemplate method and its processing. See "ProcessTag event" in the Language Reference.

Modifying a Template File to Use Replacements

In order to perform text replacements, you must first define a tag prefix for the template, in the Webclass Designer, then use an editor to insert those tags prefixes into the file wherever you want a replacement to occur. You can indicate as many replacement areas in a file as you want.

For example, suppose you have a template in your webclass called FeedbackPage. On this page, you want to perform a replacement in two areas: you want to insert the user's name and the date on which the user's order will be shipped. You have defined the tag prefix for this webitem to be WC@.

Note The tag prefix must begin with an alphabetical character and should contain a unique character to provide for optimal processing.

Suppose that your FeedbackPage file looks like this before you add the replacement tags:

<HTML>
<BODY>
<P>Thank you for your order. We estimate that your order will be shipped on the following date: xxx.
<P>
</BODY>
</HTML>

In order to indicate replacement areas for the name and date, you would modify the file as shown below:

<HTML>
<BODY>
<P>Thank you for your order, <WC@customer>firstlast</WC@customer>. We estimate that your order will be shipped on the following date: <WC@shipdate>shipdate</WC@shipdate>.
<P>
</BODY>
</HTML>

The following figure identifies the pieces of the replacement area in the previous code:

Replacement Indicators

Writing Code to Process a Replacement Area

You write all of the code to replace text in your file within the ProcessTag event. If you have a template file that contains several replacement areas and you need to treat them differently in your procedures, you would use a conditional statement such as If or Select Case to specify the different actions to take for each tag prefix on the page.

The order of processing for a file with tag prefixes is:

  1. Visual Basic processes user code and calls the WriteTemplate method from an event handler in the application. Typically, the event handler is for the Respond event.

  2. The webclass processes and replaces all prefixed tags in the file, according to the code you wrote for the ProcessTag event.

  3. The webclass writes the template file to the Response object.

  4. The WriteTemplate method sends the template file to the browser.

Note The ProcessTag event cannot be fired automatically from the user's actions in the browser. It is called by the WriteTemplate method from within another event procedure.

For example, in the example in the previous section, you modified an .htm file to include two replacement areas: one called "customer" and one called "shipdate". The following event procedure could be used to process those replacements:

Sub FeedbackPage_ProcessTag(ByVal TagName as String, TagContents as _
String, SendTags as Boolean)
   'Work with previously defined variables FirstName, LastName,
   'and ShipDate
   If TagName = "wc@customer" Then
      TagContents = FirstName & "" & LastName
   EndIf
   If TagName = "wc@shipdate" Then
      TagContents = ShipDate
   EndIf
   SendTags = False
End Sub

In this code, the webclass retrieves information from several member variables that were previously set to values the user entered. These values correspond to the user's first name, last name, and a calculated date for shipment. The values from these three variables are inserted onto the page, which is then displayed to the user.

When the ProcessTag event is fired, the TagContents argument retrieves the current value of the text between the tags in the .htm file. For example, in the following line of HTML code, "customer" is the current value of the TagContents argument.

<P>Thank you for your order, <WC@customer>customer</WC@customer>.

You set new values for the TagContents in your ProcessTag event procedure. You can use the existing contents to help retrieve the new values. For example you could use a database key as your TagContents, then reference that value in your ProcessTag event procedure to retrieve a record from the database.

To perform text replacements for a webitem in your application

  1. In the Webclass Designer, click the template for which you want to define text replacements, then set the value of the TagPrefix property in the Properties window to the prefix you want to use.

    Note The tag prefix must begin with an alphabetical character.

  2. Open the .htm file for the template in the editor of your choice, then insert replacement indicators anywhere you want to replace text, using this format:

    <tagprefix tagname>tagcontents</tagprefix tagname>

    Argument | Definition
    tagprefix | The tag prefix you defined in the Properties window, for this template.
    tagname | A unique identifier by which you will reference this replacement area in code. For example, for a tag prefix that will be used to insert the customer's order number, you might use the tagname "orderno."
    tagcontents | A placeholder for the content you will insert. This argument can also be used to provide information about what the content should be, or to hold information that will be needed when processing the replacement, such as a database key.

  3. In the Webclass Designer, right-click the template and click View Code.

  4. Write code for the ProcessTag event for the webitem, using an If or Select Case statement to work through each replacement area in the file.

Rescanning for Replacement Areas

In normal replacement processing, the webclass automatically scans the .htm file once to find all tag prefixes in the file when the webclass processes the WriteTemplate event. After finding these replacement areas, the webclass processes the tags and replaces them as indicated in code. However, your code might indicate that the webclass should insert additional tag prefixes when it replaces one of the original tags. In this situation, the webclass must search the replacement area again for these new replacement indicators and process them as well.

You can tell the webclass to search again by setting the ReScanReplacements property for the webitem. The ReScanReplacements property causes the webclass to make an additional pass through the replacement tags during the ProcessTag event. You must write code for any additional replacement areas you generate in order for the ProcessTag event to work through them.

Note The ReScanReplacements property tells the webclass to make another search during the ProcessTag event only. You cannot use this property to scan for replacement areas during other events.

To enable the webclass to recursively check the file for new tag prefixes

  • In the Properties window, select the webclass and set the ReScanReplacements property to True.


Specifying URLs for WebItems

You use the URLFor method to easily create hyperlinks that jump to other webitems in your application. When the webclass displays a webitem in the browser, it must first locate the template file for that webitem on the server. You might not know the exact location to which you will deploy the application when you write the code. URLFor provides a convenient shortcut for you to use in these situations.

Note In addition to specifying the location of the webitem, the URLFor method can also fire a specified event. The event argument is optional.

The following code shows how you might use URLFor to create a link to a webitem:

Private Sub ThankYouForm_Respond
   With Response
      .Write "<HTML>"
      .Write "<BODY>"
      .Write "Thank you for completing our survey. <P>"
      .Write "To return to the order inquiry page, select the _
following  link: <P>"
      .Write  "<A HREF=""" & URLFor(CustomerOrder) & """ & Return</A>"
      .Write "</BODY>"
      .Write "</HTML>"
   End With
End Sub

In this code, the developer creates a simple HTML page by using the Response object's Write method to create the HTML line by line. One of the elements created is a hyperlink that moves to another webitem called "CustomerOrder." The code for this hyperlink uses the URLFor method rather than specifying the exact location of the webitem on the server. No event is specified for the CustomerOrder webitem, so by default the run-time DLL launches the webitem's Respond event.

For More Information See "Firing Custom Events" for more information on using the URLFor method to fire events.


Transitioning Between WebItems

You use the NextItem property to shift processing from one webitem to another on the server. You might do this if there are several processes you want to perform before returning a response to the browser. Normally, the webclass performs these steps when it receives a request:

  1. Intercepts the request and matches it to a webitem in the application.

  2. Identifies the proper event to fire on the webitem, then fires it.

  3. Processes any code for that event.

  4. Returns a response to the browser.

NextItem is used to add an extra step to this process. After the webclass processes the code for the event it matches to the request, the NextItem property can pass processing along to another webitem. This allows the application to perform additional processing before returning a response to the user. The process is now this:

  1. Intercepts the request and matches it to a webitem in the application.

  2. Identifies the proper event to fire on the webitem, then fires it.

  3. Processes any code for that event.

  4. Processes the NextItem property and shifts processing to another webitem and fires its Respond event.

  5. Returns a response to the browser.

Note No matter where the NextItem property occurs in the event procedure, Visual Basic finishes processing that procedure before it shifts processing to the indicated webitem. This is the main difference between setting the NextItem and directly calling an event subroutine in another webitem.

The following code shows NextItem used in a Respond event:

Private Sub Feedback_Respond()
   ' Code to process the contents of the feedback form
   ' inserted here.
   ' Transition:
   Set NextItem = Thankyou
End Sub

This procedure is fired when the user submits the contents of a page containing a feedback form. In response, Visual Basic transitions to another webitem containing a thank-you page.

There are three situations in which the webclass ignores the NextItem property. If you use the property in event procedures for the following events, the webclass will not process the NextItem property:

  • EndRequest

  • ProcessTag

  • FatalErrorResponse

For More Information See "NextItem Property" in the Language Reference.


Using Redirects in Webclasses

Navigating Between Webclasses

On occasion, you may want to create an application that has more than one webclass. You might do this if you want to encapsulate certain functionality within one webclass and reuse it in other locations. For example, you might want to use a webclass to handle customer order processing, then access this application from another webclass that also must perform these same processes.

If you are working in an IIS application with multiple webclasses, you can use the Redirect method to navigate from one webclass to another. A redirect is generally placed in the Respond event for a webitem. When a redirect is called in an event procedure, the run-time DLL suspends processing on the current webclass, shifts focus to the indicated webclass, and launches its BeginRequest event. Control can be returned to the original webclass by using another redirect.

Note Each webclass in your project has its own .asp file, generated when you test or compile your project. You should specify the full path to this file in your redirect statement.

The following code shows an example of a redirect. The code in this example responds to the Click event for a button named "SearchForm" on a template called "OrderSearch." The webclass checks to see whether the action was fired as a result of a search request. If so, it uses the Request object to retrieve information from the form. If not, the server redirects to the first page in the application.

Private Sub OrderSearch_SearchForm()
   'if a search was initialized, retrieve form arguments
   If Request.Form("ACTION") = "Search" Then
      sCustMun = Request.Form("custNO")
      sOrderNum = Request.Form("orderNO")
      sLastName = Request.Form("custLN")
      'Navigate to an order status page
      Set NextItem = OrderStatus
   Else
      'if not, redirect to the opening page
      Response.Redirect "http://www.myserver.com/mydirectory/
      Project1_Welcome.asp"
   End If
End Sub

For More Information See "An Introduction to Webclasses" for more information about how an ASP page is used in a webclass.


Using Databases in Webclasses

Using Databases with Webclasses

You can use the ADO features of Visual Basic to retrieve and manipulate information stored in a database in response to user requests. Using webclasses in this way can help you create a powerful application. With ADO, you can open a connection to the database, build recordsets, retrieve data, and update information in the database.

Note If you are working with a database, you must have the appropriate database program on your Web server. In addition, you should make use of ODBC connection pooling and ADO disconnected recordsets when accessing a database. For more information on these features, see the ActiveX Data Objects documentation in your MSDN library.

There are multiple ways you might use databases in your applications. For example, you might create an application in which a series of images for a catalog are stored in a database. When users in your IIS application select a link from the catalog table of contents, the webclass can intercept that link, open a connection to the database, retrieve the image, and return it to the browser. Or you might use a search page to query a database for matching items and display the results in a generated table.

You can use the Respond event or another event for the appropriate webitem to handle your database connection and processing. For example, the following code shows how you would open a connection to an ADO database, create a recordset, and retrieve information from it:

Private Sub AuthorList_Respond()

   'Declare object variables for the database connection and recordset
   Dim cn As New ADODB.Connection
   Dim rs As New ADODB.Recordset

'Open the database connectioncn.ConnectionString = "DSN=csmith;UID=sa;PWD=sa;DATABASE=pubs"cn.Open
'Create the recordsetrs.Open "select * from customer", cn, adOpenStatic, adLockReadOnly

   'Write resulting information to a table, record by record
   With Response
      .Write "<HTML>"
      .Write "<BODY>"
      .Write "<TABLE BORDER CELLSPACING=1 CELLPADDING=7>"

      Do While rs.EOF = False
         .Write "<TR><TD>"
         .Write rs("authorlast") & ", " & rs("authorfirst")
         .Write "</TD><TD>"
         .Write rs("title")
         .Write "</TD><TD>"
         .Write rs("publisher")
         .Write "</TD><TR>"
         rs.MoveNext
      Loop
      .Write "</TABLE>"
      .Write "</BODY>"
      .Write "</HTML>"

   End With

   'Close the recordset and the database connection
   rs.Close
   cn.Close

End Sub

For More Information For more information on ADO processing, search for "ActiveX Data Objects" in the MSDN Library.


Defining Events at Runtime

Defining Webclass Events at Run Time

In addition to handling events that exist in the webclass at design time, you can create events at run time and process them dynamically. The ability to generate events at run time is useful in cases where you generate part or all of your user interface dynamically, by writing HTML in your response to the browser.

You generate user events at run time using the URLFor method, which takes two arguments: the name of a webitem in your webclass, and the name of an event. To create a user event, you pass this function the name of an event that was not defined at run time.

Processing for these run-time events is handled by the UserEvent event. In the procedure for this event, you write code that tells the webclass how to process all its dynamically generated events. If there is more than one of these events, you must use a conditional statement such as If or Select Case to tell the webclass how to respond to each of them.

Example: Dynamic Events in a Search Page

Suppose you have an application that consists of two pages:

  • A search page that looks up last names in a database. The interface for this page is fixed: it contains a few elements that are used to launch the search request.

  • A results page that displays the results in a table and provides a hyperlink for each to find more information. The interface for this page changes based on the number of records found in the database.

The following figure shows two possible outcomes of the search.

Tables with Dynamically Generated Events

Suppose that for each row the search returns, you want to generate a hyperlink the user can select to receive more information. These hyperlinks can act as events for the webclass, but you cannot handle these events at design time because they are created dynamically when the search is executed. Therefore, you must generate these events at run time.

The following shows some of the Visual Basic code you would use to generate the table and create user events for it.

With Response

   'Begin a table
   .Write "<TABLE BORDER CELLSPACING=1 CELLPADDING=7>"

   'For each record in the result set, generate a row.
   Do While rs.EOF = False
      'Start a row and cell
      .Write "<TR><TD>"

      'Insert a hyperlink calling the lastname field,
      'which represents a database key into the database
      'record, for the Response webitem
      .Write  "<A HREF=""" & URLFor(Response, rs("lastname") & """>"

      'Make the text of the hyperlink the person's name.
      .Write rs("lastname") & ", " & rs("firstname")
      .Write </A>"
      .Write "</TD></TR>

      'Move to the next record and loop
      rs.MoveNext
   Loop

   'End the table
   .Write "</TABLE>"

End With

To handle the events for the response page, you would define code for the Response webitem's UserEvent event.

Coding User Events

You handle all user events for a webitem within a single UserEvent procedure. If you have a page that contains several user events and you need to treat them differently in your procedures, you would use a conditional statement such as If or Select Case to specify the different responses for each dynamic event name on the page.

Note In the case of the table generated in the previous section, you would not use an If or Select Case statement because you want to perform the same operation for each row — retrieve the database key from the table, then look up the record and display more information.

The webclass fires the UserEvent event when it receives a request from the browser that references a user event. The following code shows the procedure you might define for the events in the table shown in the section above:

Private Sub Response_UserEvent(ByVal EventName As String)

   'Set a previously-defined variable to a new recordset
   rs = New Ado.recordset

   'Retrieve the key
   rs.GetPerson EventName

   'Code here to display a page with all of the information
   'for the retrieved record.
End Sub

In this code, the developer creates a new recordset and retrieves the value for the specified key. You would replace the word Key with the appropriate key value.


Managing State in IIS Applications

State Management in IIS Applications

Typically, Internet applications are stateless, in that the protocol that passes requests and responses between the browser and Web server is not capable of maintaining information between each request. Therefore, the protocol does not "remember" any information from previous requests when it receives a new request.

Because the protocol itself cannot maintain state, you must use other means to store information you want the application to remember between requests. There are several ways you can store state for a Web-based application:

  • Use the WebClass object or other objects on the server to store state information between client requests.

  • Use a database to store state information between requests.

  • Move state information back and forth between the server and the browser on each request, using cookies, the URLData property, or hidden HTML fields.

Note You can use a combination of these methods. For example, you might store information in a database and use cookies to send the database keys to the client.

Each approach has its own advantages and disadvantages. Storing state in objects is easy, but it may affect the scalability of your applications because the server must keep the object instantiated across requests and locate the same object each time it makes a subsequent request. Using databases is more scalable, but requires you to manage connections and recordsets. Moving state back and forth between client and server allows you to avoid storing information in either location but does increase the bandwidth of each request, has capacity limitations, and is potentially less secure.


Storing State in Objects

One of the easiest ways to store state information for an IIS application is to store it in an object. When you store state in an object, you use properties or variables within the object to hold the information you want to retrieve. There are several approaches you can take to use objects in this way for your IIS applications:

  • If your webclass is set up to stay instantiated between requests, you can store state information in member variables of the WebClass object.

  • You can store information in the Active Server Pages' Application and Session objects.

  • You can create your own Visual Basic objects that are designed to work on the Web server and store information within them. The objects you create must stay instantiated between requests in order to maintain state. You can make the objects stay instantiated in two ways: by storing them in the Session or Application objects, or by referencing them in member variables of the webclass itself, if the webclass's StateManagement property is set to wcRetainInstance. If the webclass's StateManagement property is set to wcNoState, you must use the Session or Application objects to store your Visual Basic objects.

Regardless of which approach you use, you write code in your webclass that writes information to the appropriate object upon receiving a request, retrieves information as necessary from the objects, and manipulates the stored state information.

Note If your webclass is set to stay alive between requests, the end user must have a browser that supports cookies and cookies must be enabled. For more information, see "State Management in IIS Applications."

Storing State in the WebClass Object

Normally, the webclass run time creates an instance of the webclass each time a request is made, then destroys it after the response has been sent to the browser. However, you can use a webclass property called StateManagement to alter this behavior.

Using StateManagement, you can keep the instance of the webclass instantiated, or alive, between requests. If you choose to keep the webclass alive between requests, the run-time DLL instantiates the webclass when the first request occurs and does not destroy it until the application terminates. This enables you to use variables within the webclass to store some information between browser requests. However, this solution will affect the scalability of your application because webclasses that stay alive are stored in the Active Server Pages Session object. As a result, subsequent requests must be routed to the same Web server and the ASP must take actions to ensure that the correct thread is used to process the request.

Note You can use the ReleaseInstance method to terminate an instance of a webclass you have kept alive across requests. When you use this method in a procedure, the run-time DLL terminates the instance of the webclass at the end of the procedure.

If you choose not to keep the webclass alive, the run-time DLL creates and destroys the webclass for each request. You can still maintain state information if you select this option, but you cannot store it in the WebClass object. Instead, you need to use other methods to maintain your state. Some of these methods include using objects, databases, cookies, or the URLData property to manage your state.

To keep the webclass alive across requests

  • In the Properties window, click the name of the webclass, then set the StateManagement property to 2 — wcRetainInstance.

To terminate an instance of a webclass you have previously kept alive

  • In the event procedure from which you want to terminate the webclass, call the ReleaseInstance method.

Storing State in the Session Object

You can use the Session object to maintain state information about a single webclass user. The Session object is part of the Active Server Pages object model and is treated as a property of the WebClass objects in your IIS application.

Note Using this method does not require you to set the webclass's StateManagement property to wcRetainInstance — you can store state in the Session object even if your webclass is set to store no state.

Both the Session object and the WebClass object allow you to store state on the webclass level in session-specific, server-side state. However, storing information directly in the Session object rather than that WebClass object allows you to share state with other webclasses or Active Server Pages. In addition, Visual Basic objects (such as the WebClass object) are apartment-threaded and bind all requests to a particular thread in IIS, so using the Session object is more optimal.

Suppose you want to ensure that a user in your application sees a page called CustomerInfo only once. The following code shows how you can set a Session object property during processing to record the fact that the user has seen this page:

Sub CustomerInfoForm_Submit

   'Code to process the form here

   'Set the value of the session variable accordingly.
   Session("CustomerInfoDisplayed") = True

End Sub

This code sets the value of a Session object variable to True when the user clicks a button on the CustomerInfo page. This tells the webclass that the user has been to the page in question.

Once that information is recorded, the webclass must use this information to make processing decisions. The following code shows how the webclass accesses the Session object property to determine whether or not to display the customer information form at another critical point in the application:

Sub PlaceOrder_Click

   'If the session variable is not set then customer info
   'page has not yet been displayed, so return it to the user

   If Session("CustomerInfoDisplayed")= "" Then
      Set NextItem = CustomerInfoForm
   Else
      Set NextItem = OrderForm
   EndIf
End Sub

In this code, the webclass checks a Session object property to see whether the customer information page has already been displayed. If so, it displays the order form. If not, it displays the customer information.

Storing State in the Application Object

You can use the Active Server Pages Application object to maintain state information about one or more users for the webclass. The Application object is part of the Active Server Pages object model, and is treated as a property of the WebClass objects in your IIS application. This object allows you to track more information than can be recorded in the Session object.

Suppose you want to count the number of times all users in your application access a certain page. The following code shows an event procedure you would use to store this count in the Active Server Pages Application object:

Private Sub MyWebclass_Start

   'Create a variable to store the number of users
   Dim Counter as Long

   'Temporarily lock the application, then increment
   'the count by one.
   Application.Lock
   Counter = Application("MyWebClassAccessCounter")
   Counter = Counter + 1

   'Store the value
   Application("MyWebClassAccessCounter") = Counter

   'Unlock the application
   Application.Unlock

   'Begin the application
   Set NextItem = WebItem1

End Sub


Storing State in Databases

You can use Visual Basic's ActiveX Data Objects (ADO) technology to store state information for your IIS application in a database. When you store state information in a database, you write records to a database table each time you receive a request from the browser, and retrieve information from those records as needed. This is useful when you have a webclass that is not set up to stay instantiated between requests.

Storing state information in a database involves several things:

  • Opening a connection to the database and writing information to a record there when you want to store your information.

  • Opening a connection to the database, retrieving records, and manipulating their data when you want to retrieve the state information.

Database state storage can allow you to retain information for long periods of time. For example, suppose you are working in an IIS application that manages the transactions and processing associated with an online bookstore. You have a series of pages in your application that gather information about the buyer. You could use databases to store the buyer information offline and save it for future visits by the same user. When a previous user shows up for a subsequent session, even if the next visit is weeks or even months later, the webclass can retrieve the user information from the database by using the login name and password the user supplies.

In addition, you can use the databinding properties of Visual Basic to manage some state. Because webclasses are Visual Basic objects, they can expose properties that can be bound to a database using the DataBinding and DataFormat objects. This means that property values can be transferred between the webclass and the database automatically through getting and setting those properties.

For More Information For more information on ADO processing, search for "ActiveX Data Objects" in the MSDN Library.


Moving State Between the Browser and the Web Server

A simple way of maintaining state information involves making the information a part of the requests and responses you send between the browser and the Web server. There are three ways to do this:

  • Using cookies to store information on the client and send it to the Web server in requests.

  • Using the webclass's URLData property to move information between the client and the server without permanently storing it in either location.

  • You can use hidden fields to store small pieces of data between the client and the server.

Each approach has its advantages and disadvantages. Cookies can sometimes allow you to send more information than URLData or hidden fields, but some browsers may not allow cookie functionality. URLData ties information to a specific page, which can be useful. Hidden fields bind information to a specific form on a specific page, and are best when you want to send a small amount of data.

Passing State Information with Cookies

You can use cookies to maintain some state information within a session. A cookie is a small packet of information that the server sends to the browser and which the browser stores for subsequent transactions with that server. The cookie includes information about the current user and a range of URLs for which the state is valid. When the browser makes another HTTP requests within that range, it includes the cookie in the request data. This allows you to maintain information about server requests and transport it between the client and the server.

Most frequently, cookies are used to store a database key on the client that the server then uses to retrieve state information. In this scenario, you write the database key to the cookie and pass it to the client, where it is stored. The next time the browser sends a request to the same Web site, it includes the cookie. The Web site then uses the cookie value to retrieve the appropriate information from a database table.

There are limits to the number of cookies you can send and the number that can be issued by a single site. It is recommended that you use a small number of cookies in your application and don't use them to send large amounts of data. Due to their size limitations, cookies are ideally suited for sending small pieces of information such as the user's ID.

Note Remember that users can turn off the ability to support cookies in their browsers, and that cookies can be temporary or not supported at all.

Cookies store information for a specific server, rather than for a specific page in your application. If you want to assign unique state information for a particular page, use the URLData property to manage your state information. See "Passing State Information with the URLData Property," below, for more information.

For More Information See the HTML reference of your choice for more information on using cookies.

Passing State Information with the URLData Property

You can use the URLData property to transfer information between the browser and the Web server. The URLData property appends information to specific URLs that the webclass sends to the browser. When the browser submits another request using one of these URLs, the information can be passed back to the Web server for further processing. In this way, you can send and retrieve state information without storing it in either location.

The URLData property offers several advantages:

  • It can be used with browsers that do not support cookies.

  • It stores state data on the page itself. Therefore, if the user uses the back button and resubmits the page, the webclass receives the same state data that was originally sent with the page.

There are two main disadvantages to URLData. First, URLData is restricted in the amount of data it can send. The size limitation varies from browser to browser, but most browsers can handle about 2K of data in the URL. You should test the URL length you are planning to use in your application. Second, URLData is not a feasible way of sending information if you are using a form with the GET method in Internet Explorer 4.x. Use the POST method in this situation, or use another means to transmit your state.

The URLData property can add information to URLs in the webclass's responses in two cases:

  • When the webclass calls a WriteTemplate method.

  • When the webclass calls a URLFor method.

In the WriteTemplate scenario, the webclass generates a response that typically involves formatting a template file to send to the browser. The URLData property tells the webclass to perform additional processing on the template — it must append the specified information to each URL in the template file that contains a parameter called WCU.

Note The webclass adds the WCU parameter to your template each time you connect a tag attribute to a webitem or event. You can also add this notation manually if you are working without a template.

This method of state management is particularly useful when you have selected wcNoState as the StateManagement property setting for your webclass. When wcNoState is selected, you cannot store information within the WebClass object on the server.

Setting State Information for a URLData Response

To set state information to use with URLData, you simply assign a value to the URLData property in your event procedure. Processing differs depending on whether or not you are using a template:

  • If you are working without a template, using Response.Write to send HTML to the browser, the webclass's URLFor method returns a URL to the browser that contains the URLData property's value as a parameter.

  • If you are using a template file, the webclass processes adds your URLData property value to any location in the template where the WCU parameter appears.

Note When the webclass assigns the URL data to the WCU parameters, it scans the file for all occurances of &WCU and ?WCU, and assigns the value of the URLData property to them in the format &WCU=Your URL data. If, for some reason, you want the letters ?WCU or &WCU to appear as part of your template's text and you want the webclass to ignore this text when it parses the file, enter the text as ?WCUWCU or &WCUWCU. When the webclass runtime finds such as string, it will strip out the extra WCU and leave the text as you intended. This extra level of encoding is only necessary if you assign a value to URLData.

For example, the following code shows how you might set state data within a custom event called "Set" for a webitem called "Item2":

Private Sub Item2_Set()
   'Set the value of the URLData property.
   URLData = "CustomerID: 77788"

   'Use the URLFor function to launch a response.
   Response.Write "<A HREF=""" & URLFor(Item3) & """>Go to Item 3</A>"

End Sub

Retrieving State Information from a URLData Request

If you have previously set the URLData property for a response, the information you sent to the browser will be returned to you when the webclass processes the URL that contains the data. You can retrieve the value of the URLData property in order to manipulate and process the state information on the Web server.

For example, the following code shows how you might retrieve state data within the Respond event for a webitem called "Item2":

Private Sub Item2_Respond()

   'Retrieve state information and use it to customize a response.
   Response.Write "Welcome back," & FetchNameFromDatabase(Me.URLData)

End Sub

For More Information See "Handling Sequencing in Webclasses" for information on using the URLData property to set a navigational sequence in your application. See "URLData Property" in the Language Reference.

Using Hidden Fields

You can use the HTML HiddenField control on one of your Web pages and use that field to store information for the current page. This method of state management only works on HTML pages that contain a form.

Hidden fields are good for maintaining state for specific forms and are easily accessible from cookies or other client-side scripts. The main disadvantages of using hidden fields are that the information they transmit is bound to a form and is available in the HTML source view for the page, so anyone who can access the HTML source can view the data. You may want to encrypt your data when using this method.

For More Information See the HTML reference of your choice for more information on using hidden fields.


Handling Sequencing in Webclasses

Normally, you cannot control the way that users move through your IIS application. Unlike in a forms-based application where navigation from form to form is generally fixed, Web-based applications present a unique challenge because users can select the Back button, items from the History list, or other navigational aids to move in ways that you cannot predict.

If you want to ensure that users move in a set path through your application, you can use the URLData property to include sequencing information with the requests and responses the webclass handles. For example, suppose you have a webclass with three webitems. Item one is an introductory page, item two is a customer order form, and item three is a summary page that gives information about the current order. You want to prevent the user from moving directly from item one to item three.

The URLData property enables you to set a sequence number for each webclass request. When the property is set, the run-time DLL automatically parses the data object and appends the value you assigned to the property to each URL it finds embedded in a response. For example, if you have a URL that reads:

WCI=webitem1?WCE=event1

and you use the following code to set the URLData property:

Me.URLData=01

the run-time DLL will append the value of the URLData property to the URL, using a WCU indicator. The resulting HTML looks like this:

WCI=webitem1?WCE=event1&WCU=01

This argument is passed to the client and stored there. On the next request, the browser includes the argument, telling the server that this request is the next in sequence after response 01.

At this point in your code, you would increment the value of the URLData property by one to indicate that a second response has been sent. You would do that with the following code:

Me.URLData=02

When the webclass encounters this setting, it parses the response again and appends this number to all URLs. The number 02 is then sent to the browser and stored, to be returned on the next request that contains the URL.

You can ensure proper sequencing by writing code that determines whether the URLData argument received with each request is valid. For example, you would store the value of the argument after each request, using either a variable or an external store such as a database, then compare each new request to see if the correct number has been used. The following example shows the code you might use to ensure sequence on the second request:

If URLData = 02 Then
   WebItem2.WriteTemplate
Else
   'Code here to send an error to the browser
   'and redirect to the previous page.
EndIf


Building Your IIS Application

IIS applications are compiled the same way any Visual Basic project is compiled — by choosing Make from the File menu. An IIS application must be built as an in-process component, or a DLL file.

When you run or compile the project, Visual Basic creates the following files:

  • A DLL file Visual Basic uses to run the project.

  • An .asp file that hosts the application in the browser and creates its run-time component.

  • An .exp file generated by the linker when the project is compiled and linked.

  • A .vbw file that contains the windows layout information for the project.

When you run or compile the project, you should place the DLL file in the same directory as your project files, for easiest deployment.

To compile your IIS application

  1. If the project is still in run mode, click the End button to return to design mode.

  2. On the File menu, click Make Project.dll to open the Make Project dialog box. Click OK to make the DLL file.


Debugging your IIS Application

You debug an IIS application in the same way you do any other Visual Basic application — by entering run mode from Visual Basic. Visual Basic loads the webclass run time, creates the virtual root from which to run the .asp file for the application, if necessary, and launches the system's default browser with an HTTP reference to the .asp file. The .asp file, in turn, launches the webclass.

Note Although you can view the .htm files associated with your application in the browser by opening them from the browser's File menu, this is not debugging your application. You must use the Start option from Visual Basic to enter debugging mode.

When you debug, you have the full Visual Basic development environment at your disposal. You can use all the tools available in Visual Basic — breakpoints, watch variables, debug statements, and so on — to debug your project.

Visual Basic prompts you when you debug that it is going to create a virtual directory for your project. A virtual directory is a directory outside your Web server's home directory that appears to browsers as a subdirectory of the home directory. It allows you to publish contents to the Web from directories outside the home directory structure. You cannot change the location of the virtual directory Visual Basic creates for the webclass.

The Project Properties dialog box's Debugging panel settings determine whether the system waits for you to tell it what to do when you go into run mode or automatically starts the webclass you specify. When you choose to automatically start the webclass, Visual Basic launches Internet Explorer, navigates to the URL for your application, and fires the webclass's BeginRequest event.

Visual Basic deletes all temporary files when it comes out of run mode. In addition, it destroys the instance of the Webclass Designer and restarts the designer in design mode.

For More Information See "Debugging Your Code and Handling Errors" in the Programmer's Guide for more information on how to test and debug your IIS application.

Errors in Webclasses

You can use Visual Basic's error-handling features in your IIS applications to trap errors and take corrective action. When an error occurs, Visual Basic sets the various properties of the error object, Err, such as an error number or a description. You can use the Err object and its properties in an error-handling routine so that your application can respond intelligently to an error situation.

In addition to standard error handling, IIS applications allow you to use two special features to handle errors:

  • You can use the Trace method to debug your application on the production computer.

  • You can use the FatalErrorResponse event to respond to serious run-time errors.

For More Information The basics of error handling are discussed in "Debugging Your Code and Handling Errors," in the Programmer's Guide. For information specific to ActiveX projects, see "Generating and Handling Errors in ActiveX Components", in "Building Code Components," in the Components Tools Guide.

Using the Trace Method

You can use the Trace method to help identify errors during the debug process and to track performance and statistical data. The Trace method sends a specified string to the Win32 OutputDebugString API. The string can then be captured to a suitable debugging tool such as DBMON.

Using the Trace method can allow you to debug on your production server computer and record useful information such as information about the execution of the application, error messages that occur, and any other information you need.

For More Information See "Trace Method" in the Language Reference for more information on using this method in your error handling.

Handling Fatal Errors

A fatal error on a webclass is one from which the application cannot recover or restore the appropriate webitem. For example, a fatal error might be an unhandled error within a webclass event, a structural error, or an unexpected error within the run-time DLL. Following such an error, the webclass run time fires the FatalErrorResponse event. The application is terminated and the instance of the webclass is destroyed.

When a fatal error occurs, the application can write a message to the Response object in the handler for the FatalErrorResponse event. This message can be one that you write, or it can be the default message for the .asp file associated with the webclass. To write your own message, use the Response object, then set the senddefault argument of the FatalErrorResponse event to False. To use the default error message, leave the senddefault argument set to True.

Note The webclass run time provides an Error property that is only available from within the FatalErrorResponse event. This property returns an object that describes the error that caused the webclass to terminate.

The webclass run time also logs fatal errors to the NT event log. On Windows 95 systems, the run-time DLL creates a log file in the Windows directory and logs the error there.

For More Information See "FatalErrorResponse Event" in the Language Reference for more information on handling nonrecoverable errors.


Deploying Your IIS Application

You package and deploy your IIS application using the Visual Basic Package and Deployment Wizard. The Package and Deployment Wizard packages your project .dll files and all associated files into a "cabinet" or .cab file. The wizard can then deploy this cabinet file and its associated support files to a location you indicate on a Web server.

Note Although IIS applications are deployed to a Web server and not to the client computer, you still must package them into a cabinet file and deploy them to the Web site. The .cab file is then unpacked on the Web server and the necessary components are installed and registered.

You must either deploy to an existing virtual directory on the server or manually create a virtual directory before you deploy your .cab file to it. To create a virtual directory, you define a directory on the Web server, then use the Internet Server Manager in Internet Information Server to define this directory as a virtual directory and set an alias for it. See your Microsoft Internet Information Server product documentation for further details on creating virtual directories.

The application files that must be deployed include:

  • The .dll files for the project — including the WebClass object and the Visual Basic run time, and the .dsr and .dsx files for the project. These are automatically packaged into the .cab file when you run the Package and Deployment Wizard.

  • The HTML page or pages associated with the project. The Package and Deployment Wizard does not package these into the .cab file, but it copies them to the indicated location on the Web site when you deploy the .cab file.

  • Any files referenced by the HTML pages, such as images. The Package and Deployment Wizard does not automatically recognize these dependencies, but you can add them to the list of dependency files the wizard displays.

These are the steps in deploying your IIS application to the Internet:

  1. Debug and compile your application.

  2. Use the Package and Deployment Wizard to build a .cab file that contains the necessary files for your application.

  3. Use the Package and Deployment Wizard to deploy your application to the Web server you want to use.

For More Information See "Distributing Your Applications," in the Programmer's Guide for instructions on using the Package and Deployment Wizard to package and deploy your IIS application.


Downloading ActiveX Components

Visual Basic lets you use ActiveX technology to create components that can be used on HTML pages or as alternatives to an HTML page. These components can include controls (.ocx files), code components (.exe and .dll files) that run on the client, or ActiveX documents (.vbd files) that function much like an HTML page. Using these solutions, you can create highly functional Internet solutions to your business needs. Internet component download delivers these ActiveX solutions to users through an Internet browser. This section offers information on how to package and download your ActiveX controls, code components, and ActiveX documents.

Note In addition to ActiveX controls, code components, and ActiveX documents, you can create Internet applications using ActiveX designers. Information about deploying these Internet applications, called DHTML applications and IIS applications, can be found in their respective chapters. This chapter covers downloading of other ActiveX components; however, much of the information contained here will help you understand the process of deploying Internet applications in more detail.

Delivering software on the Internet raises new issues in software distribution. For example, you cannot predict the scope of the audience that will download your software or the configuration of their computers. In addition, the process of downloading a component is often a passive process that occurs without a specific request from the user. Internet component download helps address these issues.

For More Information See Chapter 2, "Developing DHTML Applications," for information on deploying DHTML Internet applications. See Chapter 3, "Developing IIS Applications," for information on deploying IIS applications.

Topics

Steps to Prepare Your Component for Download

Describes the steps involved in preparing ActiveX controls, ActiveX documents, and code components for Internet component download.
Internet Component Download

Introduces the process of Internet component download.
Testing your Internet Component Download

Explains the major scenarios you need to test before deploying your components to the Web.
Advanced Customization for Internet Component Download

Explains how to customize and rebuild your download files.
Manually Deploying ActiveX Components

Explains how to package files for download without using the Package and Deployment Wizard.

For More Information See http://support.microsoft.com/support/inetsdk for a collection of information about Internet component download. See Microsoft Developer Network Online on the Microsoft Web site at http://www.microsoft.com/msdn/ for the latest news on ActiveX, Visual Basic, and other Microsoft technologies.


Steps to Prepare Your Component for Download

When you prepare ActiveX components for Internet download, you must package them into a file that can be delivered to the user's browser. In addition, you must perform a few precautionary steps to ensure users who download your components that your software will not harm their computers.

There are five general steps to take in preparing your ActiveX control, code component, or ActiveX document for Internet download. The following table indicates the steps and the types of ActiveX components to which each step applies.

Step | ActiveX Controls (.ocx files) | Code Components (.exe/.dll files) | ActiveX Documents (.vbd files)
Digitally sign the software to be distributed. | X | X | X
Verify the software's safety levels. | X | X | X
Arrange licensing of items that require it. | X
Package the item for download. | X | X | X
Test your download. | X | X | X


Digital Signing for ActiveX Components

Internet Explorer's default security settings require that any software available for download must have a digital signature before download can occur. A digital signature provides a way to verify:

  • The contents of a file.

  • That the file comes from a responsible source.

Signatures verify content by providing a means of ensuring that a file's contents have not been altered since it was first made available for download. A digital signature verifies the source by identifying the legal entity that created the software. When you include a signature with a piece of downloadable software, you are the legal entity. The legal entity may be held responsible for any destruction caused by signed software when it is downloaded or run.

Software That Should Be Signed

There are five types of files to which you can apply a digital signature:

  • .exe files

  • .cab files

  • .dll files

  • .ocx files

  • .vbd files

If you are providing one of these types of files for download, you should establish a digital signature for it.

Note Normally, it is sufficient to sign only the .cab file in which your components are packaged. However, if you intend to distribute an .ocx, .exe, .vbd or .dll without packaging it in a .cab file, you should sign the component itself.

You provide a digital signature by purchasing a certificate from a certificate authority. A certificate authority is a company that validates your identity and issues a certificate to you. The certificate contains your digital signature and is a verification of your credentials. In the event of any problems, the certificate authority becomes a witness to your identity.

Public Keys and Private Keys

You use a technology called Authenticode™ when working with digital signatures. The goal of Authenticode is to deter the distribution of potentially harmful code by creating accountability. Authenticode verifies the publisher of a piece of code to Internet end users who are downloading it. In addition, Authenticode ensures users that the code has not been altered after the signature was applied.

Authenticode technology is derived from public key signature technology. Public key signature technology uses what are known as key pairs to encrypt data. Key pairs are used to encrypt and decrypt files. In public key technology, a public key and a private key ensure the privacy of files. A public key is used to encrypt the data, and a private key is used to decrypt it. While this is a successful means of protecting smaller files such as e-mail messages, the process is time consuming for larger files. Authenticode is the modified form of this technology, designed for use with larger files.

Authenticode and Signing

The following steps occur in the Authenticode process:

  1. When the developer signs the file, a number called a hash is calculated. The hash number represents the total bytes in the file. This number is encrypted using a private key and inserted into the file. The developer then packages and deploys the file to a Web server.

  2. When a user downloads or installs the file, their computer calculates a second hash number and compares it to the original. If the numbers match, the content is verified.

  3. The browser uses the public key to determine your identity and the certificate authority that provided the digital signature.

  4. The certificate authority verifies the source's identity and issues a certificate that contains the source's name encrypted with the private key.

  5. The browser uses the private key to decrypt the file. Installation proceeds.

The Authenticode software utility you need to apply a digital signature can be found in the ActiveX SDK, available for download from Microsoft's Internet site. The digital signature must be obtained from an issuing authority such as GTE or VeriSign, Inc.

For More Information See the Internet Client SDK on your MSDN Library CD-ROM, or at http://www.microsoft.com/workshop/prog/inetsdk/ for more information on how to download the ActiveX SDK. For more information on public and private encryption, search the Internet for RSA or public-key signatures.

Safeguarding Your Certificate

It is very important to keep your certificate safe because your firm guarantees any file signed with the certificate, regardless of whether the signature was authorized or not. You should keep the certificate with the certificate authority that issued it and send files there for signing or, if you keep the certificate on site, strictly control access to it.

For More Information See the Authenticode link on the Microsoft Security Advisor table of contents on the Microsoft Web site at http://www.microsoft.com/security to learn the latest about security and digital signing.

How to Sign Your Code

You can sign any .ocx, .exe, .dll, .vbd or .cab file you intend to distribute. There are two main steps to perform when you sign code. First, you must apply for a certificate, then you sign your code.

To sign your code

  1. Apply for a certificate from a certificate authority. See http://www.microsoft.com/workshop/prog/security/authcode/certs.htm for instructions on obtaining a certificate.

  2. Get the latest version of the ActiveX SDK from http://www.microsoft.com/workshop/prog/sdk/. This contains the tools you need to sign your code.

  3. Prepare you files to be signed. If you are signing any .exe, .ocx, .vbd or .dll file, you do not need to do anything special. If you are signing a .cab file, you must add the following entry to your .ddf file and remake your .cab file:
    .Set ReservePerCabinetSize=6144
  4. Sign your files using the tools in the ActiveX SDK. The following is an example of how you might sign a file:
    Signcode -prog myfilename -name displayname -info http://www.mycompany.com - spc mycredentials.spc -pvk myprivatekey.pvk
  5. Test your signature:
    • To test a signed .exe, .dll, .vbd or .ocx file, run chktrust filename
      where filename is the name of the file you signed.

    • To test a signed .cab file, run chktrust -c cabfilename.cab
      where cabfilename is the name of the .cab file you signed.

If your signing process was successful, either of these tests will display your certificate.


Safety Settings for ActiveX Components

Safety settings are used to guarantee users that an ActiveX control will interact safely with their computer and its data. When you distribute ActiveX controls for Internet component download, you must assign safety levels to them. If you do not, and your controls damage users' computers or corrupt their data, you can be held legally accountable if the controls were signed.

You can help prevent these problems by verifying that your code is safe and marking it as such. There are two levels to safety in Internet component download:

  • Safe for initialization

  • Safe for scripting

Note Safety settings apply only to components downloaded in Internet Explorer.

Safe for Initialization

When you mark a control safe for initialization, you guarantee that it will not perform harmful actions on the end user's computer, no matter what data or scripts are used in its initialization. A control that is safe for initialization does not write or modify any registry entries, .ini files, or data files as a result of initialization parameters. Safe for Initialization makes no claims about the safety of the control's methods, run-time properties, or the information it makes available to a script writer.

By default, Internet Explorer displays a warning and does not download controls that have not been marked safe for scripting and initializing. You can designate your software as safe for scripting and initialization when you use the Visual Basic Package and Deployment Wizard to package it for Internet distribution.

Safe for Scripting

When you mark a control safe for scripting, you guarantee that no script can cause the control to damage the users' computers or data. Controls marked safe for scripting should not be able to obtain unauthorized information from the users' computers or corrupt their systems.

Before you mark a control safe for scripting, you must verify that it does not perform any illegal activities or allow openings that could be used to cause damage. In general, controls that can automatically obtain any information about the user and expose that information to a script writer are not safe for scripting. Such seemingly innocent activities can be considered criminal acts in some countries.

In particular, a control should not perform the following actions as a result of scripting:

  • Insertion or retrieval of custom, script-defined registry and .ini file information. In other words, users should not be able to specify through a script which registry or .ini file information to insert.

  • Insertion or retrieval of variable registry and .ini file information that is not owned by the control.

    Note It is acceptable for a control to insert and retrieve registry and .ini file information that is predefined when the control is distributed, belongs only to the control, and exists for the purpose of helping the control manage its internal functions.

  • Reading a file from a hard drive with a name specified by scripting.

There is a fine line between safe and unsafe actions. For example, an ActiveX control that always writes information to its own registry entry may be safe, but a control that lets you name that entry is unsafe. A control that creates a temporary file without using any initialization or scripting value may be safe, but a control that allows the name of the temporary file to be assigned at initialization or by scripting is unsafe.

Prior to marking a control safe for scripting, it is advisable to create documentation recording the justification, taking the same type of care due any legal contract. This documentation can be included in the .inf file for your control. Your documentation might include:

  • A review by an expert, external developer who understands both the source code and VBScript.

  • A table listing all exposed methods, events, and properties of the controls.

  • A table listing all the files opened, API calls used, and the information retrieved or written.

If there are any dependencies or data transfer between the elements of these two tables, then the control is probably not safe for scripting.

Note Determining whether a control is safe is not a trivial exercise. To understand what type of functionality you should not implement for your control, you might begin by noting the API calls and commands not implemented in VBScript. See http://www.microsoft.com/vbscript/ for information on the API calls and commands not implemented in VBScript.

Limitations of the Safety Flags

A control marked safe for scripting and safe for initialization is not necessarily safe to use. The previous two sections list actions that the control cannot perform as a result of initialization or scripting, but a control could still perform these unsafe actions at other times.

For example, suppose you create an ActiveX control that could reformat your hard-drive after 10 uses. This action does not occur as a result of scripting or initialization, so you can mark the control as safe. Of course, the person who writes such a control is liable for the usual penalties reserved for virus writers.

Responsibility for ensuring that adequate safeguards are in place belongs to the developer, not to end-users or to HTML authors. If you as the developer fail to include adequate safeguards, you can be held legally responsible.

The bottom line in ensuring the safety of your software is to always have your code independently reviewed by a seasoned developer who understands the issues well. You may want to include information about this review in the .inf file for your download.

For More Information See "Designing Controls for Use With HTML" in "Building ActiveX Controls" for guidelines for making your controls safe for scripting and initialization. See "Advanced Customization for Internet Component Download" for more information on including information in the .inf file.


Setting Safety Levels for ActiveX Components

There are two ways you can set safety levels for your downloadable components. The easiest way is to use the Package and Deployment Wizard to mark your .cab files as safe. The other option is to use the IObjectSafety interface to tell the client machine that your code is safe.

Note IObjectSafety is an advanced coding topic that requires an understanding of the Implements interface and is intended for experienced programmers. For information on the Implements interface, see "Polymorphism" in Programming with Objects," in the Programmer's Guide.

Using the Package and Deployment Wizard

If you do not implement the IObjectSafety interface on your objects, the Package and Deployment Wizard will present you with the opportunity to set safety flags on your download package. You can select the Safe for Scripting flag, the Safe for Initialization flag, or both.

Note If you have used the IObjectSafety interface for an object, it will not appear in the safety settings screen of the Package and Deployment Wizard.

For More Information See "Distributing your Applications," in the Programmer's Guide for more information on using the Package and Deployment Wizard to package your controls.

Using IObjectSafety

IObjectSafety is an interface that exposes functionality to Internet Explorer's Safe for Scripting and Safe for Initialization security features. IObjectSafety should be implemented for objects that have interfaces that support untrusted clients — or those clients for which specific functionality cannot be predicted or safe usage cannot be guaranteed — such as scripts. The interface allows you to specify which parts of the object need to be protected from use.

Using IObjectSafety, you can mark your object and its parts in three ways:

  • Safe for Automation with untrusted Automation clients or scripts

  • Safe for initialization with untrusted data

  • Safe for running untrusted scripts

There are three possible scenarios to consider for your objects:

  1. All properties and methods on your object are always safe for scripting. In this scenario, you can notify the client that your object is safe for scripting by not returning an error in the IObjectSafety_SetInterfaceSafetyOptions interface method. This allows your object to be successfully created and run.

  2. Your object is never safe for scripting. In this case, you can notify the client that your object is not safe by returning an error, E_Fail, in the IObjectSafety_SetInterfaceSafetyOptions interface method. This prevents the client from accessing any methods or properties through script.

  3. Some but not all of the properties and methods on your object are unsafe for scripting. In this case you can either notify the client that your entire object is unsafe, or notify the client that your object is safe, but disable the properties and methods that are unsafe.

For More Information See the Internet Client SDK at http://www.microsoft.com/msdn/sdk/inetsdk/help/compdev/safety.htm for more information on the IObjectSafety interface. An IObjectSafety sample application can be found in the Samples directory.


Licensing for ActiveX Components

When you add an ActiveX control to a Web page, you are distributing it to any users who download the control from the page. Unless you license the control, there is little to prevent an end user from taking your control and using it in their own applications. The license acts as a kind of copyright for your control, preventing unauthorized use.

Licensing ActiveX controls for use on the Internet is a complex subject. An expression in a license such as "used on one computer" has a totally different meaning on the Internet when the "one computer" may be an HTTP server that is hosting 200 different IP addresses. The software may be in-use (running in memory) on thousands of anonymous computers and never in use on the computer for which it was licensed. These issues should be reviewed before you license your ActiveX controls for use on the Internet.

This section explains Microsoft's current licensing scheme, how to implement it with Visual Basic, and how HTML authors can use it on their pages. A general discussion of some licensing issues is also included.

For More Information See the Internet Client SDK at http://www.microsoft.com/workshop/prog/inetsdk/ for detailed information on the licensing model and the .lpk file.

Microsoft's Licensing Scheme

Visual Basic implements a licensing scheme that uses a mechanism that contains methods the computer uses to retrieve a control's licensing information and create instances of objects. This is the most common licensing scheme in use.

The basics of licensing through this scheme are:

  • Each HTML page using one or more licensed controls requires a license file (.lpk) that contains the license strings for each control.

  • Each HTML page can use only a single .lpk file, but a single .lpk file can be used for more than one page.

  • The .lpk file must be on the same server as the HTML page it covers.

  • The .lpk file must contain a plain text copyright notice to dissuade casual copying of .lpk files.

The License File

The license file is an .lpk file you associate with one or more HTML pages. It contains license strings for each ActiveX control on the pages to which it refers. You use the License Package Authoring Tool to create an appropriate license file for their pages. This tool, which is available in the Tools directory, presents you with a list of controls you can include in you license file.

A sample license file is shown below:

LPK License Package
//////////////////////////////////////////////////////
// WARNING: The information in this file is
// protected by copyright law and international
// treaty provisions. Unauthorized reproduction or
// distribution of this file, or any portion of it,
// may result in severe criminal and civil penalties,
// and will be prosecuted to the maximum extent
// possible under the law. Further, you may not reverse
// engineer, decompile, or disassemble the file.
//////////////////////////////////////////////////////
{3d25aba1-caec-11cf-b34a-00aa00a28331}
AQWWF/QT0BG9ewCg0QKOmo=
BQAAAA=yhtrFpw/zxGAdURFU1QAACkAAAB
DAG8AcAB5AHIAaQBnAGgAdAAgACgAYwApACAAMQA5ADkANQAsACAAMQA5ADkANgAg
AE0AYQBjAHIAbwBtAGUAZABpAGEALAAgAEkAbgBjAC4A=

Important The License Package Authoring Tool won't create an entry in the .lpk file for your ActiveX control unless you check the Required License Key checkbox in the Project Options dialog of your control project.

The License Manager

You must embed an object called the license manager in the Web page from which a user will download your ActiveX controls. The license manager uses an OBJECT tag to reference the control's .lpk file. If you use the Package and Deployment Wizard to create your download, it produces the license manager and inserts it in the file for you.

The following is a sample of the license manager:

<!--    If any of the controls on this page require licensing, you must create a license package file. Run LPK_TOOL.EXE in the Tools directory to create the required .lpk file.
-->
<OBJECT CLASSID="clsid:5220cb21-c88d-11cf-b347-00aa00a28331">
<PARAM NAME="LPKPath" VALUE="LPKfilename.LPK">
</OBJECT>

Note You must replace the LPKPath and LPKfilename.LPK parameters in the example above with the path and name of your license file.

This HTML code identifies the .lpk file as an object with the class ID shown above.

For More Information See "Licensing ActiveX Controls" in the ActiveX Controls section of the Internet Client SDK at http://www.microsoft.com/workshop/prog/inetsdk.

Other License Models

There are other licensing models besides Microsoft's scheme. One requires a key that varies with time. In this approach, the following steps occur:

  1. The browser makes a call to an HTTP server that examines the URL of the HTML page making the call.

  2. The server examines the user's IP location for run-time licenses, then returns a license key.

There are also other licensing mechanisms possible. For example, if run-time licenses are required, then Basic authentication, NT authentication, or other authentication may be required before the license file may be obtained.

For More Information See the "Authentication and Security for Internet Developers" white paper at http://www.microsoft.com/workshop/server/asp/feature/security-f.htm for more information on basic text authentication and NT security.


Internet Component Download

Downloading applications or components from the Internet is significantly different from installing a standard Visual Basic application. In standard applications, the user uses a setup program to install the software. On the Web, users download the application after accessing a Web page from which it is available. The browser, rather than a setup program, is responsible for copying all the needed files to the user's computer.

End users use Internet component download to automatically download and install Internet applications and ActiveX components from Web pages. You can use the Package and Deployment Wizard to prepare your software for Internet component download. The Package and Deployment Wizard creates a cabinet (.cab) file that contains all of the information necessary to download, install, and register your software.

The benefits of Internet component download include:

  • File compression for faster download.

  • A single file for your component and an .inf file that describes other files required.

  • Dependency files, such as Msvbvm60.cab, will only be downloaded as necessary — thus minimizing download time.

  • Easier updating when new versions of your component are created.

  • Automatic installation performed when the control is downloaded.

For More Information See the "Code Download Training Site" at http://support.microsoft.com/support/inetsdk for a collection of information about Internet component download. See "Using the Package and Deployment Wizard to Prepare Software for Download" in this chapter for more information about the concepts in Internet component download.


How Internet Component Download Works

In Internet component download, you first package the components you want to download into a .cab file so that the browser can download them intelligently. The Package and Deployment Wizard's packaging process builds an HTML page that refers to this cabinet file through an OBJECT tag. When a user opens this Web page, the browser reads the OBJECT tag and retrieves the necessary files for the download.

As part of the .cab file creation process, the Package and Deployment Wizard creates an .inf file that contains information about the download. You can customize this file if you need to include additional information.

Note If you update the .inf file, you must rebuild your .cab file.

For More Information See "Packaging Files for the Package and Deployment Wizard" in this chapter for a full list of the files created with the .cab file.

The OBJECT Tag

The following HTML fragment shows the OBJECT tag for a .cab file called MyContrl.cab:

<OBJECT
   CLASSID="clsid:25BDF09D-EC8B-11CF-BD97-00AA00575603"
   CODEBASE="/Controls/MyContrl.cab#version=1,0,0,0"
   ID=MyContrl>
</OBJECT>

The OBJECT tag shown above contains three attributes:

  • CLASSID — contains the class ID used to register the control in the Windows registry.

  • CODEBASE — points to a location from which to download the .cab file and specifies the version number of the control contained in the .cab file.

  • ID — specifies the name to use when scripting the component.

The Download Process

The system performs the following actions when downloading a .cab file's contents onto a client machine:

  1. The browser checks the registry for the class ID in the OBJECT tag. If it fails to find it, it processes the CODEBASE attribute. If the browser does find the class ID, it checks the file referenced in the registry entry to see if it is current and proceeds with the download if it is not.

  2. The browser downloads and unpacks the appropriate .cab file.

  3. The browser processes the .inf file associated with the .cab file.

  4. Internet Explorer makes registry settings, including those related to the component's status as safe for scripting or safe for initializing, if appropriate.

  5. The browser checks to see whether you have any necessary files or components already installed. If you do, it checks to see whether the files or components are current. If they are current, the browser makes no changes to them. If they are not current, the browser replaces them with a new copy that it downloads and extracts.

  6. The browser installs and registers your component.

Checklist for Component Download

The following checklist shows the steps that you should follow to create a fully signed, safe Internet component download for an ActiveX control.

Note If you are creating a download for another type of ActiveX component, you would not perform the steps involving safety settings.

  1. Design the software, specifying the intended software safety level.

  2. Create the software and test. If you are using the IObjectSafety interface, implement the appropriate safety settings on your objects.

  3. Create a document showing the object is safe.

  4. Get an independent review.

  5. Recreate the .cab using the Package and Deployment Wizard, and set the appropriate safety flags if you did not use the IObjectSafety interface.

  6. Modify the .inf file as required to add readme information and other files.

  7. If you modified the .inf in step 6, recreate the .cab file using Makecab.exe.

  8. Digitally sign the .cab file.

  9. Test the final .cab file with each platform on which it may run.

Resources for Component Download

The following resources may be needed to create and test your packages for Internet component download. Most of the resources are part of Visual Basic.

Resource | Purpose | Source
pdaddin.dll or
pdcmdln.exe 		Package your components for download using the Package and Deployment Wizard, either from within Visual Basic (pdaddin.dll) or as a stand-alone executable (pdcmdln.exe). Visual Basic installation
lpk_tool.exe | Create the license for any controls. | Visual Basic Tools directory
makecab.exe | Build cabinet files. | Visual Basic installation
signcode.exe | Manage digital signatures | ActiveX SDK
Authenticode certificate | Identify the developer | Verisign Inc., GTE, or another vendor that provides signatures
Internet Explorer | Test download and installation | www.microsoft.com
regsvr32.exe | Register and unregister software for testing | Visual Basic Tools directory


File Packaging with the Package and Deployment Wizard

You can use the Visual Basic Package and Deployment Wizard to quickly and easily package your ActiveX components for Internet component download. The Package and Deployment Wizard packages your components into a file that the browser can recognize, and creates a Web page from which the user can download the page. You then deploy the resulting materials to a Web server where users can access it.

Note Before using the Package and Deployment Wizard to package your own ActiveX control for Internet component download, you should become familiar with several important concepts, including component safety and digital signatures.

Files Created by the Package and Deployment Wizard

The Package and Deployment Wizard creates two main sets of files when it packages code for Internet component download: distribution files and support files. Distribution files are located in the directory you specify when you begin the wizard. This directory typically contains the .cab file and any .htm files associated with it.

Note If you make any changes to the project after you build the .cab file, such as modifying the list of files to include with the download or digitally signing the components, you may need to rebuild the .cab file.

The wizard creates a directory for support files and places the input file (.inf file) for the .cab in this directory. In addition, the support files directory contains the Diamond Directives (.ddf) file, and other files needed for the download.

The following table lists all the file types created by the wizard:

Extension | Description
.cab | Windows setup file or "cabinet" file that contains the .ocx file, the .inf file and other dependent files. You can digitally sign this file to prevent tampering.
.htm | HTML file used to display a Web page. This file contains a link to the .cab file and is used to launch the download process.
.ddf | Diamond Directives file. This is the project file for creating the .cab files.
.inf | Code download information file. This file includes information on how the control should be installed. It permits customization of installation.
.ocx | ActiveX control. You can digitally sign this file to prevent tampering.
.dll | ActiveX document or code component.

HTML Created by the Package and Deployment Wizard

The following fragment from an .htm file illustrates what the Package and Deployment Wizard produces in a typical Internet component download.

<HTML>
<!--   If any of the controls on this page require licensing, you must create a license package file.
   Run LPK_TOOL.EXE in the tools directory to create the required LPK file.

<OBJECT CLASSID="clsid:5220cb21-c88d-11cf-b347-00aa00a28331">
   <PARAM NAME="LPKPath" VALUE="LPKfilename.LPK">
</OBJECT>
-->
<OBJECT
   classid="clsid:F651BF93-239B-11D0-8908-00A0C90395F4"
   id=ShapeLabel
   codebase="ControlDemo.CAB#version=1,0,0,1">
</OBJECT>
</HTML>

Examine the second Object tag in the example above. This tag contains a reference to a control with the ID ShapeLabel. It has a class ID of F651BF93-239B-11D0-8908-00A0C90395F4. Each ActiveX control you create will have a different class ID. The class ID is used to create an instance of the control on the HTML page, similar to the process of placing a control on a Visual Basic form.

Internet Explorer uses the class ID to check the registry to see if the control exists. When it does not exist or when the version number of the control is less than the version specified in the CODEBASE attribute, Internet Explorer downloads and installs the specified file.

The important parts of the Object tag include:

Tag Attributes | Description
CLASSID | Class identifier, contains the class ID for the component.
ID | Name of the component. Used in scripting, this is equivalent to the Name property of a control on a Visual Basic form.
CODEBASE | Minimum version number of the control required and the location of an installation point.

For More Information See "Distributing your Applications" in the Programmer's Guide for instructions on using the Package and Deployment Wizard to package your controls. See http://www.w3c.org for more information about the OBJECT tag and its use. See http://www.microsoft.com/workshop/prog/cab/ for more information on .cab files.


Testing your Internet Component Download

Testing your download file is more complex than testing a conventional setup program because the software does not install unless one of two things is true:

  • The software does not already exist on the user's computer.

    -or-

  • The software being downloaded is newer than any version that already exists on the user's machine.

Because of this, you must be careful when you test your download to ensure that you test both scenarios. As a developer, you should test your application on multiple machines with varying configurations. At a minimum, you should test the download on both your development machine and on a machine that does not have Visual Basic installed, and on multiple operating systems. In addition, you must check your safety levels.

Note If you do not have a machine without Visual Basic installed, you can simulate one. See "Testing Component Download on Clean Machines" for instructions.

Setting Safety Levels Prior to Testing

Before testing your download, be sure the safety level in Internet Explorer is set to Medium. If the safety level is set to High, the required files will not get downloaded. If you set the safety level to None, all of the missing components will be installed without warning.

When the safety level is set to Medium, the following warnings occur:

  • The browser warns you that an installation attempt is being made and prompts you to install the .cab file. If you choose to continue, the browser downloads and expands the .cab.

  • The browser asks if you want to install any dependency files, such as Microsoft Automation or the Microsoft Visual Basic run-time library. The Authenticode screen displays information about these components. If you choose to continue, the browser installs these files.

  • If you are downloading an ActiveX document in a version of Internet Explorer prior to 4.0, a script warning appears warning you that the Web page contains scripting code. This code is necessary in order to view your ActiveX document. If you choose to continue, the download proceeds. This warning does not appear in Internet Explorer 4.0 or later.

These warnings are all part of the Internet Explorer's safety mechanisms. If you do not receive all of these warnings, do not be alarmed. You may not receive a warning if a necessary component is already installed on the client machine or a security level option in Internet Explorer is set to something other than the defaults.

To check the safety level in Internet Explorer 4. x

  1. Click Internet Options from the View menu.

  2. From the Options dialog box, click the Security tab.

  3. Click the appropriate safety level, then click OK.

To check the safety level in Internet Explorer 3.x

  1. Click Options from the View menu.

  2. From the Options dialog box, click the Security tab.

  3. Click the Safety Levels button.

  4. Click the appropriate safety level, then click OK.

  5. Click OK to return to the browser.

Testing Safety Levels

You can check safety levels by creating additional .cab files and HTML pages for testing, as suggested in the following table:

To test pages with | Create a page that hosts a control with
No initialization or scripting | No PARAM values or other variables set by the control.
Initialization only | PARAM values assigned to a control.
Scripting only | PARAM values set by VBScript only.
Scripting and initialization | Initial PARAM values set and then modified by VBScript.


Testing Component Download on Development Machines

You can test your component download on your development machines to make sure that the download file installs correctly. However, because your development machines are likely to have some of the necessary components or files already installed, you may not experience the same download as your end users will. In addition to testing on your machines, it is important to also test your installation on a machine that does not have Visual Basic installed.

Note If you do not have a machine without Visual Basic installed, you can simulate one. See "Simulating a Clean Machine" for instructions.

Testing Installation of the Component on the Development Machine

The simplest way to check your download is to remove the software registration from the registry, then access the page generated by the wizard.

Note The procedures in this section require a copy of regsvr32.exe to be installed on your machine. If it is not, you can find it on the Visual Basic installation CD-ROM.

To test installation on the development machine

  • If you are using Internet Explorer 4.0 or later, access the Downloaded Program Files folder in either the Windows or the Winnt40 directory, right-click the control, and select Remove. The system automatically removes and unregisters the item and any of its dependencies.

  • If you are using a version of Internet Explorer prior to 4.0, run Regsvr32 to unregister the component, as in the following example:
    regsvr32 /u controldemo.ocx

If regsvr32.exe is not in your path, provide the full path to the file in your statement. Access the Web page the Package and Deployment Wizard created. The component should download.

Note Remember to select Binary Compatibility in your Visual Basic project settings to prevent multiple, different class IDs for your software.

Note If you leave the DestDir= line in the .inf file blank, the component file should appear in your Occache or Downloaded Program Files directory. You may find other copies in subdirectories called conflict directories, such as Occache\Conflict.2. The date and time of each file will differ, indicating different builds of the control and different class IDs. Care must be taken to ensure that the Class ID remains the same between the builds.

For More Information See "Version Compatibility in ActiveX Documents" in the Component Tools Guide for more information on binary compatibility.

Testing the Installation of Support Files on the Development Machine

In addition to removing the component registry, you should test that all of the necessary support files also install and register correctly. To do so, temporarily unregister everything in your system directory and your Occache or Downloaded Program Files directories.

Note The procedures in this section require a copy of regsvr32.exe to be installed on your machine. If it is not, you can find it on the Visual Basic installation CD-ROM.

To test installation of support files on the development machine

  1. Unregister the support files:
    • If you are using Internet Explorer 4.0 or later, access the Downloaded Program Files folder in either the Windows or the Winnt40 directory, right click the file, and select Remove. The system automatically removes and unregisters the item and any of its dependencies.

    • If you are using a version of Internet Explorer prior to 4.0, run Regsvr32 to unregister the files. Use the following command line in each applicable directory to unregister the necessary files:
      for %f in (*.ocx *.dll) do regsvr32 /u /s %f
  2. Once all of the support files have been unregistered, access the Web page the Package and Deployment Wizard created. The component should download.

To reregister support files after testing

  • Use the following command to restore the original entries:
    for %f in (*.ocx *.dll) do regsvr32 /s %f


Testing Component Download on Clean Machines

By testing your download on a clean machine — one that does not already have Visual Basic installed — you get an accurate look at the download experience your users are likely to encounter. This is an essential part of the testing process. You can simulate a clean machine by removing certain files and registry settings from one of your development machines, then restoring those files and settings after you have finished testing.

Note The procedures in this section require a copy of regsvr32.exe to be installed on your machine. If it is not, you can find it on the Visual Basic installation CD-ROM.

Simulating a Clean Machine

One of the systems you test should be a machine that does not have Visual Basic or the Visual Basic Run-time Library installed — a "clean machine". If you do not have access to this type of machine, you can simulate one by following the instructions in this section. These steps may also be taken to clean a client machine that has been exposed to one or more of the files in the download.

Warning Removing, renaming, or unregistering files as described below can impact other programs on your machine. Before deleting, renaming, or unregistering any files, it is advisable to copy these files to a safe alternate location from which you can restore them.

This process requires a copy of Regsvr32.exe. This file may be installed onto your development machine; if it is not, it may be found on the Visual Basic installation CD-ROM.

To simulate a clean machine

  1. From the Start menu, click Run.

  2. In the Run dialog, type in the following command;
    Regsvr32.exe /U <Path to Windows folder>\System\Msvbvm60.dll

    Note If you are running on a Windows NT machine, the Msvbvm60.dll is installed into the Windows\System32 folder.

  3. From the Start menu, click Find, and then select Files or Folder.

  4. Delete or rename the following two files, located in either your System or System32 directory:

    Msvbvm60.dll
    Asycfilt.dll

    Important Do not delete other versions of these files in alternative locations.

  5. Unregister and delete or rename the item to be downloaded and any remaining dependency files installed on the client machine:
    • If you are using Internet Explorer 4.0 or later, access the Downloaded Program Files folder in either the Windows or the Winnt40 directory, right click the control, and select Remove. The system automatically removes the item and any of its dependencies.

    • If you are using a version of Internet Explorer prior to 4.0, use the following syntax:

      Regsvr32.exe /U <PATH>\System\file

    where file is the name of the file to unregister.

    Note Most controls install into the Windows\System, Windows\System 32, Windows\OCCache, or the Downloaded Program Files folders. However, some downloaded controls are not visible in Explorer's file view after download. It is necessary for these files to open a DOS window, navigate to the Downloaded Program Files folder, then run regsvr32 for those .ocx files to unregister them. You can then delete them at the DOS prompt.

    Tip For a list of dependency files, open the .inf file created by the Package and Deployment Wizard and look in the Add.Code section. If you are uncertain whether a file needs to be unregistered, you can run REGSVR32.EXE with the /U switch. If you receive an error, the file does not need to be unregistered.

To test your download on a simulated clean machine

  • Once all of the dependency files have been unregistered and renamed or deleted, access the Web page the Package and Deployment Wizard created. The component should download.

Restoring Your Simulated Clean Machine

You can reverse all the changes you made when you simulated a clean machine, if necessary. Normally, you do not need to do this because downloading the component you were testing should install and register the necessary files. However, if something goes wrong during download, you can restore the files yourself.

There are two steps to restoring your machine: You must rename or restore any files you deleted or renamed, and you must re-register the files you removed from the registry. When you restore, you must rename or restore any file you deleted or renamed.

To retrieve a file you deleted

  • Retrieve it from the Windows Recycle Bin.

    -or-

  • Obtain a fresh copy from the Visual Basic CD-ROM, if it was originally obtained as part of your installation.

    -or-

  • Obtain a fresh copy from the appropriate third party, if the file did not come from the Visual Basic installation.

Once the necessary files have been restored, you can run Regsvr32.exe to register those files that you unregistered. For example, to register the Visual Basic Run-time Library, execute the following command from the run dialog:

Regsvr32 <Path to Windows>\System\Msvbvm60.dll

To register a file you unregistered

  • Use the Regsvr32.exe program to register the file, as shown in the previous example.


Troubleshooting your Internet Component Download

A few of the more common problems in Internet Component Download and their solutions are listed in this section. These include:

  • You receive an error deleting or renaming the Visual Basic run-time library when simulating a clean machine.

  • You receive the error message "The Dynamic Link Library could not be found in the specified path."

  • You receive the error message "An error has occurred copying MSVBVM60.DLL. Ensure the location specified below is correct."

  • Internet Explorer prompts "Opening file xxx.VBD. What would you like to do with this file? Open it or save to disk?"

  • You receive the error message "Internet Explorer is opening a file of unknown type: xxx.VBD"

Errors Deleting or Renaming the Visual Basic Run Time

You can receive these errors when you attempt to rename or delete the Visual Basic runtime (MSVBVM60.DLL) when simulating a clean machine. In most cases, these errors occur because the file is in use. You cannot delete the Visual Basic runtime when Visual Basic is running. In addition, this error may occur if the browser is viewing an ActiveX document or has currently done so.

To fix this error, close Visual Basic or close your browser and try to delete the file again.

Error: "The Dynamic Link Library could not be found in the specified path."

This error generally occurs during testing when the ActiveX document you have been trying to download is still present on the machine.

To fix this problem, delete the ActiveX document from either the OCCache folder or the Downloaded Program Files folder, then restart your download.

Error: "An error has occurred copying Msvbvm60.dll. Ensure the location specified below is correct:"

This error typically occurs when there is insufficient disk space on the machine to which you are attempting to download. To fix this error, free up disk space and restart your download.

Prompt: "Opening file DocumentName.VBD. What would you like to do with this file? Open it or save it to disk?

This error occurs when the Visual Basic runtime library (MSVBVM60.DLL) is not installed. Generally, this occurs when the safety level in Internet Explorer is set to High. To correct this, you must set the safety level in Internet Explorer to Medium or None.

Error: "Internet Explorer is opening file of unknown type: DocumentName.VBD from…"

This error can occur when you download an ActiveX document. The most common of these are listed below, with information on how to solve the problem.

Cause | Solution
You are using the wrong .vbd file | Make sure you are using the .vbd file provided by the Package and Deployment Wizard.
You are using an outdated .vbd file | The CLSID of your .vbd and .exe files may be out of synch. To preserve CLSIDs across builds in your projects, select Binary Compatibility on the Components tab of the Project Property dialog box.
Your actxprxy,dll is missing or not registered | Use regsvr32 to register the file, or try recompiling your project.
The Visual Basic runtime is not registered or not in the path | Use regsvr32 to register the file, or add it to your system path
Your ActiveX document is not signed or safe for scripting | Set the browser safety settings to Medium.
There is a run-time error in the ActiveX document's initialization code, particularly in the Initialize or InitProperties procedures | Do a run-time error check on your project.
.cab files are not being downloaded | Make sure you are not distributing Visual Basic's core-dependent .cab files, which are not signed, onto a machine with browser security set to High. -or- Select "Download from alternate web site" from the File Source window when packaging your project in the Package and Deployment wizard and try your download again.

Other solutions you can try include:

  • Turn on version numbering in your ActiveX document project and put the version number in a field so it displays on the main form of your downloaded page. This lets you check to find out if your project downloaded or if you are seeing an older version.

  • Always close and reopen the browser between download attempts to clear the cache.

  • Make sure you are using the latest versions of the controls and DLLs that ship with Visual Basic in your ActiveX document.

For More Information For more information on troubleshooting the download of ActiveX documents, see Article Q167380 in the Microsoft KnowledgeBase at http://premium.microsoft.com/support/kb/ARTICLES/.


Advanced Customization for Internet Component Download

There are several things you can do to customize your file download, including:

  • Modifying an .inf file for the project. You might do this if you want to include information about your security settings, or to include an outside review of your component.

  • Modifying the .ddf file for the project. You need to do this if you have added any additional files to your download, or if you have changed your .inf file.

  • Manually rebuilding the .cab file. You need to do this if you have modified your .inf or .ddf files in any way.

The following sections provide more information on making these changes.

Modifying the .inf File

You can customize the installation process by modifying the .inf file. The modified file can then be included in a manually rebuilt .cab file (using the .ddf project file), or it can be directly referenced by the CODEBASE attribute of the OBJECT tag.

Note An .inf file can be used in the CODEBASE attribute, but this should be avoided. Since an .inf file cannot have a digital signature, using it here would require you to digitally sign any compiled files (for example, .ocx files) to which it refers. This may require you to sign and deploy several executables individually, rather than signing one .cab file that packages all of them.

Typical modifications to an .inf file include:

  • Adding a license agreement.

  • Adding a readme file.

  • Adding additional documentation.

Note As an ActiveX control developer, you can modify your Internet component download files, but you should be aware that doing so may place a potential liability on yourself or your firm if the modifications are done incorrectly. Some changes indicate that you guarantee, assure, or warranty that the changes are correct and truthful. Attempts to avoid these liabilities by citing "as is" or "suitability" clauses in a license agreement may be ruled invalid by many courts.

Sample .inf File

Here is an example of an .inf file:

;inf file for ControlDemo.ocx
;DestDir can be 10 for Windows directory, 11 for Windows\System(32) directory, or left blank for the Occache or the Downloaded Program Files directory.

[version]
signature=$CHICAGO$

[Add.Code]
CONTROLDEMO.OCX=CONTROLDEMO.OCX
MSVBVM60.DLL=MSVBVM60.DLL

[CONTROLDEMO.OCX]
file-win32-x86=thiscab
RegisterServer=yes
clsid={F651BF93-239B-11D0-8908-00A0C90395F4}
DestDir=
FileVersion=1,0,0,1

[MSVBVM60.DLL]
hook=MSVBVM60.cab_Installer
FileVersion=5,1,43,19

[MSVBVM60.cab_Installer]
file-win32-x86=http://activex.microsoft.com/controls/vb6/MSVBVM60.cab
InfFile=MSVBVM60.inf

Important Only the original developer may legally mark a control as safe. There are no circumstances where you should ever mark someone else's control as safe. Changing the safety of a control can result in both a copyright infringement and criminal charges.

Adding Version Information to the .inf File

You can use the .inf file to specify the version of your component. The .inf file should have a file section that specifies a version and the same class ID specified in the <OBJECT> tag.

As illustrated in the example below, this file version specifies the version of the ActiveX control to be downloaded:

[circ3.ocx]
; The lines below specify that the specified Circ3.ocx (clsid,
; version) needs to be installed on the system. If it doesn't exist
; already, can be downloaded from the given location (a .cab).
NOTE:
; If "thiscab" is specified instead of the file location, it is
; assumed that the desired file is present in the same .cab cabinet
; that the .inf originated from. If the location pointed to
; is a different .cab, that cabinet is also downloaded and
; unpacked in order to extract the desired file.

file=http://www.code.com/circ3/circ3.cab
clsid={9DBAFCCF-592F-101B-85CE-00608CEC297B}

; Note that the {}s are required when entering the CLSID in the .inf
; file. This is slightly different from the HTML syntax for inserting
; CLSIDs in an <OBJECT> tag.

FileVersion=1,0,0,143

Modifying the .ddf File

You need to modify the project's .ddf file if you modify the .inf file or digitally sign your control in the Support directory. You also need to verify that any files added to the .inf file have also been added to the .ddf file, or they will not be available for installation.

Here is an example of a .ddf file created by the Package and Deployment Wizard:

.OPTION EXPLICIT
.Set Cabinet=on
.Set Compress=on
.Set MaxDiskSize=CDROM
.Set ReservePerCabinetSize=6144
.Set DiskDirectoryTemplate=
.Set CompressionType=MSZIP
.Set CompressionLevel=7
.Set CompressionMemory=21
.Set CabinetNameTemplate="ControlDemo.cab"
"C:\Website\Cabfiles\ControlDemo.inf"
"C:\ControlDemo\ControlDemo.ocx"

The following entries in the .ddf file should not be changed:

Entry | Description
MaxDiskSize=CDROM | This allows the .cab file to be as large as needed.
ReservePerCabinetSize=6144 | This reserves space for a digital signature.

Manually Building .cab Files

You need to rebuild the .cab file every time you modify the .inf or .ddf file associated with it. To rebuild the .cab file, you need Microsoft's Cabinet Builder, Makecab.exe. This file is usually located in \VB\Setupkit\kitfil32.

The simplest way to rebuild the .cab file is to use the existing .ddf file for the project with the following command:

makecab /f yourcontrolname.ddf

The Cabinet Builder generates two additional informational files, setup.inf and setup.rpt.

For More Information For more information on modifying files associated with a .cab file, see to the Microsoft Developer's Network. For detailed information about .inf files, see the documentation included with Microsoft's Visual C++ and on the MSDN Library CD-ROM.


Manually Deploying ActiveX Components

In addition to using the Package and Deployment Wizard to package your components for download, you can also manually prepare your files for Internet download. This approach requires more effort on your part than using the Package and Deployment Wizard. In most cases, you will want to use the wizard to package and deploy your controls. However, you may want to manually package your components if you want to include custom information not supported by the Package and Deployment Wizard.

The following sections explain the basics of manually preparing your controls, ActiveX documents, and code components for download:

  • Manually Deploying ActiveX Controls

  • Manually Deploying Code Components

  • Manually Deploying ActiveX Documents


Manually Deploying ActiveX Controls

You perform these steps to manually deploy ActiveX controls on an HTML page:

  1. Create an HTML page the users can access to download your control.

  2. On the HTML page, use the OBJECT tag to provide a means for the browser to download, register, and reference the ActiveX control. The object tag includes the control's class ID, an ID attribute used to reference the control, the CODEBASE tag, and parameters to set properties. The following shows an example of the HTML you might use:
    <OBJECT
classid="clsid:2F390484-1C7D-11D0-8908-00A0C90395F4"
codebase="http://www.mysite.com/controls/label.ocx
   #version=1,0,0,0"
id="Catalog"
width="150"
height="20"
align="center"
vspace="0"
>
<PARAM NAME="Caption" value="Our Catalog">
<PARAM NAME="FontName" value="News Gothic MT">
<PARAM NAME="FontSize" value="11">
<PARAM NAME="FontBold" value="1">
<PARAM NAME="ForeColor" value="000000">
</OBJECT>
  3. Use VBScript to handle click events so that clicking each control will load a specific .htm file in a specific HTML frame. The following code shows an example:
    <SCRIPT LANGUAGE="VBScript">
Sub Welcome_Click
Parent.Parent.Frames(1).Location.Href="Welcome.htm"
Welcome.ForeColor = 000000
End Sub
</SCRIPT>


Manually Deploying ActiveX Components

Manually Deploying Code Components

Code components are .exe or .dll files, rather than .ocx files. For client-side deployment, you can reference and script ActiveX components with the same kinds of HTML and VBScript code you might use for ActiveX controls. You perform the following steps to deploy a code component to a client:

  1. Use HTML to create a page containing an element to launch the display of the component. For example, you might create a button that would launch the component. Include a name for the element to use in script, a type to show on the page, and a "value" — similar to the Caption property of a command button in Visual Basic.

    The following code shows a sample of what your HTML might look like for a button that references a Login dialog box:

    <FORM NAME="LoginButton">
Click here to log in:
<INPUT NAME="cmdLogin" TYPE="Button" VALUE="Log in…">
</FORM>
  2. Use the OBJECT tag to provide a means for the browser to download, register, and reference the ActiveX component. The OBJECT tag includes the component's class ID, a unique identifier used to reference the component, and a CODEBASE attribute to tell the browser where to find the component.

    Note See "Manually Deploying ActiveX Controls" for an example of the OBJECT tag.

  3. Use VBScript to show the component. The following shows an example of how you would do this for a login dialog component:
    <SCRIPT LANGUAGE="VBScript">
' Create variables for the HTML form containing the
' button, and for the object exposing the method that
' shows the dialog box.
Dim dlgLogin
Dim TheForm
Set TheForm = ActiveX document.LoginButton
' Include a procedure that shows the dialog box
' when the button is clicked.
Sub cmdLogin_onClick
Set dlgLogin = Login
dlgLogin.ShowDialog
End Sub
</SCRIPT>

In this example, the component itself would include code that forms the login string and sends it to the server for validation.


Manually Deploying ActiveX Documents

The process for deploying ActiveX documents manually differs depending on whether you are deploying for Internet Explorer 3.x or 4.x.

Deploying ActiveX Documents in Internet Explorer 3.x

You perform these steps to deploy an ActiveX document manually in Internet Explorer 3.x:

  1. Create an HTML page that will be used to download the ActiveX document.

  2. In an existing HTML page from your application or Web site, create a link to the page you just created.

  3. In the HTML page used to download the ActiveX document, use the OBJECT tag to provide a means for the browser to download, register, and navigate to the ActiveX document.

    Note See "Manually Deploying ActiveX Controls" for an example of the object tag.

  4. In the download page, insert VBScript that instructs Internet Explorer to navigate immediately to the ActiveX document through its .vbd file. The following code shows an example:
    <HTML>
<OBJECT ID="SampleDoc1"
CLASSID="CLSID:11111111-1111-1111-1111-111111111111" CODEBASE="Project1.CAB#version=1,0,0,0">
</OBJECT>
<SCRIPT LANGUAGE="VBScript">
Sub Window_OnLoad
Location.Href = "SampleDoc1.VBD"
End Sub
</SCRIPT>
</HTML>

    In this code fragment, only the name of the .vbd file is given, rather than a fully qualified path. Internet Explorer looks for it in the same directory as the .htm file containing the VBScript.

Downloading ActiveX Documents in Internet Explorer 4.x

If you want to manually set up an ActiveX document for deployment on systems using Internet Explorer 4.0 or later, you can use a tool called the CodeBase Fixup Utility. CodeBase Fixup Utility is a tool that adds codebase information to a .vbd file or other ActiveX documents. The code information specifies the version number of the ActiveX document and the URL of the server used to download it. This approach presents several advantages:

  • You no longer need a separate HTML page to launch your download.

  • You can now mail the URL to the .vbd file and users can select it to view the ActiveX document in the browser.

  • The browser can automatically download the latest version of the ActiveX document if it is not installed on the user's machine.

The Package and Deployment Wizard automatically configures your ActiveX documents using the CodeBase Fixup Utility. If you are using the wizard to package your components for download, you do not need to perform this procedure.

For More Information For instructions on how to use the CodeBase Fixup Utility tool, see the Readme.txt file installed with the tool in the Tools directory, or on the Visual Basic owner's Web site at http://www.microsoft.com/vstudio.


Visual Basic Enterprise Development

Use the following links to get more information about enterprise development with Visual Basic and Visual Studio.

Visual Basic Enterprise Features

A complete list of enterprise features included with the Visual Basic Enterprise Edition, along with links to more information about each.

Developing for the Enterprise

Visual Studio’s guide to distributed application development for business-critical data management. This book includes information on designing solutions based on a flexible architecture, developing large-scale data access strategies, and creating integrated solutions with Visual Studio tools.


Data Access Guide

Welcome to the Visual Basic® Data Access Guide. This documentation discusses data access and connects to information on related tools and technologies.

This guide is divided into three parts. The first part covers tools and technology options. The second part presents tutorials that walk you through "scenarios" which employ the tools and technologies in real-world situations. The third part presents information on two older Visual Basic data access technologies, DAO and RDO.

Chapters

  • Accessing Data Using Visual Basic

    Introduces the different data access programming models available for accessing data, and discusses system and design issues that enter into a choice of a data access strategy. The emphasis is on ActiveX Data Objects (ADO), the new Microsoft technology.

  • Data Access Tools in Visual Basic

    Introduces mechanisms (such as the Query Designer and Database Designer) and technologies (such as Format Objects) that make data access programming easier.

  • About the Data Environment Designer

    This designer provides an extensive design-time interface that enables you to create and modify Data Environment objects.

  • Writing Reports with the Microsoft Data Report Designer

    This designer allows you to generate data reports from several different relational tables.

  • Tutorial: Interacting with Data in a Microsoft Jet/Microsoft Access Database

    Demonstrates how to use a data environment to create a Command object based on a Microsoft Access query, bind Visual Basic controls to fields in the query's recordset, create a master/detail form, and create a data-bound report.

  • Tutorial: Create a DHTML Application that Interacts with SQL Server Data

    Demonstrates how to create an application that adds a table to an SQL Server database; uses HTML pages to enter, view, and update data in the table; and uses an HTML page to view data returned by a stored procedure.

  • Tutorial: Interacting with Data in an ASCII Text File

    Demonstrates how to use a data-aware class and ADO to create a simple database application that interacts with data in a tab-delimited text file.

  • Converting from RDO 2.0 to ADO 2.0

    Discusses the changes needed to upgrade an existing RDO-based project to Microsoft's new data access technology.

  • Using Data Access Objects with Remote Databases

    Data Access Objects (DAO) is an older database technology that can be used either with the standalone Microsoft Jet database engine, or with direct access to remote data sources through the new ODBCDirect option. This section discusses DAO functionality in both modes, emphasizing the special design considerations of large scale client/server applications.

  • Using Remote Data Objects and the RemoteData Control

    Remote Data Objects (RDO) is an older database technology that provides a direct Visual Basic interface to remote ODBC data sources, and the RemoteData Control (RDC) makes this functionality available in a simple user interface control. This chapter provides a thorough discussion of the RDO/RDC features and functionality in client/server applications.


Accessing Data Using Visual Basic

The figure below is a roadmap of data access technologies found in Visual Basic. The figure features "hot" zones, which you can click to find out more information about any particular set of data, access tools or technologies.

vbconDataAwareControls.htm vbconDHTMLVisualBasicDataAccess.htm vbconUsingMicrosoftDataReportDesigner.htm vbconDataBindingInVisualBasic.htm vbconDataBindingInVisualBasic.htm vbconVisualBasicDataSources.htm vbconVisualBasicDataSources.htm vbconRemotingFeaturesOfVisualBasic.htm vbconRemotingFeaturesOfVisualBasic.htm vbconUsingADODAORDOInVisualBasic.htm vbconMiddleTierComponents.htm vbconCreatingOLEComponents.htm vbconTheDataViewWindow.htm

Microsoft Visual Data Tools

Using Visual Basic 6.0 you can create components that encapsulate every step in a data access system. Beginning with the data source, Microsoft Visual Data Tools (accessible through the Data View window) give you the ability to view and manipulate tables, views, stored procedures, and database schemas on SQL Server and Oracle systems.

Middle Tier Components and Microsoft Transaction Server

The power of Visual Basic is also leveraged to create the middle tier components in your application, as you make your own ActiveX DLLs and EXEs. Visual Basic now includes enhancements that tailor applications to work with Microsoft Transaction Server.

ActiveX Data Objects (ADO)

The bridge between the data providers and data consumers is through data sources created using Microsoft ActiveX Data Objects (ADO), which is the primary method in Visual Basic to access data in any data source, both relational and non-relational. For backward compatibility and project maintenance, Remote Data Objects (RDO) and Data Access Objects (DAO) are still supported.

Data Sources and Data Controls

On the client side, several new data sources are available, including the Data Environment, a graphical designer that allows you to quickly create ADO Connections and Commands to access your data. The Data Environment designer provides a dynamic programmatic interface to the data access objects in your project. In addition, the Data Environment provides advanced data shaping services — the ability to create hierarchies of related data, aggregates, and automatic groupings, all without code.

The new ADO Data control is similar to the intrinsic data control and Remote Data control, except that it uses ADO to access data. You can now use an ADO Recordset as a data source for your controls and objects in Visual Basic.

In Visual Basic you can now create your own data sources either as user controls or classes, to encapsulate business rules or proprietary data structures. The class module now features the DataSourceBehavior property and the GetDataMember event, which allow you to configure a class as a data source.

Dynamic Data Binding

The ability to dynamically bind a data source to a data consumer is now possible in Visual Basic. At run time, you can now set the DataSource property of a data consumer (such as the DataGrid control) to a data source (such as the ADO Data control). This capability, unavailable in previous versions of Visual Basic, allows you to create applications, which can access a multitude of data sources.

Presenting Data to the End User

Visual Basic offers a variety of rich ways to present data to your end users. ADO/OLE DB-based versions of all the data bound controls are included in Visual Basic:

  • The DataList and DataCombo controls are the ADO/OLE DB equivalents of DBList and DBCombo controls.

  • The DataGrid is the successor to DBGrid.

  • The Chart control is now data bound.

  • A new version of the FlexGrid control, called the Hierarchical FlexGrid, supports the hierarchical abilities of the Data Environment.

  • The new DataRepeater control functions as a scrolling container of data bound user controls where each control views a single record.

The Data Report is a new ActiveX designer that creates reports from any data source, including the Data Environment. With the Data Report designer, formatted reports can be viewed online, printed, or exported to text or HTML pages.

Data Formatting and Data Validation

The new DataFormat object allows you to display data with custom formatting, but write it back to the database in the native format. For example, you can now display dates in the format appropriate to a country, while the actual data is stored in a date format. Data is formatted coming out of the source, and unformatted going back in. You can also do custom formatting and perform additional checks using the Format and Unformat events.

Data validation is also enhanced using the CausesValidation property with the Validate event. By setting the CausesValidation property to True, the Validate event for the previous control in the tab order will occur. Thus, by programming the Validate event, you can prevent a control from losing focus until the information it contains has been validated.

Language Features

New data-related enhancements to the Visual Basic language include the ability to pass User-defined Types (UDTs) and arrays across processes. You can now define a UDT and pass it as a parameter to another process, such as an ActiveX EXE or DLL.

DHTML and Data Access

Using Visual Basic, you can create complete web applications for data access. All of the data tools and technologies can also be used in DHTML pages, and on web server (IIS) applications.

Other Enterprise features

For information on data access programming in the enterprise, see Data Access Strategies in Developing for the Enterprise.


Forms and Data-Aware Controls

To create interfaces for data entry, data editing, or data viewing, Visual Basic forms and controls have always offered versatility and ease of programming.

In version 6.0, several new data-aware (data-bound) controls have been added. In addition to these new controls, the new Validate event and CausesValidation property prevent a control from losing focus until all data has been validated. For example, you can create data-entry forms that validate data by preventing the user from tabbing off the form until all fields have been filled in.

Another enhancement always available to you is the Microsoft Data Formatting Object Library. This new object library allow you to format data and preserve the underlying format to write back to the database.

New Data-Aware Controls

Several new ActiveX controls, specifically designed to view and/or edit data, have been added. These include:

  • DataGrid Control — a new grid control that can work with the ADO Data Control or ADO Recordset objects.

  • DataList Control — a control that functions exactly like the DBList control, but is optimized for use with OLE DB data sources.

  • DataCombo Control — functions like the DBCombo control, but can use OLE DB data sources.

  • Hierarchical FlexGrid Control — capable of displaying hierarchical cursors created with the Data Environment.

  • DataRepeater Control — lets you use a user control to display data, and "repeats" the control to view multiple records.

  • MonthView Control — displays dates graphically as a calendar.

  • DateTimePicker Control — similarly to the MonthView control, it displays dates in a text box; clicking the text box causes a graphic calendar to drop down for selection of a new date.

Data Binding Capabilities Added to Existing Controls

Intrinsic controls and many ActiveX Controls have new data properties associated with them now, in addition to the DataSource and DataField properties that have always existed. These are the four properties of interest:

  • DataSource Property: Returns or sets the source of data for a control. You can now dynamically set the DataSource of a control or object at runtime to any valid data source, for example: an ADO Recordset, the DataEnvironment, a class that’s a data source, or to a data source control on the form.

  • DataMember Property: Returns or sets the specific data set within the source to use. Data sources in Visual Basic 6.0 can contain multiple sets of data, and the DataMember property lets you specify which set of data to use. (Not all data sources have multiple members, for example the ADO Data Control returns only a single recordset.) The Data Environment, on the other hand, returns a DataMember for each Command defined. Visual Basic displays a list of available data members when this property is selected.

  • DataField Property: Returns or sets the specific field to bind a control to. (Same functionality as in previous versions of Visual Basic.)

  • DataFormat Property: Lets you define the automatic formatting of data as its retrieved from the data source.

The first three properties narrow in scope, from the DataSource down to the DataField, qualifying the source of the data. However, for complex data consumers such as the DataGrid, the DataField and DataFormat properties aren’t required, as the control handles them for you. For more information about the difference between simple-bound and complex-bound controls, see Creating Data-Aware Classes.

The controls which can be data bound using some or all of these properties include:

  • CheckBox Control

  • ComboBox Control

  • DataCombo Control (new)

  • DataGrid Control (new)

  • DataList Control (new)

  • DataRepeater Control (new)

  • DateTimePicker Control (new)

  • DBCombo Control

  • DBList Control

  • FlexGrid Control

  • Hierarchical FlexGrid Control (new)

  • Image Control

  • ImageCombo Control (new)

  • Label Control

  • ListBox Control

  • Masked Edit Control

  • MonthView Control (new)

  • MSChart Control

  • PictureBox Control

  • RichTextBox Control

  • TextBox Control

Additionally, the new Extender object also includes the DataSource, DataMember, and DataField properties. The Extender object is used to program controls added dynamically to your forms. For more information on the Extender and adding controls at run time, see Add Method (Controls Collection) and Extender Object.

Validation Enhancements

Data validation is now easier using the Validate event with the CausesValidation property. Using the two new features in tandem, you can prevent a control from losing focus until the information it contains has been validated. For more information about these features, see Validating Control Data by Restricting Focus.


ADO, DAO and RDO in Visual Basic

In Visual Basic, three data access interfaces are available to you: ActiveX Data Objects (ADO), Remote Data Objects (RDO), and Data Access Objects (DAO). A data access interface is an object model that represents various facets of accessing data. Using Visual Basic, you can programmatically control the connection, statement builders, and returned data for use in any application.

Why are there three data access interfaces in Visual Basic? Data access technology is constantly evolving, and each of the three interfaces represent a different state of the art. The latest is ADO, which features a simpler — yet more flexible — object model than either RDO or DAO. For new projects, you should use ADO as your data access interface.

Why Use ADO?

ADO is designed as an easy-to-use application level interface to Microsoft's newest and most powerful data access paradigm, OLE DB. OLE DB provides high-performance access to any data source, including relational and non-relational databases, email and file systems, text and graphics, custom business objects, and more. ADO is implemented for minimal network traffic in key Internet scenarios, and a minimal number of layers between the front-end and data source — all to provide a lightweight, high-performance interface. ADO is called using a familiar metaphor — the OLE Automation interface. And ADO uses conventions and features similar to DAO and RDO, with simplified semantics that make it easy to learn.

For a brief overview, see OLE DB Providers.

For detailed information about ADO, see Getting Started with ADO 2.0.

DAO and RDO

For backward compatibility, Visual Basic continues to support DAO and RDO for existing projects.

For More Information For more information on RDO programming, see Using Remote Data Objects and the RemoteData Control. For information on DAO programming, see Using Data Access Objects with Remote Databases. Complete DAO reference can also be found at Microsoft DAO 3.51.

Upgrading from RDO to ADO

Consider upgrading if you decide ADO offers benefits your RDO-based application can use. See ADO Compared with RDO and DAO for a discussion of the differences among the platforms and for guidance on changing an RDO-based project to an ADO project. See Converting from RDO 2.0 to ADO 2.0 for upgrade guidance.


OLE DB Providers

OLE DB is a new low-level interface that introduces a "universal" data access paradigm. That is, OLE DB is not restricted to ISAM, Jet, or even relational data sources, but is capable of dealing with any type of data regardless of its format or storage method. In practice, this versatility means you can access data that resides in an Excel spreadsheet, text files, or even on a mail server such as Microsoft Exchange.

In Visual Basic 6.0, you leverage the flexibility of OLE DB through ADO, the programmer interface to OLE DB. You can even create your own OLE DB Providers in Visual Basic.

OLE DB and ADO

OLE DB is not designed to be accessed directly from Visual Basic due to its complex interfaces. Instead ActiveX Data Objects (ADO) encapsulates and exposes virtually all of OLE DB’s functionality.

OLE DB Providers

Visual Basic does allow you to create your own OLE DB Provider by setting the DataSourceBehavior property to vbOLEDBProvider in an ActiveX DLL project. (See Creating the MyOSPObject Class.) By doing so, the OnDataConnection event becomes available to provide notification a connection has been made. For more information, see DataSourceBehavior property.

For More Information For more background on OLE DB see Microsoft OLE DB Overview.


Visual Basic Data Sources

As the term implies, a data source is a readily accessible object that provides data to any data consumer (any class or control that can be bound to a source of external data). In past versions of Visual Basic, data sources included the intrinsic Data control and the RemoteData Control. Visual Basic 6.0 introduces several new data sources that enable you to create rich applications to view and edit data. For example, the Data Environment designer allows you to create hierarchical recordsets, or data from several related tables in a tree-view format. In addition to new data sources, you can create your own data source by setting the class module's DataSourceBehavior property to vbDataSource.

New Data Sources

Visual Basic's new richer set of data sources includes:

  • Data-Aware Class Modules

  • Data-Aware User Controls

  • Data Environment

  • ADO Recordset Objects

  • ADO Data Control

Creating Data Sources Using Data-Aware Classes and User Controls

Visual Basic enables you to create your own data sources. Using new features such as the DataBindingBehavior and DataSourceBehavior properties of the class module, you can encapsulate the methods, properties, and events necessary to create a data source or data consumer that accesses data from any kind of database. For details on creating data sources and data consumers using a data-aware class, see Creating Data-Aware Classes.

In addition to creating data-aware classes, you can also create your own data-aware user controls. Such a control could resemble the new ADO Data Control and be customized to your needs. For a step-by-step example of creating a data-aware user control, see Creating the MyDataControl Project.

The Data Environment Designer

The Data Environment is a new feature that allows you to create hierarchical cursors. A hierarchical cursor is a unique structure of parent and child recordsets. In general, a hierarchical cursor mirrors the structure of related tables in a relational database. For example, the Northwind database has a table named "Products" that contains a field named "SupplierID." That field contains unique IDs from the related table named "Suppliers." Using the Data Environment designer, you can create a hierarchical cursor that can be displayed in a control such as the Hierarchical FlexGrid control.

For details about hierarchical cursors, see Hierarchical Cursors and Data Shaping. For details about the DataEnvironment, see About the Data Environment Designer.

Creatable ADO Recordsets

Using the ActiveX Data Object Recordset (ADOR) library, you can create ADO recordsets in memory, as shown below:

Private rs As New ADODB.Recordset ' variable for recordset

Private Sub CreateRecordset()
With rs
.Fields.Append "ID", adInteger
.Fields.Append "Item", adBSTR, 255
.CursorType = adOpenStatic
.LockType = adLockOptimistic
.Open ' No connection object needed.
End With
Dim i As Integer

   For i = 1 To 100
      rs.AddNew
      rs!ID= i
      rs!Item = "thing " & i
      rs.Update
   Next i
   rs.MoveFirst
End Sub

Once such a recordset is filled with data, set the DataSource property of a data consumer to the recordset, as shown below:

' myControl is a data-bound user control and rsTempData is a temporary
' recordset created using ADODB, and filled from some data store.
Set myControl.DataSource = rs

For an example of creating ADO recordsets in code, see Creating a Data Source. Another example of creating a recordset and using it as a data source for the DataGrid control can be found in Using the DataGrid Control with a Class Module.


Visual Basic and Microsoft Transaction Server

The ability to build middle-tier components — ActiveX EXEs and DLLs — is not new to Visual Basic. What's new to Version 6.0 is the ability to tailor your components to run in conjunction with Microsoft Transaction Server (MTS).

Microsoft Transaction Server is a component-based transaction processing system for developing, deploying, and managing high performance, scalable, and robust server applications. MTS defines a programming model and provides a run-time environment and graphical administration tool for managing enterprise applications.

Using the new MTSTransactionMode property of the class module, you can create components that ignore MTS, or support transactions by setting a property. When the component is not running in an MTS environment, the property is ignored.

For More Information For more information about MTS, see Quick Tour of Microsoft Transaction Server. For information about the MTSTransactionMode property, see MTSTransactionMode Property. For information about using Visual Basic, MTS, and Microsoft Message Queue, see Visual Basic and the Microsoft Message Queue.


Data Binding In Visual Basic

In previous versions of Visual Basic, binding a data source (such as the Data control) to a data consumer (such as the DBCombo control or simply to a text box) could only be done at design-time. Visual Basic now allows you to bind almost any data source to any data consumer at run time. For example, at run time, you can now set the DataSource property as shown below:

Text1.DataMember = "Employees"
Text1.DataField = "Salary"
Set Text1.DataSource = DataEnvironment1

With this capability, you can also bind controls that have been added to the Controls collection using the new Add method. The code below adds a user control and binds it.

Private myCtl As Extender
Form1.Licenses.Add "Project1.userControl1", "uc1_Key"
Set myCtl = Form1.Controls.Add("Project1.userControl1", "Ctl_Key")
myCtl.DataMember = "Employees"
myCtl.DataField = "Salary"
Set myCtl.DataSource = DataEnvironment1

Exceptions

The only data sources that cannot be bound in this manner are the intrinsic Data control and the RemoteData Control.

The BindingCollection Object

In addition to this flexibility, you can also create your own data source using new data-aware features of the class module. Once you have created such a data source, you can also bind it at run time to a data consumer using the BindingCollection object. For details, see Creating Data-Aware Classes and Creating a Data Source.

Data-Aware User Controls

Visual Basic 6.0 also allows you to create your own user controls that function as data sources.

For More Information For more information about new data-bound controls, see Forms and Data-Aware Controls.


Remoting Features of Visual Basic

"Remoting" is the process of passing parameters between two different processes, usually across a network. For example, imagine a three-tier system. On the client machine, the application makes a call for data, passing several parameters as the criteria. On the middle-tier machine, an ActiveX EXE accepts the call and uses the criteria for retrieving the data.

For example, code on a middle-tier application might resemble:

Option Explicit

' This code is in a code module.
Public Type udtMyType ' Definition of a Public UDT
   birthDate As Date
   lastName As String
   firstName As String
   address As String
End Type

Public Function passUDT(myrec As udtMyType) As udtMyType
   ' Modify the data somehow.
   passUDT = myrec ' Return the UDT.
End Function

While code on the client machine that calls the function would be:

Option Explicit
Private myrec As udtMyType

Private Sub Command1_Click()
   Dim x As udtMyType
   x = passUDT(myrec)
   ' Do something with the UDT data.
End Sub

Passing a UDT as a Parameter of a Public Sub

While passing parameters has always been possible in previous versions of Visual Basic, passing user-defined types (UDTs) as parameters of public subs has not. This is now possible, as shown in the example above.

Performance Considerations

The cost of passing parameters out-of-process is far higher than passing them in-process. When passing a parameter, the data must be marshaled and passed to the external process. The code to accomplish this action can be expensive, but Visual Basic conceals this cost. The advantage of remoting data, however, is to create code that is easily comprehensible. Depending on the size of the UDT, it may also be easier to maintain than an ADO Recordset object.

For More Information For details about creating and using UDTs, see Creating Your Own Data Types.

Remoting ADO Recordsets

ADO Recordset objects can also be remoted. With this capability, ADO recordsets are especially suited for use on intranet and Internet client-server applications. For example, you can create an HTML or DHTML page that accesses data across the Internet from a web server application. When creating the HTML page, you can include the Microsoft ActiveX Data Access Recordset 2.0 Library, which features only the Recordset object. Since that library doesn't include the Command, Connection, and Parameter objects, your application will have the smallest possible footprint while retaining the functionality of the ADO Recordset features. The code below shows an example of remoting ADO recordsets.

' This code is in a code module.
' Set a reference to the Microsoft ActiveX Data Objects 2.0 Library
Private MyADORecordset As ADODB.Recordset

Public Function GetCustomer(LastName As String) As ADODB.Recordset
   ' Query the DB
   MyADORecordset.Open "SELECT * FROM Customers WHERE " & _
   "LastName = '" & LastName & "'", cn, adOpenForwardOnly, adLockReadOnly
   Set MyADORecordset.ActiveConnection = Nothing
   Set GetCustomer = MyADORecordset ' Return the recordset.
End Function

While code on the client machine that calls the function would be:

Option Explicit
Private SomeServer As Object

Private Sub Command1_Click()
   ' Client can use the lighter ADOR library. Set a reference to
   ' the Microsoft ActiveX Data Objects Recordset 2.0 Library.
   Dim MyData As ADOR.Recordset
   Set SomeServer = CreateObject("foo.bar", myserver)
   Set MyData = SomeServer.GetCustomer("Smith")
   ' Do something with the data.
End Sub


DHTML and Visual Basic Data Access

Using Visual Basic 6.0, you can now create data sources and data consumers, and all of the objects you create can be used as part of an intranet or Internet solution. For example, using the ADO Recordset object with a data-aware class, you can create a data control that can be distributed as part of a web page created with the DHMTL Page Designer or the WebClass Designer.

IIS Applications and DHTML Applications

Both client- (DHTML) and server-based Internet applications (IIS) can interact with all Visual Basic data features. On the server side, you can use a webclass to open database connections in response to an HTTP request, retrieve recordsets, and return them to the user. On the client side, you can create HTML elements on a page and bind them to data sources.

For details about using databases with IIS applications, see Using Databases with Webclasses. For information about creating a DHTML application that accesses data, see Creating a DHTML Application that Interacts with SQL Server Data.


Data Access Tools in Visual Basic

Visual Basic provides a number of tools to meet data access programming needs, such as controls and data-bound controls, designers, and technologies such as Format objects. This chapter introduces the tools listed below.

Topics

  • DataView Window and the Microsoft Visual Database tools: Database Designer and Query Designer.

  • Data Environment Designer

  • Data Report Designer

  • Format Objects

  • The SQL Editor

  • T-SQL Debugger

  • Wizards Related to Data Access

  • UserConnection Designer This ActiveX designer is used for RDO programming.

Other Data Access Subjects

For information on controls related to data access, see the Programmer's Guide:

  • Using the ADO Data Control

  • Using the DataCombo and DataList Controls

  • Using the DataGrid Control

  • Using the Microsoft FlexGrid and Hierarchical FlexGrid Controls

For information on data binding, see Building Data Sources.


The Data View Window and Visual Database Tools

The Data View window presents a view onto one or more database connections, providing access to the entire structure of the database on a particular connection. The Data View window provides the means to use the Microsoft Visual Database Tools (Query Designer and Database Designer) to visually manipulate that structure of a database.

To open the Data View window, click Data View on the View menu, or click the Data View button on the Standard toolbar.

Click the jumps below to see detailed discussions of the Data View window and its related tools.

  • Managing Databases in Data View

  • Introducing Microsoft Visual Database Tools

  • Database Designer

  • Query Designer


Format Objects

Format objects allow you to format and unformat data passed between a database and a bound object. You can also use the traditional Format function to format your data, but format objects offer these advantages:

  • The Format and Unformat events allow closer control over data formatting.

  • You can set formats either through code or using the IDE; in either case when you use format objects you'll write less code.

  • Format objects give you more formatting types.

Types of formatting available from format objects include the standard types supported by the Format function (currency, date/time, string) plus the types in the following table.

Formatting option | Description
Boolean | Specifies values displayed in the bound control when data read from the database is a Boolean true or false. Writes a Boolean true or false to the database when the data matches a specified value.
Binary | Allows any binary data to be read from and written to the database.
Object | Allows an object to be read from and written to the database.
Picture | Allows a picture to be read from and written to the database.
Checkbox | Values read from the database will determine the Value property for a checkbox. Checkbox Value property settings will determine true/false values written to the database.

Stepping Through the Process

You can set formatting options in code or using property pages available from the bound control's DataFormat property in the property window. When the bound control fetches a record from the database, formatting is applied according to properties of the StdDataFormat object, then the Format event is fired and data is displayed by the bound control.

When data is written back to the database, the formatting you applied goes with it. In most cases this works fine, but if you use the Format property to apply complex formatting you may create strings your database cannot unformat. In these cases you need to unformat the data in the Unformat event before it is written to the database.

Understanding the Object Hierarchy

Format objects include:

  • StdDataFormat object

    This object provides the Format and Unformat events, and it lets you select format types and apply format strings.

  • StdDataFormats collection

    Complex bound controls will usually need more than one StdDataFormat object. In this case the StdDataFormats collection gives you top-level access to the collection.

  • DataValue object

    You can use the DataValue object in the Format and Unformat events to customize formatting beyond what is provided by the StdDataFormat object.

  • DataFormat property

    The DataFormat property of the bound control sets or returns the StdDataFormat object.


Format Objects Tutorial

The following procedure gives you a brief overview of using format objects with bound controls attached to the ADO Data Control. The example uses the Nwind.mdb sample database provided with Visual Basic.

Note that although this example uses the ADO Data Control, format objects can be used with any data source.

Using the ADO Data Control with Format Objects

  1. Create an OLEDB Data Source for the Northwind database, named "Northwind".

    If a data source has not been created, follow the steps in Creating the Northwind OLEDB Data Source.

  2. From the Project menu, set references to the Microsoft Data Formatting Object Library and the Microsoft Data Binding Collection.

  3. On the Project menu click Components, and check the ADO Data Control.

  4. Add the ADO Data control and three TextBox controls to a form.

  5. Set property values as shown in the following table.
    Object | Property | Setting
    ADO control | ConnectionString | northwind.mdl
    ADO control | RecordSource | select * from Employees
    Text1 control | DataSource | ADODC1
    Text1 control | DataField | LastName
    Text2 control | DataSource | ADODC1
    Text2 control | DataField | HireDate
    Text3 control | DataSource | ADODC1
    Text3 control | DataField | Extension

  6. Add the following declarations to the form.
    ' Binds textboxes to the ADO control.
Dim bc As New BindingCollection

' We'll add code to the Format event on this object.
Dim WithEvents f1 as StdDataFormat
Dim WithEvents f2 As StdDataFormat

    Declaring f2 with the WithEvents statement exposes the object's events. Step 8 shows code using the Format event to work on data as it is passed back and forth between the database and bound object.

  7. Add the following code to the form's Load event.
    ' Connect the BindingCollection object to the datasource.
Set bc.DataSource = ADODC1

'set a long date format string
Set f1 = New StdDataFormat
f1.Type = fmtCustom
f1.Format = "long date"
' Use the BindingCollection object to bind the 2nd text box.
bc.Add Text2, "text", "hiredate", f1

'set a currency format string
Set f2 = New StdDataFormat
f2.Type = fmtCustom
f2.Format = "$0.00"
' Use the BindingCollection object to bind the 3rd text box.
Set bc.DataSource = ADODC1
bc.Add Text3, "text", "extension", f2
  8. Add the following code to the f2 object's Format event.
    If DataValue.Value < 3000 Then
Text3.ForeColor = vbRed
Else
Text3.ForeColor = vbBlack
End If
  9. Run the project and experiment with the ADO Data control.


The SQL Editor

The SQL Editor allows you to create and edit stored procedures and triggers in both SQL Server and Oracle from within the Visual Basic development environment. The topics listed below give you an overview of using the SQL Editor.

Stored Procedures in the SQL Editor

Triggers in the SQL Editor


Stored Procedures in the SQL Editor

Stored procedures enable you to manage your server-based database and display information about that database and its users. For example, you can use a stored procedure to display the title (from the titles table) and publisher (from the publishers table) for each author in the authors table.

Stored procedures can contain program flow, logic, and queries against the database. They can accept parameters, generate parameters, return single or multiple result sets, and return values.

You can use stored procedures for any purpose for which you would use SQL statements, with these advantages:

  • You can execute a series of SQL statements in a single stored procedure.

  • You can reference other stored procedures from within your stored procedure, which can simplify a series of complex statements.

  • The stored procedure is compiled on the server when it is created, so it executes faster than individual SQL statements.

  • The compiled stored procedure can be cached in memory as well, for faster execution.

Creating New Stored Procedures

The SQL editor can be opened from the Data View window. The Data View window can be opened from the View menu or the Standard toolbar.

To create a new stored procedure

  1. In Data View, right-click the Stored Procedures folder or any stored procedure in that folder.

  2. Choose New Stored Procedure from the shortcut menu.

    A new stored procedure is created using a template containing SQL statements.

  3. Replace StoredProcedure in the first line with the name of the procedure. For example, you might use "MyProcedure" as the name:
    Create Procedure MyProcedure

    Note Stored procedures must have unique names. If you choose a name that is already assigned to another stored procedure in your project, an error message is displayed.

  4. Write the remaining procedure text in SQL.

For more information and examples of stored procedures, see the documentation for your server. If you are using Microsoft® SQL Server™, see CREATE PROCEDURE statement in the SQL Server documentation. The Visual Basic SQL Editor also works with Oracle Stored Procedures, Functions, and Packages.

Running Stored Procedures

You can run a stored procedure against your database to execute the SQL statements it contains and display the results in the Immediate window.

To run a stored procedure

  1. In Data View, expand the Stored Procedures folder.

  2. Right-click the name of the stored procedure that you want to run. Choose Run from the shortcut menu.

  3. If any parameters are required, a dialog appears to enter the parameter values.

Copying Stored Procedures

You can copy a stored procedure as the first step in creating a new stored procedure for your database. Because stored procedures must have names that are unique, the new stored procedure is automatically assigned a new unique name based on the original name.

To copy a stored procedure

  1. In Data View, expand the Stored Procedures folder.

  2. Right-click the name of the stored procedure that you want to copy. Choose Design from the shortcut menu.

    The SQL Editor opens with the stored procedure.

  3. Select all the text in the SQL Editor.

  4. Right-click the SQL Editor. Choose Copy from the shortcut menu.

  5. Right-click the SQL Editor. Choose New Stored Procedure Wizard from the shortcut menu.

  6. Clear the template text.

  7. Right-click the SQL Editor. Choose Paste from the shortcut menu to create your new stored procedure.

  8. Change the name of the stored procedure.

  9. Save the stored procedure to the database.

You can edit the SQL statements in the new stored procedure.

Setting Execution Permissions on Stored Procedures

You can set execute permissions for a stored procedure that you own to allow access to the stored procedure by specific users or groups. In many databases, such as Microsoft® SQL Server™ and Oracle Database Server, if you are not the database owner, then you must explicitly grant permissions for your stored procedure to other users. Use SQL Server’s ISQL utility, Enterprise Manager, or an Oracle Tool to run the change commands or visually set the permissions.


Triggers in the SQL Editor

A trigger is a special kind of stored procedure that goes into effect when you modify data in a specified table using one or more data modification operations: UPDATE, INSERT, or DELETE. Triggers can query other tables and can include complex SQL statements. They are primarily useful for enforcing complex business rules or requirements. For example, you could control whether to allow an order to be inserted based on a customer's current account status.

Triggers are also useful for enforcing referential integrity, which preserves the defined relationships between tables when you add, update, or delete the rows in those tables. However, the best way to enforce referential integrity is to define primary key and foreign key constraints in the related tables. If you use database diagrams, you can create a relationship between tables to automatically create a foreign key constraint.

Advantages of Using Triggers

Triggers are useful in these ways:

  • Triggers are automatic: they are activated immediately after any modification to the table's data, such as a manual entry or an application action.

  • Triggers can cascade changes through related tables in the database. For example, you can write a delete trigger on the title_id column of the titles table to cause a deletion of matching rows in other tables. The trigger uses the title_id column as a unique key to locate matching rows in the titleauthor, sales, and roysched tables.

  • Triggers can enforce restrictions that are more complex than those defined with check constraints. Unlike check constraints, triggers can reference columns in other tables. For example, a trigger can roll back updates that attempt to apply a discount (stored in the discounts table) to books (stored in the titles table) with a price of less than $10.

Creating a Trigger

A trigger is a database object that you create by specifying:

  • The current table.

  • The data modification transactions that activate the trigger: adding new data (INSERT), updating existing data (UPDATE), or deleting existing data (DELETE).

  • The actions that the trigger will take immediately following the transactions you specified.

You write triggers in Transact-SQL for Microsoft® SQL Server™ databases or PL/SQL for Oracle databases.

To create a trigger

  1. In Data View (available from the Standard toolbar or the View menu), expand the Tables folder.

  2. Right-click the name of the table that you want to create a trigger on. Choose New Trigger from the shortcut menu.

    A new trigger is created with the following SQL statements already defined for you:

    Create Trigger /*Trigger_Name*/
on /*Table_name*/
For /*Insert, Update, Delete*/
As
  print 'Trigger Fired'
  3. Modify the trigger text as follows:
    Line | Replace | With
    1 | /*Trigger_Name*/ | The name you want to assign to the trigger
    2 | /*Table_name*/ | The name of the table you want to attach the trigger to
    3 | /*Insert, Update, Delete*/ | The type of transactions that will activate this trigger

    For example, to create a trigger named employee_insupd for insert and update transactions on the employee table, you would change the first three lines of the trigger text to the following:

    Create Trigger employee_insupd
on employee
For Insert, Update
  4. Write the remaining trigger text in SQL.

For examples of triggers for Microsoft SQL Server databases, see Creating Triggers in the SQL Server documentation. For details about the Transact-SQL syntax of triggers, see CREATE TRIGGER Statement in the SQL Server documentation.

Opening a Trigger

You can open a trigger to view or edit the text of an existing trigger that is stored in your database. Triggers are scripted in Transact-SQL for Microsoft® SQL Server™ databases or PL/SQL for Oracle databases.

To open a trigger

  1. In Data View, expand the Tables folder.

  2. Expand the table whose trigger you want to open.

  3. Right-click the name of the trigger that you want to open and click Design on the shortcut menu.

    -or-

    Double-click the name of the trigger that you want to open.

Saving a Trigger

To add a new trigger to the database or to update an existing trigger that you have modified, you can save a trigger.

To save a trigger

  1. In the SQL Editor, choose Save to Database from the File menu.

  2. If you are updating an existing trigger, a message box prompts you to confirm the save action. Choose Yes.

A saved trigger appears in the Tables folder in Data View under the table that it's attached to.

Deleting a Trigger

To disable the actions defined in the trigger that are automatically carried out on your database immediately following the specified transactions, you can delete a trigger.

You might also want to delete any triggers that enforce referential integrity between related tables if you use database diagrams to design your database. Database diagrams use relationships instead of triggers for this purpose. Thus, if a trigger duplicates a relationship in a database diagram, you should delete either the trigger or the relationship.

To delete a trigger

  1. In Data View, expand the Tables folder.

  2. Expand the table whose trigger you want to delete.

  3. Right-click the trigger that you want to delete and choose Delete from the shortcut menu.

  4. A message prompts you to confirm the deletion. Choose Yes.

    The trigger is deleted from the database and Data View.


The T-SQL Debugger

The T-SQL debugger is integrated with the Data Environment designer. It allows you to interactively debug remote stored procedures written in Microsoft SQL Server's Transact SQL dialect, from within the Visual Basic development environment. Using the T-SQL debugger, you can:

  • Display the SQL call stack, local variables, and parameters for the SQL stored procedure.

  • Control and manage breakpoints.

  • View and modify local variables and parameters.

  • View global variables.

Setup and Compatibility

In order to use the T-SQL debugger, you must have SQL Server version 6.5 with Service Pack 3 or later installed as your database server. The debugger uses the functionality exposed by SQL Server's Sdi.dll, and exposes that functionality through Remote Automation.

The client-side components of the T-SQL debugger are correctly installed and configured when you choose to install all the Enterprise tools in your Visual Basic installation. If it is necessary to repeat the setup process, select "Custom" from the CD Installation dialog box, and choose "Select All" for the Enterprise Tools selection.

Server-Side Setup

With SQL Server version 6.5 and Service Pack 3 or later installed, you can install and register the SQL Debugger interface and Remote Automation component on the server. These components are located at \Program files\Common Files\Microsoft Shared\SQL Debugging. On Windows NT 4.0, simply run the setup program Sdi_nt4.exe.

Note For setup on NT Server 3.51, you must manually copy and register the necessary files. Complete instructions for this process are included in the Readme.txt file in the \Program Files\Common Files\Microsoft Shared\SQL Debugging folder.

Using the T-SQL Debugger

There are different methods you can use to invoke T-SQL debugging.

  1. To debug a stored procedure or batch query at design time, add the T-SQL Debugger Add-In via Visual Basic's Add-In Manager (on the Add-Ins menu). Then you can start the add-in by clicking T-SQL Debugger on the Add-Ins menu. You then simply select a DSN, and either Stored Procedure or Batch SQL and click the Execute button. This will invoke the debugger and allow you to debug the SQL you are interested in.

  2. To debug stored procedures while debugging Visual Basic code (run-time debugging), select T-SQL Debugging Options on Visual Basic's Tools menu. The options dialog box allows you to:
    • Turn on automatic step into stored procedures, which will bring up the T-SQL Debugger whenever you step into an ADO or RDO method that executes a stored procedure.

    • Turn Safe Mode on, which will automatically roll back any design-time queries that you debug.

    • Limit the number of rows that appear in the T-SQL Debugger output window when debugging design time queries.

    • Set the login timeout value that the debugger uses to connect to the database, to get internal SQL State.

    Once you have selected the Automatically step into Stored Procedures check box, if you step into (F8) a line of code that executes an ADO or RDO method that invokes a stored procedure, the debugger will automatically be started. You can then step through the stored procedure and then continue debugging your Visual Basic code.

    Note SQL Server will return from a stored procedure before it has finished executing if the stored procedure returns enough data to fill its buffers. If this happens, both the T-SQL Debugger and the Visual Basic debugger will be active at the same time. Your Visual Basic code must fetch the results from ADO or RDO before the stored procedure will complete its execution. If this happens, make sure your basic code reads the result sets by placing Visual Basic in Run Mode (F5) and setting breakpoints where you would like to stop execution. You can toggle back and forth between Visual Basic and the T-SQL Debugger by using the taskbar or using the ALT+TAB key combination.

  3. You can also launch the T-SQL Debugger:
    • From the Data Environment designer

    • While stepping through ADO or RDO code

    • By right-clicking a stored procedure in the Data View window and choosing the Debug command

    • From the UserConnection designer

Once you have started the debugger, it establishes the ODBC connection and displays the Enter Unassigned Parameters dialog box, as shown.

Unassigned Parameters Dialog box

Enter values for any unassigned parameters in the Value field, then click OK. The T-SQL debugger interface appears and displays the text of the stored procedure:

T-SQL Debugger Interface

Debugging Options

With the SQL statement displayed, several debugging options are available on the toolbar buttons and on the Debug menu. These options include:

  • Go

  • Set and clear breakpoints

  • Step

  • Step into subexpression

  • Step over subexpression

  • Run to cursor

  • Stop debugging

  • Restart

Views and Options

In addition to the code window containing the SQL statement you are debugging, the T-SQL debugger interface presents separate output windows for local and global variables, and for the output (result set) of the query. The View menu also allows you to open a separate Call Stack window and a Temp Table Dump window, so that you can examine these as the code executes.

The Options menu lets you customize the appearance of the T-SQL debugger by changing the fonts and colors used for display.

Exiting from the T-SQL Debugger

When you are finished with your debugging session, click Exit on the File menu to close the debugger. To execute a query again, click Restart on the Debug menu.

Troubleshooting

If you are having problems getting T-SQL debugging to work, you will need to check the event log on the server. SDI.DLL will log events in the application section of the event viewer. COM or distributed COM errors will log events in the system section of the viewer.

  • Make sure that the two computers can communicate with each other. The easiest mechanism to do this is by typing ping and the computer name of the client at a command prompt on the server if you are running TCP/IP. If this fails, fix the connectivity problem between the machines.

  • Make sure the file SDI.DLL resides in the same directory as SQLSERVR.EXE. This will be in the binn sub-directory under the main SQL Server directory. The default is c:\mssql\binn.

  • Ensure that the RPC services are started on the server machine. You do this by starting the control panel, opening the services application and checking that the Remote Procedure Call ( RPC ) Service is running and set to start automatically, as well as the Remote Procedure Call ( RPC ) Locator.

  • Ensure that SQL Server is not set to log on as the SystemAccount. You do this by starting the control panel, opening the services application and double clicking on the MSSQLServer service. If the service is set to run as the SystemAccount, change this so the server will log on to a specific account that is valid to the domain that you are in. If debugging still fails, make sure that the account SQL Server started as has sufficient rights to launch an automation server on the client machine.

  • If you see COM error 80080005 in the event log, make sure that you did not start remote automation (autmgr32) from the command prompt. Autmgr32.exe should only be running in the winstation of the account that SQL Server logged in as. Any other winstation will cause problems. If this is the case, close down autmgr32.exe via the task manager and let the sdi.dll and autprx32.dll load autmgr32 via COM.

  • Make sure Remote Automation is successfully installed on the server and client machines, if both the client and server do not have Distributed COM (DCOM) installed and loaded.

  • If your client system is running Windows NT 4.0 or later, run DCOMCNFG and make sure that everyone has launch and access permission for vbsdicli.exe.


The UserConnection Designer

The UserConnection designer uses Visual Basic's ActiveX designer architecture to provide design-time support for programmatic data access. It allows you to create connection and query objects (based on the RDO rdoConnection and rdoQuery objects) at design time. These connections and queries are persisted as project-level objects. You can pre-set properties, define new properties and methods, and write code behind the objects to catch events.

Note The Data Environment designer is an improved version of the UserConnection designer. The UserConnection Designer is for RDO programming only.

This provides a simplified method for responding to events raised from connections and queries as well as for calling stored procedures and client-defined queries at run-time.

Here are the basic steps to using the UserConnection designer:

  1. Enable the designer and insert it into your project.

  2. Use the UserConnection object's property page to establish connection information and specify settings for properties that can be set at design time.

  3. Create new Query objects for the connection by selecting stored procedures from the target database or inserting SQL code for user-defined queries.

  4. Configure each Query object by setting properties on its property page.

  5. Write Visual Basic code that creates an instance of the new UserConnection class and calls any of its queries as methods.


Inserting a New UserConnection Object

To insert a new connection object

  1. Make sure the Remote Data Object 2.0 library is referenced in the References dialog box available from the Project menu.

  2. Make sure the designer is enabled by checking Microsoft UserConnection on the Designers tab of the Components dialog box, available on the Projects menu.

  3. Insert the UserConnection designer into the project by selecting Microsoft UserConnection from the Add ActiveX Designer item on the Project menu.

The Connection Designer Properties Window

When you insert the UserConnection designer, a new connection class is added to the current project and brings up a visual representation of the class, overlaid with its properties window, shown below.

UserConnection Properties Connection tab

The UserConnection properties window has three tabs: Connection, Authentication, and Miscellaneous.

Connection Properties

The Connection properties are used to establish connections to the server during design time (for retrieving a list of available stored procedures) and is also persisted in the new UserConnection object so that you don't have to specify connection information when the class is created at run time. This connection information is just the data source name (or, if you select Use DSN-less Connection String, the driver and server name) along with any special connection tokens specified in the "Other ODBC Attributes" box.

The Use ODBC Datasource box displays all DSNs on the current computer. You can also define a new DSN by clicking on the New button.

Authentication Properties

The authentication tab contains information needed for connecting to the data source specified in the connection information tab. These properties of the connection may or may not be persisted with the new connection object, depending on the setting of the check boxes at the bottom of the page. See "Persistence of Authentication" below for more details.

Authentication tab

The user name and password text boxes are self-explanatory. The password box displays asterisks as the password is entered.

The Prompt Behavior box allows you to choose the prompt behavior for the rdoConnection object.

Persistence of Authentication

Due to concerns about security and password protection, there are two levels of persistence available for the UserConnection object's authentication properties. By default both of these levels are turned off, so that no caching or persistence of authentication properties occurs.

If Save Connection Information for new Run-Mode Class is checked, the user name and password properties are stored in the properties of the actual class and are persisted in the built executable or DLL.

If Save Connection Information for Design Time is checked, the user name and password properties persist only during design-time, and are not written into the built .exe or .dll file.

Miscellaneous Properties

The last tab on the connection property page allows the developer to adjust the Timeout properties and cursor library for the new UserConnection class.

Miscellaneous Properties

The Logon Timeout and Query Timeout values can be set to any values allowed in RDO 2.0.

The Cursor Driver combo box contains options that map to the RDO constants for the "CursorDriver" property.

The UserConnection designer form

After entering the properties to establish an ODBC connection for your UserConnection object, click OK to close the properties window. The new object appears in the main UserConnection form.

The UserConnection Form

The toolbar buttons at the top of the form allow you to add single or multiple queries, delete queries, examine or change query properties, and change global options for the UserConnection designer.


Inserting a New Query Object

Once a new UserConnection object has been inserted into the project, you can add new Query objects to it. These query objects can be either stored procedure calls or user-defined SQL statements. Each query object defined at design-time will automatically be available as a method of the new UserConnection class.

To add a Query object, click the leftmost button on the toolbar. The new Query object is added hierarchically below the UserConnection object, and its Properties Window appears.

Selecting Stored Procedures

Select Insert Stored Procedure to view the available stored procedures on the database. The UserConnection designer attempts to establish a connection to the database, using the connection properties you entered. If successful, it enumerates the available stored procedures on the database, allowing you to select one to define the Query object:

Query Properties dialog box

Inserting User-Defined Queries

Choose Based on User-Defined SQL to create a local query against the remote database. This enables the SQL property box, into which you enter SQL statements to define your query.

Alternatively, you can click the Build button, which opens the MS-Query query design tool. MS-Query provides a visual interface that allows you to drag and drop database tables and fields, define conditions and set relationships to define the query. When you exit from MS-Query, your visually-designed query is automatically translated into SQL code and inserted into the User-Defined SQL property box.

For More Information For more information about using the MS-Query query builder, see MSQry.hlp which is installed in the \Program Files\Common Files\Microsoft Shared\vba directory.

Setting Other Query Properties

In addition to the defining properties discussed above, the Query Property Page has two additional tabs for defining Parameters and advanced properties.

The Parameters tab on the query property page allows the developer to adjust properties for each parameter in the current query.

Parameters properties

The parameters shown in the Parameters list box are determined automatically from the query source, and cannot be changed here. Parameters are generated for each parameter marker in the SQL statement for the query (or call statement for stored procedures). Parameter markers are designated by a "?" in accordance with the ODBC specification. For example, the following query would contain two parameters:

SELECT * FROM authors WHERE state = ? AND zip = ?

The parameter properties that can be changed here are the Name, Direction, ODBC Binding Data Type, and Visual Basic Data Type.

The Name property can be changed to allow your Visual Basic code to recognize the parameter by a familiar sounding name, if its actual name is complex or not intuitive.

Setting the Direction of a parameter may be necessary for some ODBC databases for which the driver is not capable of determining the direction (input, input/output, or return values). The same is true for the data types, and you may wish to override the default type conversion into Visual Basic data types for use with your code.

Advanced Query Properties

The Advanced tab of the Query Properties page allows for fine-tuning the query by setting limits and thresholds.

Advanced properties

The Call Syntax text box lets you change the call syntax that RDO will use to call the stored procedure. This can be edited to adjust the number of parameters and presence of the return value. Editing the call syntax is an advanced operation and should only be done by developers who understand the ramifications.

Using the UserConnection Object in Your Code

You can insert the UserConnection object into your Visual Basic code just as you would with any class, by creating an object variable and instantiating it with a new instance of the UserConnection class you have defined. For example:

Option Explicit
Private MyConnect As MyConnection
Set MyConnect = New MyConnection

You can write your own code "behind" the inserted UserConnection object to sink events raised from the connection or any of the queries defined under it. You can also implement your own methods, or write Property Get and Property Let procedures to implement your own properties, which are added to the type library for the new object.


Wizards Related to Data Access

Three Visual Basic wizards can assist you in your data access programming:

  • Data Form Wizard

  • Data Object Wizard

  • MS FlexGrid Wizard


About the Data Environment Designer

The Data Environment designer provides an interactive, design time environment for creating programmatic run-time data access. At design time, you set property values for Connection and Command objects, write code to respond to ActiveX® Data Object (ADO) events, execute commands, and create aggregates and hierarchies. You can also drag Data Environment objects onto forms or reports to create data-bound controls.

With the Data Environment designer, you can accomplish the following tasks:

  • Add a Data Environment designer to a Visual Basic project.

  • Create Connection objects.

  • Create Command objects based on stored procedures, tables, views, synonyms, and SQL statements.

  • Create hierarchies of commands based on a grouping of Command objects, or by relating one or more Command objects together.

  • Write and run code for Connection and Recordset objects.

  • Drag fields within a Command object from the Data Environment designer onto a Visual Basic form or the Data Report designer.

Differences Between the UserConnection and Data Environment Designers

The Data Environment designer provides a means to easily access data in your Visual Basic project. In previous releases, you used the ActiveX UserConnection designer to create Remote Data Objects (RDO) at design time. Now, you can create ADO objects at design time using the Data Environment designer. In addition to supporting all of the functionality of the UserConnection designer, the Data Environment designer supports:

  • Multiple Connection objects that allow you to access multiple data sources within a single Data Environment.

  • Connection and Command objects that you can organize by either connection or object.

  • OLE DB data sources, as well as ODBC data sources.

  • Drag-and-drop functionality that allows you to drag fields and tables from your Data Environment designer onto a form or the Data Report ActiveX designer. Data-bound controls are automatically created on the form. You can also specify the default control type that is created.

  • Execution of commands included in your Data Environment as programmatic run-time methods.

  • Programmatic access to a Data Environment that is bound to controls on a form without a variable reference, such as moving to a new record.

  • The ability to relate Command objects to create a relation hierarchy, or to group Command objects to create a grouped hierarchy.

  • The ability to create aggregates that automatically calculate values within any Command hierarchy.

  • The ability to manually bind data-aware controls to Field objects within a Command object.

  • The Data Environment extensibility object model, which allows you to create add-ins. These add-ins can programmatically manipulate any DataEnvironment object within a Visual Basic project.

Functionality Comparison: UserConnection designer vs. Data Environment designer

Functionality | UserConnection designer | Data Environment designer
Data Access exposed using… | Remote Data Objects (RDO). | ActiveX Data Objects (ADO).
Objects exposed include... | only one RDO connection with multiple queries. | multiple ADO Connection and Command objects within one DataEnvironment object.
Events exposed include... | only events from the UserConnection object. | all ADO events for the Connection and Command object.
Direct data binding... | cannot be used as a direct data source. | can be directly bound to controls on a form.
Programmatic access exposes… | queries as methods from the UserConnection object with one ResultSet property. | Command objects as methods from the DataEnvironment object, with one RecordSet property per Command object.
Design environment provides… | a basic view that only displays the list of queries in the UserConnection. | two views of objects that list Connection and Command objects, as well as the Field objects returned from each Command object.

Topics

  • Designing a DataEnvironment Object

  • Connection Objects

  • Command Objects

  • Field Mapping

  • Manipulating Your Data Environment

  • Attaching Code to ADO Events

  • Using a Data Environment with Your Application

  • The Data Environment Extensibility Object Model

  • Scenarios For Using the Data Environment Designer


Designing a DataEnvironment Object

At design time, you can use the Data Environment designer to create a DataEnvironment object. The DataEnvironment object can include Connection and Command objects, hierarchies (relationships between Command objects), groupings, and aggregates. Before designing your DataEnvironment object, you should determine what information you want to present, identify the databases that contain the information, and determine your run-time objective (for example, creating a Data Report or Hierarchical FlexGrid control).

Before you can access the Data Environment designer, you must reference it in Visual Basic.

To reference the Data Environment designer

  1. On the Project menu, click References.

  2. From the References dialog box, select Data Environment 1.0, and then click OK.

To add a Data Environment designer object to a new Visual Basic project

  1. From the New tab of the New Project dialog box, choose Standard EXE project, and then click Open.

  2. From the Project menu, choose Add Data Environment.

    The Data Environment designer is added to your Visual Basic project, the Data Environment designer window appears, and a Connection object is added to your Data Environment.

Once you have added a Data Environment designer to your project, you can create a Connection; see the procedure in Connection Objects. Once a Connection is created, you can add Commands to it; see the procedure in Command Objects.


Connection Objects

To access data using your Data Environment, you must create a Connection object. Therefore, every Data Environment should include at least one Connection object. A Connection object represents a connection to a remote database that is used as a data source.

Upon adding a Data Environment to your Visual Basic project, the Data Environment designer automatically includes a new connection, called Connection1. At design time, the Data Environment opens the connection and obtains metadata from the connection, including database object names, table structures, and procedure parameters.

Note If Show properties immediately after object creation is selected in the Options dialog box, the Data Link Properties dialog box will appear when you add a Data Environment to your project. This option is not selected by default.

Creating a Connection Object

The Add Connection function is available at all times and is independent of the existence of other objects.

To create a database connection

  • Click Add Connection on the Data Environment designer toolbar.

    -or–

    Right-click your Data Environment designer and select Add Connection from the shortcut menu.

    Once you have added a Connection, the Data Environment is updated to show the new Connection object. The default name for this object is "Connection," followed by a number, such as Connection1.

    Use the following procedure to specify Connection object properties.

To set the Connection Name and Data Source

  1. In the Visual Basic Properties window, change the default Name to a more meaningful name for your data source database. For example, you may wish to change Connection1 to "Northwind" if the data source is the Northwind database.

  2. Right-click the Connection object and choose Properties to access the Data Link Properties dialog box.

  3. From the Data Link Properties dialog box, specify the connection information on the Provider and Connection tabs. This is typically a database that contains data or stored procedures. You may select only one source for each Connection object.

    Note Regardless of the selected data source type, the Data Environment accesses all data via ADO and OLE DB interfaces.

  4. Click OK to apply the properties and close the dialog box.

Setting Logon Information

Logon information only needs to be supplied if the database being accessed via the Connection object requires authentication information. You can specify a different set of logon information to be used at design time and run time. For example, you might want to develop your application using a system administrator user identification and password, but supply a general guest user identification when the application is run.

Note Any logon information specified on the Data Link Properties dialog box is overridden by logon information specified on the Visual Basic Properties window.

To specify logon information

  1. On the Visual Basic Properties window, specify the user identification and password to be used at design time and run time, if necessary. You can also specify the prompt behavior.

  2. Set DesignSaveAuthentication to True if you want the specified information to persist for design time. This information is not written into the built executable file or dynamic-link library (DLL). If this option is False, any information in DesignUserName and DesignPassword is lost once you close and re-open the project.

  3. Set RunSaveAuthentication to True if you want the specified authentication used at run time. The authentication information is stored in the properties of the class and persists in the built executable file or DLL. If this option is False, any information in RunUserName and RunPassword is lost once you close and re-open the project.

    Note Since the password is not encrypted, for maximum security you should not specify your password to persist for run time or design time.

Setting Miscellaneous Connection Information

The miscellaneous connection information consists of advanced options that change how the database is accessed.

To specify miscellaneous information

  1. On the Visual Basic Properties window, specify the following:
    • The CursorLocation to be used for the connection. For information on cursors, see Choosing and Managing Cursors.

    • Additional parameters for the connection string in ConnectionSource. Any string placed here is appended to the ConnectionString property when connecting to the data source. For example, Database=pubs.
  2. Right-click the Connection object and choose Properties to access the Data Link Properties dialog box.

  3. On the Advanced and All tabs, you can:
    • Specify the number of seconds for the connection and command timeouts.

    • Indicate permissions and network settings.
  4. Click OK to apply the properties and close the Data Link Properties dialog box.

    Note A Connection object is not opened until information is needed from the connection, such as a list of tables. When a Connection object is selected in the Data Environment outline view, the status bar text can be used to determine if the connection is currently established (open).

Dragging From a Data View to Your Data Environment

You can automatically create Connection objects by dragging a connection from the Data View window to your Data Environment designer. This is an easy and efficient way to create Connection objects that already exist in your Data View.


Command Objects

Command objects define specific detailed information about what data is retrieved from a database connection. Command objects can be based on either a database object (such as a table, view, stored procedure or synonym) or a Structured Query Language (SQL) query. You can also create relationships between Command objects to retrieve a set of related data in the form of a hierarchy (see Command Hierarchies).

Note To be valid, a Command object must be associated with a Connection object.

If a Command object returns data, it is "recordset returning," and the results can be accessed using a Recordset object available from the DataEnvironment object. However, if a Command object does not return data (for example, stored procedures or SQL text that performs an update), it is "non-recordset returning." The Data Environment Designer automatically identifies whether the Command is recordset returning. You can override this setting by using the Recordset Returning check box on the Advanced tab of the Command Properties dialog box.

At run time, how you access the Command object depends on whether the Command object is recordset returning. If the Command object is recordset returning, you can access the Command object as either a property or method from the DataEnvironment object. If it is non-recordset returning, your Command object is only accessible as a method. See Using a Data Environment with Your Application for more information.

Creating a Command Object

The Add Command function is available at all times and is independent of the existence of other objects. However, a Command object that is not associated with a Connection object is invalid.

If a Connection object can be identified from the current focus during the add process, the ActiveConnection property of the Command object is set to that Connection object. If a Connection object is not identified, the Command object is invalid until you associate it with a connection.

To add a Command object

  • Click Add Command in the Data Environment designer toolbar.

    -or–

    Right-click a Connection object, or your Data Environment designer, and choose Add Command from the shortcut menu.

    Once a Command object is added, the Data Environment's outline view shows the new Command object. The default name for this object is "Command," followed by a number, such as Command1.

    Use the following procedure to specify Command object properties.

To specify Command object properties

  1. Right-click the Connection object and choose Properties to access the Command Properties dialog box.

  2. Click the General tab, and set the following:
    Item | Purpose
    Command Name | Change the default Command Name to a more meaningful name for your database object. For example, you may wish to change Command1 to "Customers" if the Command object is based on a table called "customers."
    Connection If the Command object was created from a Connection object's shortcut menu, the Connection name is automatically set. However, you can change this connection.

    Note To be valid, each Command object must be associated with a Connection object.
    Database Object | Select the type of database object from the drop-down list. This can be a stored procedure, synonym, table, or view.
    Object Name | Select an object name from the drop-down list. The listed objects are from the connection and match the selected Database Object type.
    –or–
    SQL Statement If this is selected as your data source, type an SQL query that is valid for your database in the SQL Statement box.

    -or–

    To build the query, click SQL Builder to launch the query designer.

  3. If the Command object is based on a parameterized query or a stored procedure, it may have a parameters collection. To set the parameter properties, click the Parameters tab in the Command Properties dialog box.

  4. Use the Relation, Grouping, and Aggregates tabs to define relationships and shape the data included in the Recordset. For more information, see Command Hierarchies.

  5. Click the Advanced tab in the Command Properties dialog box to set the properties that change how the data is retrieved or manipulated at run time. On this tab, set the advanced properties that provide your Data Environment control over the Command object properties and its resulting Recordset object.

  6. Click OK to apply the properties to the new Command object and close the dialog box.

    If a recordset-returning Command object was successfully created and the Show Fields option is selected in the Options dialog box, you can click the expand (+) bitmap from the Data Environment designer's outline view to see a list of fields. If no fields are shown, the cause could be an empty Recordset, an invalid Command object, or an invalid connection.

Customizing the Parameter Objects of a Command Object

If a Command object is based on a parameterized query or a stored procedure with parameters, the Command object has a Parameters collection. You may want to customize the Parameter objects contained in the collection by changing the data type or making the name more descriptive. For more information, see ADO Parameters.

Changing the Properties of Associated Parameter Objects

The following procedure describes how you can change the properties of Parameter objects that are associated with a Command object.

To change a Command object's associated Parameter object properties

  1. Right-click the Command object that you wish to customize, and then select Properties from the shortcut menu.

  2. From the Parameters tab, select a Parameter object from the Parameter list box, and then set the following properties:
    Item | Purpose
    Name | Provide a unique, meaningful name for the selected Parameter object.
    Direction | Specify whether this is an input or output parameter, or both, or if the parameter is the return value from the procedure.
    DataType | Specify the data type to which the Parameter object is converted.
    Precision | Specify the maximum size, in bytes.
    Scale | Specify the maximum number of digits to the right of the decimal point.
    Size | Specify the maximum size, in bytes.
    Host Data Type | Specify the data type used when this Parameter object is referenced by the host application. Changing this setting affects the data types used in building the type library information for the host.
    Required Specify whether the parameter value is required when the Command object is executed.

    Note If a required parameter is not set when the Command object is executed, the command will fail.
    Value | Specify the default value that is used at run time (unless a value is provided programmatically), and if necessary, at design time, if the Command object must be executed to obtain the field information.

  3. Click OK to apply the parameter properties to the selected Command object and exit the dialog box.

For more information, see ADO Parameters.

Dragging From a Data View to Your Data Environment

You can automatically create Command objects by dragging from the Data View window to your Data Environment designer. This is an easy and efficient way to create Command objects from tables, views, or stored procedures that are listed in your Data View. If the connection associated with the Command object being dropped doesn't already exist in the Data Environment, a Connection object is automatically created.

Creating Multiple Command Objects from Stored Procedures

You can create multiple Command objects in your Data Environment designer from stored procedures using the Insert Stored Procedures dialog box.

To insert multiple stored procedures

  1. Click Insert Stored Procedures in the Data Environment designer toolbar.

    -or–

    Right-click a DataEnvironment or Connection object and choose Insert Stored Procedures from the shortcut menu.

  2. In the Insert Stored Procedures dialog box, move one or more stored procedures from the Available list to the Add list using the arrows.

    Use > to move the stored procedures to the Add list one at a time, or use >> to move all stored procedures at once. Use < to remove the stored procedures from the Add list one at a time, or use << to remove all stored procedures at once.

  3. Once the stored procedures are in the Add list, click Insert to add them to your Data Environment. A new Command object is created for each stored procedure.

    Note The name of the Command object defaults to the name of the stored procedure.

  4. Click Close to exit the dialog box.


Command Hierarchies

A Command hierarchy is a group of recordset-returning Command objects that define an ADO hierarchical Recordset. Once a Command hierarchy is established, the Data Environment designer builds an ADO SHAPE Command, which is executed at run time and results in an ADO hierarchical Recordset. For more information, see Data Shaping.

The Data Environment designer provides an easy-to-use interface to create ADO Recordset hierarchies. You can build relation hierarchies, grouping hierarchies, and aggregate calculations. Once a hierarchy is built, at run time, you can navigate through multiple Recordsets as a single unit or bind each Recordset object to controls on a form.

You can use the Microsoft Hierarchical FlexGrid control to display the data from all the recordsets in the hierarchy at one time in a banded format. You can also bind the hierarchical recordset to a set of controls on a form, as described in Binding Data to Your Data Environment.


Relation Hierarchies

You can use the Data Environment designer to relate two or more Command objects together based on command data. The concept is similar to joining two related tables using a SQL SELECT statement. However, the run-time result is a set of hierarchical recordsets instead of a flat table. This type of hierarchy is called a relation hierarchy.

A relation hierarchy consists of a parent Command object and one or more child Command objects that are related through linking the parent's Field objects to the child's fields and/or parameters. In a relation hierarchy, the child Command objects become fields in the parent Command object.

For example, you might want to create a relation hierarchy between a Customers and Orders table. By relating the Orders table to the Customers table based on the CustID field, the Orders recordset becomes a field in the Customers recordset. Thus, the value of this field in each row becomes a reference to a recordset that contains all the Orders for that particular Customer. This is illustrated by the following figure.

Note All Command objects used in the relation hierarchy must be associated with the same Connection object. You cannot relate Commands from two different databases.

For more information on relation hierarchies, see Data Shaping.

Creating a Relation Hierarchy from Two Existing Command Objects

Once there are two Command objects at the same level in the Data Environment outline view, you can create a relation hierarchy, as described in the following procedure.

Note The Command objects involved in a relation hierarchy should be recordset returning. If not, on the Advanced tab of the Command Properties dialog box, choose Recordset Returning, and click OK to apply the changes to the Command object.

To create a relation hierarchy from two existing Command objects

  1. Right-click the Command object that will be the child in the relation, and click Properties on the shortcut menu to open the Command Properties dialog box.

  2. Select the Relation tab and then choose Relate to a Parent Command Object.

  3. In the Parent Command box, select the parent Command object's name. All Command objects that are associated with the same connection are shown, except for any Command object that is a child of the current Command object.

  4. Define the relation between the two Command objects as follows:
    Item | Description
    Parent Fields | Select a field from the parent Command object. When added, this shows on the left side of the relate expression.
    Child Fields/Parameters Select a field or parameter from the child Command object. When added, this shows on the right side of the key expression.

    Note You must link all required parameters to fields in the parent Command object. If the required parameters are not linked, the hierarchy cannot be successfully executed.
    Add After you have selected from both the Parent Fields and the Child Fields/Parameters lists, click Add. The new relation pair shows in the Relation Definition relate expression (for example, CustomerID TO CustomerID). Repeat this process until you have all relations defined, as necessary.

    Note You should relate the two Command objects on fields that contain similar data. For example, if one Command object returns data from an Orders table and another returns data from a Customers table, you should use a field that exists in both tables, such as orderID.
    Remove | To remove a relation pair, select the pair in the display area and click Remove.

  5. Click OK to accept the relation definitions and close the dialog box.

    Note While you can relate any two Command objects, the Data Environment designer does not check for valid input. Therefore, if you specify an invalid relationship, the data retrieved may not be what you expect.

Once a relation is in place, you can use the Microsoft Hierarchical FlexGrid control to display the data as a hierarchy.

Creating a Child Command Object from a Parent Command Object

An easy way to directly construct a relation hierarchy is to create a child Command object from the parent Command object. To create a child Command object directly, first create the parent Command object, as described in Command Objects, and then perform the following procedure.

Note The Command objects involved in a relation hierarchy should be recordset returning. If not, on the Advanced tab of the Command Properties dialog box, choose Recordset Returning, and click OK to apply the changes to the Command object.

To create a child Command object from a parent Command object

  1. On the Data Environment toolbar, click Add Child Command.

    -or–

    Right-click the parent Command object and click Add Child Command on the shortcut menu to open the Command Properties dialog box.

    Note The child Command and parent Command objects' connection are the same.

  2. The default Command Name (shown on the General tab of the Command Properties dialog box) of the child Command is the name of the Field object that will be appended to the parent Command object. You can change this name to a more meaningful, unique name. For example, you may wish to name the child "Orders" if it is based on a database object called "orders."

    Note On the General tab of the Command Properties dialog box, Connection is disabled. This is because the child and parent Command objects must be associated with the same Connection object.

  3. Select the Relation tab, and define the relation by specifying the fields or parameters in each Command object that contain common data.

  4. Click OK to create the child Command object and close the dialog box. If successfully created, the child Command object appears below its parent in the Data Environment outline view.


Grouping Command Objects

You can create a Command hierarchy by grouping a Command object. When a Command object is grouped, the grouped fields are added to a grouping Command object that becomes the parent of the original Command object. In the resulting recordsets, the grouping Command object contains a row for each unique set of values of the grouped fields.

For example, if the Command object that you want to group is called Customers, you can specify to group by its "Country" Field object and name the grouping Command object "ByCountry." The resulting group hierarchy consists of a Command object named ByCountry that contains two fields: Country and Customers. This is illustrated by the following figures.

Original Customers Table

Customers Table Grouped ByCountry

A grouped-based hierarchy displays in the Data Environment outline view as a single Command object with two folders of fields, one for the grouping Command and the other for the original, or detail, Command object.

Note The name of all grouping Command objects must be unique among all other Command objects within the current Data Environment. For example, if you are creating a group based on a Field object named country, a logical group name may be "ByCountry."

To group a Command object

  1. Right-click the Command object and click Properties on the shortcut menu to access the Command Properties dialog box.

  2. Click the Grouping tab, and then select Group Command Object. This indicates that this Command object is part of a group and enables other fields on the tab.

  3. Change the default Grouping Command Name for the grouped Command object to a more logical name, if preferred. For example, if you are grouping the Field objects in a Customers table by country, you might call the group "ByCountry."

    Note The default name is CommandName_Grouping where CommandName is the name of the selected Command object.

  4. Select the grouping fields from the list appearing in the Fields in Command box. Choose either one field at a time by selecting the field and click > or click >> to select all fields simultaneously.

    -or–

    Alternatively, to deselect fields, select the field to remove and click <, or click << to deselect all fields simultaneously.

    Note If no fields are available, the arrow buttons are disabled. In addition, if you use the Properties dialog box of a child Command object to establish a group, the fields that define the parent-child relationship must be included as fields for the group. The Data Environment designer prohibits these fields from being removed.

  5. Click OK to save the grouping and close the dialog box.


Aggregates

An aggregate is a special type of Field object that you can use to have data automatically calculated based on a hierarchy of Command objects. You can define an aggregate on any relation or grouped-based hierarchy. Each aggregate you define adds a new Field object to the current Command object. At run time, you can access the calculated data just like any other field.

In addition, you can create a Grand Total Aggregate, which can be used on any top-level Command object to calculate its values. When a Grand Total Aggregate is created, a new Command object is created as the parent of the Command object on which the calculated values are based. Thus, the existing Command object becomes a child Command object and is referenced through a field in the parent Command object.

For example, if an aggregate is based on the CustID field, you can obtain the total number of orders for each customer. This is illustrated by the following figure.

The aggregate can be based any of the operations in the following table.

Operation | Description
Any | Returns one of the values from the rows of the selected field.
Average | Returns the average of the values of the selected field.
Count | Returns the count of the number of records of the selected field.
Maximum | Returns the highest value of the selected field.
Minimum | Returns the smallest value of the selected field.
Standard Deviation | Returns the standard deviation of the selected field.
Sum | Returns the sum of all the values of the selected field.

To create an aggregate within a Command hierarchy

  1. Right-click any Command object and select Properties from the shortcut menu. (To create an aggregate other than a Grand Total, the Command object must have a child or be grouped.) The Command Properties dialog box appears.

  2. Click the Aggregates tab. The Aggregates frame lists all currently defined aggregates.

  3. Click Add to add an aggregate to the Aggregates box.

    Note To delete an aggregate from the Aggregates box, select the aggregate to delete, and then click Remove.

  4. Specify the Aggregate Settings information, as follows:
    Item | Description
    Name The name of the Field object that will be added to the Command object. At run time, this field will contain the calculated aggregate value.

    You can change this to a more meaningful, unique name that describes the aggregate. For example, if you are creating an aggregate that provides the average number of items on each order, an appropriate name may be AverageOrderNumber.
    Function | Select the operation to associate with the aggregate: Any, Average, Count, Maximum, Minimum, Standard Deviation, or Sum, as described above.
    Aggregate On Select a grouping, a child Command object, or Grand Total from the drop-down list. The selected item is what the aggregate is based upon.

    Note Grand Total is only available on top-level Command objects. Choosing Grand Total implies that an additional Command object will be created to provide a grand total of your data.
    Field | Choose a Field object on which to base the aggregate. For example, if the aggregate is to provide the average number of orders, specify the field that contains the quantity of orders.
    Name If you have selected Grand Total in the Aggregate On box, specify a name for the Grand Total Command. If Grand Total is not selected, this option is disabled. Once created, the Grand Total Name displays as the name of a new top-level Command object within the Data Environment.

    Note The Grand Total Name can be the same as the Aggregate Name.

  5. Click OK to save the aggregate definition and close the dialog box. The aggregate appears as a Field object of the selected Command object in the Data Environment outline view.


Accessing Hierarchy Information and the SHAPE Command

For run time, the Data Environment constructs an ADO SHAPE Command that is used by ADO to create an ADO hierarchical Recordset. You can display this underlying ADO hierarchical Recordset and ADO SHAPE Syntax. The hierarchy information shows the structure of the ADO Recordsets that are created at run time. This information is important if you wish to understand how the run-time hierarchy of recordsets are structured. You need to know this when programmatically accessing the ADO hierarchical Recordset. In addition, you can copy the SHAPE Command and paste it into code when you are programmatically creating hierarchies using ADO directly.

For more information, see ADO SHAPE Commands.

To view the underlying hierarchy information and SHAPE Command of a hierarchical Command object

  1. Right-click the parent Command object and click Hierarchy Info on the shortcut menu. The Hierarchy Information dialog box appears.

    Note If Hierarchy Info is unavailable, the Command object is either not a parent Command object or it is not using the SHAPE Command.

  2. Select View Shape Command to display the underlying SHAPE Command of the current Command object.


Field Mapping

One of the features of the Data Environment is that it allows you to drag fields from the Data Environment onto a form or report. When you are designing a Data Environment, you can specify what control is created when the field is dropped based on the data type of the field. For example, you can have a Spin Edit Control created for all integer types. You can also specify a specific control on a per-field basis.

There are three levels of controls that you can specify. When a field is dropped onto a form, the Data Environment looks for a control in the order described in the following table. The first level that has a control associated with it is used.

Control Level | Description
Field Object | Within a DataEnvironment object, a control can be directly associated with a Field object in a Command object. The control is specified as a property of the Field object and is only applicable to that specific field. If no control is specified for the dropped Field object, the Data Environment moves to the next level: ADO Data Type.
ADO Data Type | A control can be associated with a specific ADO data type. For example, a control can be associated with the ADO data type adTinyInt. Thus, whenever a field of this type is dropped on the form (and it does not have a control associated with the Field object), the control associated with the ADO data type is created. Note These settings will affect all DataEnvironment objects on your system. If no control is associated with the specific ADO data type, the Data Environment moves to the next level: Field Type Categories.
Field Type Categories | Since there are a large number of ADO data types that store similar data, a set of Field Type categories are defined. For example, all integer-related data types (such as adBigInt, adTinyInt, and so forth) are combined into a Field Type category of Integer. If no control is associated with the specific ADO data type, the control associated with the Field Type that contains the ADO data type is used. If there is no control associated with the Field Type category, the system defaults to an intrinsic TextBox control.

There are two special Field Type categories: Caption and Multiple. The control associated with Caption is the control used when a label object is created next to a new control. The control associated with Multiple appears when you complete a right-drag of a Command object, and is typically used to specify a grid control.

The following procedures describe how to set field mapping information for your drag and drop controls.

To change the Caption control for a single Field object

  1. Right-click a Field object and click Properties on the shortcut menu.

  2. In the Field Mapping frame, set the Control and the Caption.

  3. Click OK to set the field mapping properties for the current Field object and close the dialog box.

To change the Caption control for more than one Field object at a time

  1. Select a Command object and click Options on the Data Environment designer toolbar.

    -or-

    Right-click your DataEnvironment object and click Options on the shortcut menu.

  2. From the Field Mapping tab of the Options dialog box, select Show All Data Types.

  3. Select each Field object from the Category/Field Type and Control list, and change its control in the Control box.

  4. Click OK to set the Caption control properties and close the dialog box.

To specify an ADO Data Type or Category control

  1. Select a Command object and click Options on the Data Environment designer toolbar.

    -or-

    Right-click your DataEnvironment object and click Options on the shortcut menu.

  2. On the Field Mapping tab of the Options dialog box, select Show all data types, and then specify the control:
    Control | Description
    ADO Data Type | Select a Data Type (for example, adBinary) from the Default Control Association list, and then associate it with a Control list item.
    Category | Select a Category (for example, Binary) from the Default Control Association list, and then associate it with a Control list item.

  3. Click OK to set the control properties and close the dialog box.

To prevent the creation of labels for Field objects

  1. Select a Command object and click Options on the Data Environment designer toolbar.

    -or-

    Right-click your DataEnvironment object and choose Options from the shortcut menu.

  2. On the Field Mapping tab of the Options dialog box, deselect Drag and Drop Field Captions. The Field object captions (labels) will not be created on a drag and drop function.

  3. Click OK to set the control properties and close the dialog box.


Manipulating Your Data Environment

This section contains information on viewing objects within your Data Environment designer, modifying and deleting objects, refreshing Command and Connection objects, and setting Data Environment designer options.

Data Environment Designer Views

The Data Environment designer window has an outline view that shows the defined Connection and Command objects. This outline view can be arranged either by object type or by connection. You can also set this outline view to either display or hide the field objects of each Command object.

View By Connection

By Connection displays the objects grouped by connection, with the Command objects listed under the Connection object to which they are linked. Thus, you can easily view the association of Command objects with Connection objects. All defined Command hierarchies retain their structure.

To view By Connection

  • Click the Arrange by Connections button in the Data Environment designer toolbar.

Outline View By Connection

View By Object

By Object groups objects of the same type together. This enables you to focus on objects of a specific type, regardless of their associated connection.

To view By Object

  • Click the Arrange by Objects button in the Data Environment designer toolbar.

Outline View By Object

Hiding Field Objects in Your Data Environment Designer

By default, the Field objects of each Command object display in the outline view of the Data Environment designer. You can expand or collapse the view of these Field objects by clicking the plus (+) or minus (-) bitmap associated with the Command object. However, you can hide all Field objects completely from the outline view to clearly view relationships between Command objects.

To hide the Field objects for all Command objects

  1. Right-click on the DataEnvironment object. The DataEnvironment object is the top-most object in your Data Environment outline view.

  2. Clear the selection of Show Fields from the shortcut menu.

Modifying an Object in Your Data Environment Designer

You can modify any existing Command or Connection object by accessing either its Properties dialog box or the Visual Basic Properties window. Common modifications include changing a Command object's connection source, adjusting a Connection object's logon information, and renaming an object.

Note While using the Visual Basic Properties window provides a direct means of changing individual property values, it can be more difficult if you are unfamiliar with the properties associated with the object.

To modify an existing Command or Connection object using its Properties dialog box

  1. Right-click the Command or Connection object to modify, and then select Properties from the shortcut menu to access its Properties dialog box.

    -or-

    Double-click your Command or Connection object to open its Properties dialog box.

  2. To change any of the Command object's properties, from the Command Properties dialog box, click the appropriate tab: General, Parameters, Relation, Grouping, Aggregates, or Advanced, and then make the appropriate changes. For information on each tab's settings, click the tab, and then press F1 to access online Help for the Command object's properties.

    Note When the source of a Command object is changed, the Data Environment updates its associated field and parameter information.

  3. To change any of the Connection object's properties, from the Data Link Properties dialog box, click the appropriate tab: Provider, Connection, Advanced, or All, and then make the appropriate changes. For information on each tab's settings, press F1 or Help to access the online Help for the Connection object's properties.

  4. Click OK to apply the properties to the object.

To modify an existing Command or Connection object using the Visual Basic Properties window

  1. Select the Command or Connection object from the drop-down list in the Visual Basic Properties window.

  2. Choose either the Alphabetic or the Categorized tab, and then change the property information in the right column as necessary.

Renaming an Object in Your Data Environment Designer

Upon creation, objects in your Data Environment designer are given default names, such as Command1, Connection1, or DataEnvironment1. However, you can change these to more meaningful, unique names. For example, if you are creating a connection based on the Northwind database, a logical name may be "Northwind." Likewise, if you are creating a command based on the customer's table, a logical name may be "Customers."

Note When a Connection object is renamed, any Command objects associated with it automatically update in the ActiveConnectionName property. In addition, when a Command object is renamed, its link to the Connection object and other Command objects is maintained.

To rename an object in your Data Environment designer

  • Right-click the object that you wish to rename and choose Properties from the shortcut menu. The Properties dialog box appears. Type a new name in the Name box and click OK to apply the changes and exit the dialog box.

    -or-

    Right-click the Command or Connection object you wish to rename and choose Rename from the shortcut menu. The outline view is now in edit mode. Type the new name, and then press enter to accept the change.

    -or-

    On the Visual Basic Properties window, select the entry to the right of (Name), and type the new name.

Deleting an Object from Your Data Environment

You can remove DataEnvironment, Connection, or Command objects from your Data Environment designer. When deleting an object, a warning message appears, unless Prompt before deleting objects is cleared in the Options dialog box. In addition, if the object selected for deletion has dependent objects, a message appears, asking if you also want to delete the dependent objects. For example, a Connection object has dependent objects if it is associated with a Command object, or a Command object has dependent objects if it is involved in a relation or grouping hierarchy.

Caution Once a Connection object is deleted, any Command objects associated with it become invalid, and the ActiveConnectionName property for all its associated Command objects is cleared.

To delete a Command or Connection object from your Data Environment designer

  1. Select the object that you wish to remove and press delete.

    -or-

    Right-click the Command or Connection object that you wish to remove and choose Delete from the shortcut menu.

  2. If Prompt before deleting objects is selected in the Options dialog box, warning messages display to alert you to any affects of the delete.

Refreshing an Object in Your Data Environment Designer

The Refresh function differs in function between Connection and Command objects.

Refreshing Connection Objects

When a refresh is performed on a Connection object, the connection is closed and the internal cache is cleared of the metadata associated with the Connection object. In addition, refreshing a Connection forces the Data Environment to open the connection. After the refresh, the next time a list of tables or stored procedures is accessed, the list is rebuilt.

Note Refreshing a Connection object does not automatically refresh its associated Command objects.

Refreshing Command Objects

When a refresh is performed on a Command object, all metadata stored with the object, including the Field and Parameter objects, is rebuilt. If the Command object's definition of the table or stored procedure has changed, the changes are reflected in the Command object after the refresh.

Caution Refreshing a Command object removes user-defined data, including parameter and field mapping information. Once removed, it cannot be retrieved.

To refresh a Connection or Command object

  • Select the Command or Connection object to refresh, and then click Refresh in the Data Environment designer toolbar.

    -or-

    Right-click the Command or Connection object to refresh, and choose Refresh from the shortcut menu.

Connection and Command objects are also refreshed whenever their respective source properties are modified.

Note The status bar text indicates whether a Connection object is open (connected).

Changing Warning and Confirmation Dialog Box Display

You can change the display of warning and confirmation dialog boxes using the Options dialog box.

To change warning and confirmation dialog box display

  1. Click Options in the Data Environment designer toolbar.

    -or-

    Right-click a DataEnvironment object and choose Options from the shortcut menu.

  2. From the General tab of the Options dialog box, set the following:
    Item | Description
    Prompt before deleting object When selected, a confirmation dialog box displays when you delete an object.

    Note It is recommended that this option remain selected, so that the system presents an alert when deletion of an object affects other objects within your Data Environment.
    Disable Warnings | When not selected, warning messages appear when appropriate.
    Prompt before executing command When selected, the system prompts you before executing a Command object.

    Occasionally it is not possible for the Data Environment to obtain field information about a Command object from the data provider using its normal processes. When the Data Environment is unsuccessful in obtaining metadata about the Recordset of a particular Command object, it attempts to obtain this information from the data provider by executing the Command object. Thus, your permission is requested before the Data Environment executes the user-defined code.

    Note This situation generally only occurs for Command objects based on stored procedures or SQL text.

  3. Click OK to apply the options to your Data Environment designer and close the Options dialog box.


Attaching Code to ADO Events

ADO exposes a set of events from its Connection and Recordset objects. These events are fired so you can attach code to be executed when something occurs on the object. For example, a MoveComplete event is fired on the Recordset object whenever a record is navigated.

To add code to an ADO Connection or Recordset event

  1. Click View Code in the Data Environment designer toolbar.

    -or-

    Right-click a Connection or Command object and click View Code on the shortcut menu.

  2. Select an object in the left drop-down list and then select an event in the right drop-down list.

  3. Add code in the Code window, as appropriate.


Using a Data Environment with Your Application

At run-time, the Data Environment creates ADO Command and Connection objects for each Command and Connection object defined in the Data Environment designer. If the Command object is marked as Recordset Returning (on the Advanced tab of the Command Properties dialog box), then an ADO Recordset object is also created. The ADO Command object is added as a method from the Data Environment run-time object, and the ADO Connection and Recordset objects are added as properties.

There are two ways to use a Data Environment in your application at run time:

  • As a direct data source for data binding to controls on a form.

  • To programmatically create an instance of the Data Environment and execute its Command objects.

In addition, the run-time Data Environment contains Commands, Connections, and Recordsets collections. These 1-based collections offer another means of programmatically accessing the ADO objects, allowing you to enumerate the various objects.

Note The index for the first member of the collection starts at the number one in 1-based collections. ADO objects are 0-based, thus the Fields and Parameters collections are 0-based, and thus, start at zero.

In the Data Environment, the names of ADO Recordset objects are prefaced with "rs" to distinguish them from their corresponding Command objects. For example, a Command object named Customers creates a Recordset object named rsCustomers. By default, Recordset objects are closed. When the Recordset object's corresponding Command method executes, the Recordset object opens. For example, executing the Customers method opens the Recordset object of "rsCustomers." In addition, you can open a Recordset object directly using the ADO Open method. Using this method, you can manipulate a Recordset object before it is opened.


Binding Data to Your Data Environment

Data-bound controls are the data-aware controls through which you can display information from a database. You can bind these data-aware controls to Command objects that have been defined in a DataEnvironment object. At run-time, Visual Basic automatically opens these Command objects and their associated connections.

At run time, up to three data-aware properties define what the controls are bound to. The following table describes the controls you should use to set the data-aware properties of a Data Environment's Recordset object or Recordset field.

Property | Description
DataSource | Specifies the name of the DataEnvironment object to be used as the data source, such as DataEnvironment1.
DataMember | Specifies the name of the Command object in the DataSource, such as Customers.
DataField | Specifies the name of the Field object from the DataMember, such as Country.

To bind a Visual Basic control to a DataEnvironment object

  1. Drop a control, such as a TextBox, onto your Visual Basic form.

  2. On the Properties window, click the DataSource property and select the DataEnvironment object that you wish to bind to your control.

  3. Click the DataMember property and select the Command object within the Data Environment that contains the field that you wish to bind to your control.

    Note When binding to a Command object that contains parameters, make sure the parameters contain a valid value in their Value property.

  4. Click the DataField property, if applicable, and select the field from the Command object that you wish to bind to your control.


Dragging Objects to Create Data-Bound Controls

The process of creating and binding a control can be simplified by dragging a Command or Field object from the Data Environment designer and dropping it onto a Visual Basic form or report.

When this is done, Visual Basic creates a control with the DataSource, DataMember, and, if appropriate, DataField properties automatically set. The control created is based on the field mapping information specified in the DataEnvironment object. In addition, if specified, a label is automatically created.

Dragging Objects from Your Data Environment Designer to a Form

The section below explains how the Data Environment determines what control should be created.

To drag Field or Command objects onto a Data Report, see Creating a Simple Data Report.

To drag a Field or Command object onto a form

  1. Select a Field or Command object from the Data Environment designer. To select all Field objects within a Command object, select the Command object.

  2. Drag the selected Field or Command object from the Data Environment designer to the form.

    Each item dragged to the form creates a data-bound control that is based on the control associated with its source object. The dragged object's property setting links to the source object in the Data Environment.

To create a grid from a Command object

  1. Select a Command object and right-drag it onto a Visual Basic form.

  2. Select an option from the menu: Data Grid, multiple-bound controls, or any control associated with the multiple setting.


Navigating Recordsets

Once you have bound controls on a Visual Basic form to a DataEnvironment object, you can add navigation buttons that allow the user to move through data at run time.

To add navigation buttons to your Visual Basic form

  1. Drop two Command buttons on your form. You will use these to navigate through your Data Environment's Recordset object.

  2. Right-click your Visual Basic form and select View Code from the shortcut menu.

  3. For the Previous button, add the following code to the Click event.
    DECustomers.rsCustomers.MovePrevious
  4. For the Next button, add the following code to the Click event.
    DECustomers.rsCustomers.MoveNext
  5. Click Run on the Visual Basic toolbar to run your form, and click the Previous and Next buttons to navigate between records.

    Note You can use any ADO method in this procedure, such as MoveFirst or MoveNext. To add update and delete capabilities to your form, use the Update and Delete methods.


Programmatically Accessing Objects in Your Data Environment Designer

Once Connection or Command objects are created, you can programmatically access and manipulate these ADO Command, Connection, and Recordset objects as though you had created them directly through ADO. This makes it possible for you to programmatically bind data-aware controls to a Command object or to fields within a Command object at run time. You can also dynamically set properties on ADO Connection or Recordset objects while they are closed, and set parameter values before data binding.

In addition, when you programmatically access data exposed by the Data Environment, you have greater control over execution options and can create multiple instances of a DataEnvironment object.

When a Command object is executed using a method or data binding, the following occurs:

  • ADO objects are created for the associated Connection and Command objects, and the resulting Recordset. Also, the design-time properties from the Data Environment's objects transfer to the ADO objects.

  • The ADO Connection opens.

  • The ADO Command opens.

You can also programmatically create instances of your Data Environment by declaring a variable within a procedure using the DIM statement. For example:

Dim DE as New MyDE

Programmatically Manipulating Objects within Your Data Environment Designer

Each Command object that you create is exposed programmatically as a method from the DataEnvironment object. Therefore, to execute the Command object, you can execute the method from the Data Environment.

In addition to exposing a method for each Command object, a Recordset object is also exposed, depending on the Recordset Returning property setting. Since it is not possible to surface both a method and a property of the same name, the Recordsets are exposed as the name of the Command preceded by "rs."

Upon creation, the Recordset object is closed. When you execute the method off the Data Environment, the Recordset opens. In addition, you can manipulate the Recordset before and after it is opened. For example, once the Recordset is opened, you can navigate to the next record using the MoveNext method.

All ADO Recordset methods and properties are available from the Recordset that is associated with the DataEnvironment object.

To programmatically access your Data Environment from a Visual Basic form

  1. Drop a Command button on your form. The code attached to this button is used to step through each record within the Command object in your DataEnvironment object.

  2. In the Click event of the button, add the following line of code in the Code window.
    Dim DE as New MyDE
MyDE.Customers

    Note This example assumes that there are no parameters associated with the Command object.

  3. The records are available from the Recordset associated with the Data Environment. This has the same name as the Command object, preceded by "rs". Add the following code to move to the first record in the Customers Command object.
    DE.rsCustomers.MoveFirst

    At this point, all methods associated with an ADO Recordset are available for DE.rsCustomers, such as the methods used to add, update, delete, and step through records. For example, the following code uses ADO methods to loop through all records in the recordset.

    Do While DE.rsCustomers.EOF = False
Debug.Print DE.rsCustomers.Fields(1).Value
DE.rsCustomers.MoveNext
Loop

Programmatically Executing Objects

The following are examples of executing Command objects using the method and Recordset objects associated with the DataEnvironment object. The examples use a Command object that is based on a recordset-returning stored procedure with two input parameters.

Executing a Command Object with Multiple Parameters

If the Command object has multiple parameters, and you want to pass selected parameters, you must manually set the value of the parameters using the Parameters collection. Then, you must use the Open method from the Command object. For example, the Command object "InsertCustomer" contains the parameter's identification, first name, and nickname. To execute this method and include all parameters, you can use the following method:

MyDE.InsertCustomer "34","Fred","Freddy"

However, to only include the identification and first name, you would use the following code:

MyDE.Commands("InsertCustomer").Parameters("ID").value = "34"
MyDE.Commands("InsertCustomer").Parameters("Name").value = "Fred
MyDE.Commands("InsertCustomer").Open

Executing a Command Object with Parameters

The following example shows recordset-returning stored procedures with two Parameter objects.

Dim MyDE As DataEnvironment1
Dim nRecords As Integer
Dim nSum As Long

MyDE.SalesTotalByCityState "Seattle","WA"
MyDE.RSSalesByCityState.MoveFirst
For nRecords = 1 To MyDE.RSSalesByCityState.RecordCount
   nSum = nSum + MyDE.RSSalesByCityState.Fields("Invoice_Amt")
   MyDE.RSSalesByCityState.MoveNext
Next nRecords
Debug.Print nSum

Following is a more complicated example that shows a Command object that is based on a stored procedure that returns both a Recordset object and a return value. The example also contains an input and an output Parameter object.

Dim MyDE As DataEnvironment1
Dim sOutStatus As String, nNumRecords As Long
nNumRecords = MyDE.OrdersByEmployee("SMITH", sOutStatus)
If sOutStatus = "Succeeded" Then
   MyDE.OrdersByEmployee_RS.MoveFirst
   While Not MyDE.RSOrdersByEmployee.EOF
      Debug.Print MyDE.RSOrdersByEmployee.Fields("OrderDate")
      MyDE.RSOrdersByEmployee.MoveNext
   End
End If

Executing a Non-Recordset Returning Command Object

The following example shows a non-recordset-returning stored procedure with a return value and Parameter object. A common scenario is to use stored procedures to insert, update and delete records. These stored procedures do not return Recordset objects, but do use input and output Parameter objects.

Dim MyDE As New DataEnvironment1
Dim sOutStatus As String, nNumRecordsAffected As Long
nNumRecords = MyDE.DeleteEmployee("34", sOutStatus)
If sOutStatus = "Succeeded" Then
   Debug.Print nNumRecords + " employee(s) were deleted."
Else
   Debug.Print "Delete was not successful."
End If


The Data Environment Extensibility Object Model

Using the Data Environment Extensibility Object Model you can programmatically manipulate your DataEnvironment object at design time. Extensibility is the capacity to extend the function of the development environment, thereby adding something to it that didn't exist there before. The Data Environment has an easy-to-understand modular system for customizing its environment through a programming interface known as the Extensibility Object Model. Using this model you can create add-in extensions to the Visual Basic Integrated Development Environment (IDE).

All functionality that is available through the Data Environment designer's user interface is also available through its Extensibility Object Model.

To prevent the display of user interface when using the Extensibility Object Model to add and delete objects, you must set the option properties directly from the Data Environment. For example, to ensure that no deletion confirmation user interface appears, set the PromptDelete property of the DataEnvironment object to False.

Note This example changes the confirmation setting for all DataEnvironment objects on your system and is not automatically reset. Therefore, you should return this to its default setting when complete.

Before you can use the Data Environment Extensibility Object Model, you must reference it in Visual Basic.

To reference the Data Environment Extensibility Object Model

  1. On the Project menu in Visual Basic, click References.

  2. From the References dialog box, select Microsoft Data Environment Extensibility Objects 1.0, and then click OK.

When using the Extensibility Object Model, the DesignerID of the Data Environment designer is DEDesigner.DataEnvironment, and the object reference is DEExtDesigner.

All Data Environment methods, objects, and properties available to you through the Extensibility Object Model are documented in the Visual Basic Language Reference. See the DEExtDesigner Object.

Using Your Data Environment in an Add-in

By using the Extensibility Object Model you can use your Data Environment in an add-in. The add-in can be:

  • A wizard to create a DataEnvironment object and automatically bind it to a form.

  • An alternative interface (other than the Data Environment) to create and modify DataEnvironment objects.

  • A means to programmatically obtain information about an existing DataEnvironment object.

  • A way to programmatically create a hierarchical set of Command objects using properties, and then obtain the ShapeText property to obtain the valid SHAPE Command generated by the DataEnvironment object.


Scenarios For Using the Data Environment Designer

This section presents scenarios that illustrate how you can use the Data Environment designer. Among the features highlighted are creating a hierarchy, adding aggregates, grouping, and binding to a Hierarchical FlexGrid.

Creating a Relation Hierarchy Between a Customer and an Order Table

To create a relation hierarchy between a Customer and an Order table

  1. Create Command object, connected to the Northwind database.

  2. Select the Customers table as the Command object's source. Change the Command object name to "Customers."

  3. Create a child Command off the Customer Command object. Select the Orders table as the child's source. Change the child Command object's name to "Orders."

  4. Click the Relation tab, and then link the parent and child Command objects using their CustomerID fields.

To view the results, see "Binding Your Data Environment to a Hierarchical FlexGrid Control" later in this topic.

Adding an Aggregate to the Customer Command Object

To add an aggregate to the Customer Command object to obtain the SUM of the order amounts for all orders per customer

  1. Edit the Customer Command object's properties.

  2. Click the Aggregate tab, and then click Add.

  3. Name the aggregate "OrdersTotal."

  4. Select Sum as the function.

  5. Select the Aggregate On item "Orders".

  6. Select OrderID as the Field to Sum.

The OrdersTotal aggregate is added to the DataEnvironment object as a new Field object. At run time, the OrdersTotal aggregate will contain a calculated value for each customer.

To view the results, see "Binding Your Data Environment to a Hierarchical FlexGrid Control" later in this topic.

Grouping the Customer Command Object by State

To group the customer Command object by state

  1. Edit the Customer Command object's properties.

  2. Click the Grouping tab, and select Group Command Object.

  3. Name the grouping Command object "ByCountry."

  4. Select "Country" from the Fields in Command list, and then click > to move it into the Fields Used for Grouping list.

An additional Recordset object, ByCountry, is exposed on top of the Customers Command object. When binding to the Hierarchical FlexGrid, you should bind to the ByCountry Recordset, not to the Customers Command object.

To view the results, see "Binding Your Data Environment to a Hierarchical FlexGrid Control" later in this topic.

Adding an Aggregate to Count the Number of Customers per State

To add an aggregate to count the number of customers per state

  1. Edit the Customer Command object's properties.

  2. Click the Aggregates tab, and click Add.

  3. Name the aggregate "CustCount."

  4. Select Count as the function.

  5. Select the Aggregate On item "Grouping".

  6. Select CustID as the Field to Count.

Another field is added to the summary (grouping) Command object. This Field object contains a calculated value that counts the number of customers per state.

To view the results, see "Binding Your Data Environment to a Hierarchical FlexGrid Control" later in this topic.

Adding a Grand Total Aggregate to Count the Number of States with Customers

To add a Grand Total Aggregate to count the number of states with customers

  1. Edit the Customer Command object's properties.

  2. Click the Aggregates tab, and then click Add.

  3. Name the aggregate "NumStatesWithCustomers."

  4. Select Count as the function.

  5. Select GrandTotal as the Aggregate On item.

  6. Select the "State" as the Field.

  7. Name the Grand Total "CustomerGrandTotal."

An additional Recordset object, NumStatesWithCustomers, is exposed on top of the Customers Command object. This new object contains the number of states that have customers.

To view the results, see the following section.

Binding Your Data Environment to a Hierarchical FlexGrid Control

The following scenario is provided so you can view the results of the previous scenarios. See the Hierarchical FlexGrid Control for more information.

To bind your Data Environment to a Hierarchical FlexGrid control

  1. Once you have completed the previous Data Environment scenarios, drag an MSHFlexGrid control onto a Visual Basic form.

  2. From the Visual Basic property sheet, set the DataSource property to the DataEnvironment object that contains the Command object that you want to bind to the Hierarchical FlexGrid control. For example, specify DataEnvironment1.

  3. From the Visual Basic Properties window, set the DataMember property to a parent Command object in your Data Environment. Your data is now bound to a Hierarchical FlexGrid control.

  4. To view the data in the Hierarchical FlexGrid, select Start from the Run menu or press f5. The structure is retrieved and displays in the Hierarchical FlexGrid.


Writing Reports with the Microsoft Data Report Designer

The Microsoft Data Report designer is a versatile data report generator that features the ability to created banded hierarchical reports. Used in conjunction with a data source such as the Data Environment designer, you can create reports from several different relational tables. In addition to creating printable reports, you can also export the report to HTML or text files.

Possible Uses

  • Automatically create reports that are exported in HTML format for instant distribution on the Internet.

  • Create reports that show the sums of transactions occurring on a daily basis.

Data Report Designer Features

The Data Report designer has several features:

  1. Drag-and-Drop Functionality for Fields—Drag fields from the Microsoft Data Environment designer to the Data Report designer. When you do this, Visual Basic automatically creates a text box control on the data report and sets the DataMember and DataField properties of the dropped field. You can also drag a Command object from the Data Environment designer to the Data Report designer. In that case, for each of the fields contained by the Command object, a text box control will be created on the data report; the DataMember and DataField property for each text box will be set to the appropriate values.

  2. Toolbox Controls—The Data Report designer features its own set of controls. When a Data Report designer is added to a project, the controls are automatically created on a new Toolbox tab named DataReport. Most of the controls are functionally identical to Visual Basic intrinsic controls, and include a Label, Shape, Image, TextBox, and Line control. The sixth control, the Function control, automatically generates one of four kinds of information: Sum, Average, Minimum, or Maximum. For more information about the Function control, see "Adding a Function Control to the Data Report."

  3. Print Preview—Preview the report by using the Show method. The data report is then generated and displayed in its own window.

    Note A printer must be installed on the computer to show the report in print preview mode.

  4. Print Reports—Print a report programmatically by calling the PrintReport method. When the data report is in preview mode, users can also print by clicking the printer icon on the toolbar.

    Note A printer must be installed on the computer to print a report.

  5. File Export—Export the data report information using the ExportReport method. Formats for export include HTML and text.

  6. Export Templates—You can create a collection of file templates to be used with the ExportReport method. This is useful for exporting reports in a variety of formats, each tailored to the report type.

  7. Asynchonous Operation—The DataReport object's PrintReport and ExportReport methods are asynchronous operations. Using the ProcessingTimeout event, you can monitor the state of these operations and cancel any that are taking too long.

Topics

  • Parts of the Data Report

  • Creating a Simple Data Report

  • Exporting a Data Report

  • Printing a Data Report

  • Data Report Events

  • Understanding Control Placement on the Data Report Designer

Sample Application

The sample application named prjNwind corresponds to the project built in the series of topics that begin with "Creating a Simple Data Report." If you have installed the Visual Basic sample applications, the file can be found in the Samples directory. If you have not installed the sample applications, the file can be found on the MSDN™ CD that accompanies Visual Basic.


Parts of the Data Report

The Data Report designer consists of the following objects:

  1. DataReport object—Similar to a Visual Basic form, the DataReport object has both a visual designer and a code module. Use the designer to create the layout of a report. You can also add code to the designer's code module to programmatically format controls or sections contained by the designer.

  2. Section object—Each section of the Data Report designer is represented by a Section object in a Sections collection. At design time, each section is represented by a header that you can click to select the section, and the section's pane where you can place and position controls. Use the object and its properties to dynamically reconfigure a report before it is built.

  3. Data Report Controls—Special controls that only work on the Data Report designer are included with it. (Note: you cannot use Visual Basic's intrinsic controls, or any ActiveX controls, on the Data Report designer). These controls are found in the Visual Basic Toolbox, but they are placed on a separate tab named "DataReport."

Sections of the Data Report Designer

The default Data Report designer contains these Sections:

  • Report Header—contains the text that appears at the very beginning of a report, such as the report title, author, or database name. If you want the Report Header to be the first page in the report, set its ForcePageBreak property to rptPageBreakAfter.

  • Page Header—contains information that goes at the top of every page, such as the report's title.

  • Group Header/Footer—contains a "repeating" section of the data report. Each group header is matched with a group footer. The header and footer pair are associated with a single Command object in the Data Environment designer.

  • Details—contains the innermost "repeating" part (the records) of the report. The details section is associated with the lowest-level Command object in a Data Environment hierarchy.

  • Page Footer—contains the information that goes at the bottom of every page, such as the page number.

  • Report Footer—contains the text that appears at the very end of the report, such as summary information, or an address or contact name. The Report Footer appears between the last Page Header and Page Footer.

Data Report Controls

When a new Data Report designer is added to a project, the following controls are automatically placed in the Toolbox tab named DataReport:

  • TextBox Control (RptTextBox)—allows you to format text, or assign a DataFormat.

  • Label Control (RptLabel)—allows you to place labels on the report to identify fields or sections.

  • Image Control (RptImage)—enables you to place graphics on your report. Note that this control cannot be bound to a data field.

  • Line Control (RptLine)—lets you draw rules on the report to further distinguish sections.

  • Shape Control (RptShape)—enables you to place rectangles, triangles, or circles (and ovals) on a report.

  • Function Control (RptFunction)—a special text box that calculates values as the report is generated.


Creating a Simple Data Report

This topic creates a simple data report using a Data Environment designer as a data source. The Data Environment designer uses the NorthWind database supplied with Visual Basic to create a simple hierarchical cursor. The cursor contains two tables, Customers and Orders, and uses the CustomerID field to link the two. The finished report resembles the figure below.

Simple Data Report: Order Dates by Customers

Before you begin the step-by-step process, ensure that the Northwind database (Nwind.mdb) is present on your computer. If it is not present, copy the file from your Visual Basic CD onto your hard disk.

To create a simple hierarchical cursor in the Data Environment designer

  1. Create a new Standard EXE project.

  2. On the Project menu, click Add Data Environment to add a designer to your project. If the designer is not listed on the Project menu, click Components. Click the Designers tab, and click Data Environment to add the designer to the menu.

    Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

  3. On the Data Link Properties dialog box, click Microsoft Jet 3.51 OLE DB Provider. This selects the correct OLE DB provider for accessing a Jet database.

  4. Click the Next button to get to the Connection tab.

  5. Click the ellipsis button (…) next to the first text box.

  6. Use the Select Access Database dialog box to navigate to the nwind.mdb file, which is installed in the Program Files\Microsoft Visual Studio\Vb98 directory.

  7. Click OK to close the dialog box.

  8. Right-click the Connection1 icon, and click Rename. Change the name of the icon to Northwind.

  9. Right-click the Northwind icon, and then click Add Command to display the Command1 dialog box. In the dialog box, set the properties as shown below:
    Property | Setting
    Command Name | Customers
    Connection | Northwind
    DataBase Object | Table
    Object Name | Customers

  10. Click OK to close the dialog box.

  11. Right-click the Customers command, and click Add Child Command to display the Command2 dialog box. In the dialog box, set the properties as shown below:
    Property | Setting
    Command Name | Orders
    Connection | Northwind
    DataBase Object | Table
    Object Name | Orders

  12. Click the Relation tab. The Relate to a Parent Command Object check box should be checked. The Parent box should contain Customers; both the Parent Fields and Child Fields/Parameters boxes should contain CustomerID.

    When designing relational databases, it's customary for related tables to use the same name for linking fields. In this case, the linking fields are both named CustomerID. The Data Environment designer automatically matches such pairs in the dialog box.

  13. Click Add. Click OK to close the dialog box.

    Clicking the Add button adds the relation to the Command object. After closing the dialog box, the Data Environment designer reflects the relationship by displaying the two commands as a hierarchy. This hierarchy will be used to create the data report.

  14. Set the properties of the project and designer according to the settings below, then save the project:
    Object | Property | Setting
    Project | Name | prjNwind
    DataEnvironment | Name | deNwind
    Form | Name | frmShowReport

Creating the Data Report

Once the Data Environment designer has been created, you can create a data report. Because not all of the fields in the data environment will be useful in a report, this series of topics creates a limited report that displays only a few fields.

To create a new data report

  1. On the Project menu, click Add Data Report, and Visual Basic will add it to your project. If the designer is not on the Project menu, click Components. Click the Designers tab, and click Data Report to add the designer to the menu.

    Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

  2. Set the properties of the DataReport object according to the table below:
    Property | Setting
    Name | rptNwind
    Caption | Northwind Data Report
  3. On the Properties window, click DataSource and then click deNwind. Then click DataMember and click Customers.

    Important To set the DataSource property to deNwind, the Data Environment designer must be open. If it is closed, press CTRL+R to display the Project window, then double-click the data environment icon.

  4. Right-click the Data Report designer, and click Retrieve Structure.

    You have added a new group section to the designer. Each group section has a one-to-one correspondence to a Command object in the data environment; in this case, the new Group section corresponds to the Customers Command object. Notice also that the Group Header has a matching Group Footer section.

    Note The Data Environment allows you to create hierarchies of Command objects wherein a Command object has more than one child object — child Command objects parallel to each other. The Data Report designer, however, is not as flexible, and can't display more than one child object at a time. In such cases, when executing a Retrieve Structure command, the Data Report will display only the first of the child commands, and none below it. Thus you should avoid creating Command hierarchies with parallel children commands.

  5. From the Data Environment designer, drag the CompanyName field (under the Customers command) onto the Group Header (Customers_Header) section.

    The Group Header section can contain any field from the Customers command, however, for demonstration purposes, only the Customer name is displayed at this time.

  6. Delete the Label control (rptLabel) named Label1.

    If you do not want a Label control to be included with the TextBox control, you can uncheck the Drag and Drop Fields Caption option on the Field Mapping tab of the Data Environment designer's Options dialog box.

  7. From the Data Environment designer, drag the OrderDate field (under the Orders command) onto the Details (Orders_Detail) section. Delete the Label control.

    The Details section represents the innermost "repeating" section, and thus corresponds to the lowest Command object in the Data Environment hierarchy: the Orders Command object.

  8. Resize the Data Report designer's sections to resemble the figure below:

    It's important to resize the height of the Details section to be as short as possible because the height will be multiplied for every OrderDate returned for the CompanyName. Any extra space below or above the OrderDate text box will result in unneeded space in the final report.

  9. Save the project.

Preview the Data Report Using the Show Method

Now that the data environment and the data report objects have been created, you are almost ready to run the project. One step remains: to write code to show the data report.

To show the data report at run time

  1. On the Project Explorer window, double-click the frmShowReport icon to display the Form designer.

  2. On Toolbox, click the General tab.

    When you add a Data Report designer to your project, its controls are added to the tab named DataReport. To use the standard Visual Basic controls, you must switch to the General tab.

  3. Click the CommandButton icon and draw a CommandButton on the form.

  4. Set the properties of the Command1 control according to the table below:
    Property | Setting
    Name | cmdShow
    Caption | Show Report

  5. In the button's Click event, paste the code below.
    Private Sub cmdShow_Click()
rptNwind.Show
End Sub
  6. Save and run the project.

  7. Click Show Report to display the report in print preview mode.

Optional—Setting the Data Report as the Startup Object

You can also display the data report with no code at all.

  1. On the Project menu, click prjNwind Properties.

  2. In the Startup Object box, select rptNwind.

  3. Save and run the project.

    Note If you use this method, you can remove the Form object from your project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Extending the Data Report


Extending the Data Report

Once you have a simple data report, it's easy to extend it and make a more complex presentation. In the procedure below, the data environment is first extended with the Order Details and Products tables. The extended report includes the names of products ordered on a specific date, and resembles the figure below.

Extended Data Report With Order Details

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

To extend the data environment

  1. On the Data Environment designer, right-click the Orders Command object. Then click Add Child Command.

  2. On the Command1 Properties dialog box, set the following properties:
    Property | Setting
    Command Name | OrderDetails
    Connection | Northwind
    DataBase Object | Table
    Object Name | Order Details

  3. Click the Relation tab. The Relate to a Parent Command Object check box should be checked. The Parent box should contain Orders; both the Parent Fields and Child Fields/Parameters boxes should contain OrderID. Click the Add button and then click OK to close the dialog box.

  4. Right-click the OrderDetails Command object, and click Add Child Command. Set the properties of the connection as shown below:
    Property | Setting
    Command Name | Products
    Connection | Northwind
    DataBase Object | Table
    Object Name | Products
  5. Click the Relation tab. The Relate to a Parent Command Object check box should be checked. The Parent box should contain OrderDetails; both the Parent Fields and Child Fields/Parameters boxes should contain ProductID. Click the Add button and then click OK to close the dialog box.

Extending the Data Report

Once the data environment has been extended with new tables, you can extend the data report as well by dragging fields from the Data Environment designer to the Data Report designer.

To Extend the data report

  1. Right-click the Data Report designer, and clear Show Page Header/Footer box.

    Clearing this option deletes the page header and footer, which are not being used at this point.

  2. Right-click the Data Report designer, and click Insert Group Header/Footer. The Insert New Group Header/Footer dialog box will be displayed.

    The dialog box allows you to determine if the new header and footer will "bracket" other header/footer pairs. This becomes important as you add more header and footer pairs because the outermost pair of header/footers subordinates all other pairs. This is discussed in greater detail in "Parts of the Data Report."

  3. Click OK to select the default placement of the new header and footer pair and close the dialog box.

  4. Select the new group header, and on the Properties window, change its name from Section1 to Orders_Header. Change the corresponding footer name from Section4 to Orders_Footer.

  5. Repeat steps 2 to 3. Name the new group header OrderDetails_Header, and the new group footer OrderDetails_Footer.

  6. Click the Detail (Orders_Detail) section to select it. On the Properties window, change the section's name to Products_Detail.

  7. Using the mouse, drag the OrderDate field from the Detail (Products_Detail) section to the Orders_Header section.

  8. From the Data Environment designer, drag the ProductName field (under the Products command) into the Detail (Products_Detail) section.

  9. Delete the Label control named Label1.

  10. Resize the group headers, and rearrange the text box controls to resemble the figure below.

    The figure above requires some explanation. First, the Group footers are all closed in order to take up the least possible space. Like the Details section, any additional space left in any header or footer will be multiplied in the final report. Therefore, if a header or footer doesn't contain any fields, you can close the distance between the headers or footers.

    The Group Header named OrderDetails_Header is also closed. If you wonder why no fields are being shown, you must understand that the Order Details table in the Northwind database is a join table—the table contains only the IDs of records from the Orders table joined to IDs of records from the Products table. Thus the Order Details table doesn't contain fields which are actually displayed. Instead, it functions only to join two other tables. In the Data Report designer, the Order Details table therefore functions only to create groups of records—the product names grouped under the order dates.

    Finally, the Details section contains only the names of products. The Details section contains the innermost level of repeating records.

  11. Save and run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Adding a Calculated Field to the Data Report
Start from the beginning | Creating a Simple Data Report


Adding a Calculated Field to the Data Report

A calculated field is a field whose value is calculated as the report is generated. For example, when estimating the tax on an order, you must multiply the total price by the local tax rate. But since local tax rates differ and won't be stored in the database, the tax is generated as the report is created—a calculated field.

In this example, the extended Data Report designer is modified to include a Total field that calculates the value of Quantity * UnitPrice. Adding a calculated field involves the following steps:

  1. Using a SQL statement in the data environment's Command object to create the calculated field.

  2. Adding three text box controls to the data report: Quantity, UnitPrice, and (for the calculated field) Total.

The modified data report resembles the figure below.

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

To add a calculated field to the data report

  1. In the Data Environment designer, right-click the OrderDetails command. Then click Properties to display the OrderDetails Properties dialog box.

  2. On the General tab, click the SQL Statement button, and add the following statement to the box:
    Select OrderID, ProductID, UnitPrice, Quantity, (Quantity * UnitPrice) As Total From [Order Details]

    The SQL statement multiplies the Quantity value by the UnitPrice value to create the Total value—the calculated field. Also note that the name of the table (Order Details) contains a space, and must be enclosed by brackets.

  3. Click OK to close the dialog box.

  4. From the Data Environment designer, drag the Quantity, UnitPrice, and Total fields (under the OrderDetails command) onto the Detail (Products_Detail) section of the Data Report designer.

  5. Delete two Label controls, and change the Caption value of the remaining Label control to *, and arrange the controls to resemble the figure below:

  6. On the Data Report designer, click the UnitPrice text box to select it. On the Properties window, double-click DataFormat to display the Property Pages dialog box.

  7. In the Format Type box, click Currency. In the Symbol combo box, select the currency appropriate to your country.

  8. Repeat steps 6 and 7 to change the DataFormat property of the Total text box to Currency.

  9. Click the Total text box control to select it. On the Properties window, change the Alignment property to 1 – rptJustifyRight.

  10. Save and run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Adding a Function Control to the Data Report
Start from the beginning | Creating a Simple Data Report


Adding a Function Control to the Data Report

The Data Report designer features its own set of controls. Among these, the Function control has no counterpart among the Visual Basic intrinsic controls and deserves further explanation.

The Function control displays data that is calculated at run time, using a built-in function, as the report is generated. A typical example is shown in the figure below, where the Function control is used to display the sum of sub-totals to create a total value for a particular order.

If you have created a calculated field using a SQL statement (in the previous topic, "Adding a Calculated Field to the Data Report"), you may wonder why a Function control can't also be used to calculate the value of Quantity * UnitPrice. In short, the Function control can calculate values only after all other records in a group section have been processed. In contrast, a SQL statement calculates the values as part of the fields in a record as they are processed.

The steps to adding a Function control to the Data Report designer are:

  1. Draw a Function control in an appropriate Footer section of the Data Report designer.

  2. Set the DataMember and DataField properties to appropriate values (a numeric field from a relevant data environment Command object.)

Data Report with Function Control Showing Totals

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

To add a Function control to the data report

  1. Using the mouse pointer, click the group footer named Customers_Footer, and drag it towards the bottom of the window, to create a space between it and the Orders_Footer footer.

  2. On the Toolbox, click the rptFunction control.

  3. Draw the rptFunction control in the space between the two footers.

  4. Set the properties of the rptFunction control according to the table below:
    Property | Setting
    DataMember | OrderDetails
    DataField | Total
    Name | fncTotal
    Alignment | 1 – rptJustifyRight
    DataFormat | Currency

    The FunctionType property of the control determines what operation will be performed with the data found in the DataField. By default, the property is set to 0-RptFuncSum, to sum the data. Other functions include: Average, Minimum, Maximum, Row Count, STDDev (Standard Deviation), and Value Count.

  5. Draw a Line control just above the Quantity text box, and a Label control to the left of the text box. Set the Caption property of the label to Total.

  6. Click the fncTotal control to select it. On the Properties window, double-click Font to display the Font dialog box. Change the Font style to Bold.

  7. Repeat step 6 with the Label control.

  8. Position the controls on the designer to resemble the figure below:

  9. Save and run the project.

Increasing the scope of the function

You can increase the scope of the function by placing the control in a lower group footer. For example, you may want to find out the grand total of each customer's set of orders.

To show a grand total for all orders

  1. Draw a new Function control in the Customers_Footer section. Align the new control with the Function control created above.

  2. Draw a Label control to the left of the new Function control.

  3. Set the properties of the controls as shown in the table below:
    Control | Property | Setting
    rptFunction | Name | fncGrandTotal
    rptFunction | DataMember | OrderDetails
    rptFunction | DataField | Total
    rptFunction | Font (Font Style) | Bold
    rptFunction | Alignment | 1 – rptJustifyRight
    rptLabel | Caption | Grand Total
    rptLabel | Font (Font Style) | Bold
    rptLabel | Alignment | 1 – rptJustifyRight

  4. The Data Report designer should now resemble the figure below:

  5. Save and run the project.

Optional—Adding a Report Total

It's possible to increase the scope of the Function control even further. If you wish to see a total of all orders in the report, you can add a Function control to the Report Footer section.

To add a Report Total to the data report

  1. Right-click the Data Report designer and click Show Report Header/Footer.

  2. Add a Function control to the report footer.

  3. Set the property of the control as shown in the table below:
    Property | Setting
    Name | fncReportTotal
    DataMember | OrderDetails
    DataField | Total
    DataFormat | Currency
    Alignment | 1 – rptJustifyRight

    Note The number for the entire report will be especially large. Therefore the width of the Function control will have to be adjusted accordingly.

  4. Save and run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Grouping Information in the Data Report
Start from the beginning | Creating a Simple Data Report


Grouping Information in the Data Report

Information that is grouped can give the user a different perspective on data. Working in tandem with the Data Environment designer, the Data Report designer gives you the ability to group data according to any field in a table. The figure below shows a data report grouped by countries.

Data Report Grouped By Country

Grouping by Command Field

The grouping field provided by the data environment's Command object differs from the grouping already achieved by creating group headers and footers. Instead of using the table of a database as the basis for grouping, the Grouping feature of the Data Environment designer allows you to select a particular field in the table as the grouping field without having to create a new Command object.

The steps to adding a grouping field include:

  1. Creating a group field in the Data Environment designer.

  2. Adding a Group Header/Footer to the Data Report designer to correspond to the new command.

  3. Resetting the DataMember property of the data report to the new Grouping Command object created in the data environment.

  4. Dragging the group field from the data environment to the data report.

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

To add a grouping field to the Data Report designer

  1. In the Data Environment designer, right click the Customers Command object. Click Properties to show the Customers Properties dialog box.

  2. Click the Grouping tab.

  3. Click Group Command Object.

  4. In the Fields in Command box, double-click Country. Click OK to close the dialog box.

    Notice that the Customers Command object has been renamed as Customers Grouped using Customers_Grouping, and that two new folders have been created for the fields. The first, Summary fields in Customer_Grouping contains the field(s) that the subordinate fields will be grouped by. The subordinate fields are contained in the second folder, Detail Fields in Customer_Grouping.

  5. Right-click the Data Report designer, and then click Insert Group Header/Footer.

  6. Click the up arrow three times to insert the group header at the outermost edge of the header/footer pairs. Click OK to close the dialog box.

  7. On the Data Report designer, click the new Group Header to select it, and change its name from Section1 to Customers_Grouping_Header in the Properties window. Click the new Group Footer to select it, and change its name to Customers_Grouping_Footer.

  8. Click the Data Report designer's title bar to select the entire data report. On the Properties window, click DataMember and change the property from Customers to Customers_Grouping.

    When the grouping field was added to the data environment, the equivalent of a new Command object was also added to the data report. That virtual Command object is displayed in the drop-down list of data members as Customers_Grouping.

  9. In the Data Environment designer, open Summary Fields in Customers_Grouping. Drag the Country field into the new section on the Data Report designer.

  10. Delete the Label control that accompanies the Country field. Place the new field at the leftmost edge of the designer, which should now resemble the figure below:

  11. Save and run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Adding an Aggregate Field to the Data Report
Start from the beginning | Creating a Simple Data Report


Adding an Aggregate Field to the Data Report

In the Data Environment designer you can also create aggregate fields—fields that summarize data from a section. An aggregate field is thus similar to the Function control in that both are calculated as the report is generated. But there are a few differences: whereas the Function control can only be placed in a Group Footer, an aggregate field can be placed in any section of the Data Report designer, except the Report Header/Footer and Page Header/Footer sections.

Another difference is found in how the two fields are created: while the Function control is a feature of the Data Report designer, the aggregate field is a feature of the Data Environment designer. The steps to adding an aggregate field to the report are:

  1. Create an aggregate field in the Data Environment designer.

  2. From the Data Environment designer, drag the aggregate field onto the Data Report designer.

As an example, the figure below shows an aggregate field that displays the number of products ordered.

Data Report with Aggregate Field

Using a Function control can be more efficient than an aggregate field. When the data report calculates the value for a Function control, it takes a certain amount of processing to create each value. When the data environment creates an aggregate field, it takes a similar amount of processing. However, you can save that processing time by using the Function control because the Data Report designer must create the entire report in its own process.

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

To create an aggregate field in the data environment

  1. On the Data Environment designer, open the Detail Fields in Customers folder, and right-click Orders, then click Properties.

  2. On the Orders Properties dialog box, click the Aggregates tab.

  3. Click the Add button.

  4. Click the Name box and type ProductCount.

  5. In the Function combo box, select CNT.

  6. In the Aggregate combo box, select OrderDetails (it should be selected by default).

  7. In the Field combo box, select ProductID.

  8. Click OK to close the dialog box.

Now that you have created the aggregate field, you can place it on the Data Report designer.

To Add the aggregate field to the data report

  1. From the Data Environment designer, drag the ProductCount aggregate field onto the group section named Orders_Header.

  2. Change the Caption of the Label control to Product(s) Ordered.

  3. Reposition and resize the TextBox control and the Label control so the designer resembles the figure below:

  4. Save and run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Grouping Information in the Data Report
Start from the beginning | Creating a Simple Data Report


Forcing Page Breaks in the Data Report

A page break helps to make a report more readable (and therefore understandable) by breaking the data into logical chunks of information. The Data Report designer allows you to force page breaks before or after any group header or footer, or force a page break after the report header or before the report footer. (Forcing of page breaks on page headers and page footers is ignored because they already occur before and after a page break.)

Note This topic is part of a series that walks you through creating a sample data report. It begins with the topic, Creating a Simple Data Report.

The example below adds a page break before the group header that contains the CompanyName field.

To force a page break before the CompanyName field

  1. Click the group header named Customers to select it.

  2. On the Properties window, click ForcePageBreak, then click the arrow that appears at the right of the box.

  3. Click the 1 – rptPageBreakBefore from the drop-down list.

  4. Run the project.

Step by Step

This topic is part of a series that walks you through creating a sample data report.

To | See
Go to the next step | Adding Date, Time, Page Number, and Title to the Data Report
Start from the beginning | Creating a Simple Data Report


Adding Date, Time, Page Number, and Title to the Data Report

You can add Header and Footer data to each page of a data report by adding an appropriate Label control to the Page Header or Page Footer section. The Data Report designer features the following header and footer sections:

  • Report Header—Occurs only once, at the very top of the report. Can be used for introductory material, such as the purpose of the report.

  • Report Footer—Like the Report Header, occurs only once, at the very end of the report. Can be used for summary material, such as conclusions, or highlights of the report, or grand totals using the Function control.

  • Page Header—Occurs at the top of every page. Can be used for page number, date and time of the report, or title of the report.

  • Page Footer—Occurs at the bottom of every page. Can also be used for page number, date and time of the report, or other incidental information.

The Data Report designer contains several pre-configured controls that can be used to quickly add the date, time, page number, or report title to any section of the report. These controls are simply Label controls containing codes that display the following information:

Function | Code
Current Page Number | %p
Total Number of Pages | %P
Current Date (Short Format) | %d
Current Date (Long Format) | %D
Current Time (Short Format) | %t
Current Time (Long Format) | %T
Report Title | %i

Note To display a percent sign, use %%.

To add the title and page numbers to the page header

  1. Click the Data Report designer's title bar to select it.

  2. On the Properties window, click Title and type Orders By Date Under Customer Grouped by Country. This is the title that will display in the page header.

  3. Right-click the Data Report designer and click Show Page Header/Footer.

  4. Right-click the page header section and click Insert Control.

  5. From the menu, click Report Title.

  6. Reposition the new control in the middle of the section, extending it wide enough to display the entire report title.

  7. Repeat step 4.

  8. From the menu, click Current Page Number.

  9. Position the new control in the far right corner of the designer.

  10. Set the new control's Alignment property to 1 – Justify Right.

Step by Step

This topic is the last in a series that walks you through creating a sample data report.

To | See
Start from the beginning | Creating a Simple Data Report


Exporting a Data Report

After compiling a report you may wish to reuse it, either as part of a larger document or perhaps for distribution on an intranet or the Internet. The Data Report designer's ExportReport method allows you to accomplish these tasks. Using the ExportReport method, you can export any report as a text file or as an HTML file. Additionally, you can use any of a number of ExportFormat objects to tailor the content and appearance of an exported file.

Important The ExportReport method does not support the exporting of images or graphic shapes.

ExportFormat Objects

The ExportReport method was designed to work with the ExportFormats collection. Each ExportFormat object in the collection represents a separate format for the report. For example, a report formatted for intranet distribution might include names of groups or employees as part of the report header; for Internet distribution, those same names would be removed or replaced. You would therefore create at least two ExportFormat objects, each tailored for the distribution mechanism. However, it is possible to export a report without creating any ExportFormat objects because four are already provided for you.

Four Default ExportFormat Objects

By default, the ExportFormats collection contains four members. The four members and their associated file filters are shown in the chart below:

Object | File Filter | Description
ExportFormats(1) | *.htm, *.html | HTML
ExportFormats(2) | *.htm, *.html | Unicode HTML
ExportFormats(3) | *.txt | Text
ExportFormats(4) | *.txt | Unicode Text

If you need to use any of the default types, you can also use the Key property to specify a default type. The Key property values and the constants are shown below:

Object | Key | Constant
ExportFormats(1) | key_def_HTML | rptKeyHTML
ExportFormats(2) | key_def_UnicodeHTML_UTF8 | rptKeyUnicodeHTML_UTF8
ExportFormats(3) | key_def_Text | rptKeyText
ExportFormats(4) | key_def_UnicodeText | rptKeyUnicodeText

By using one of the four members, you can export a report without creating another ExportFormat object, provided the default meets your requirements. For example, to export a daily HTML report, you might use the following code:

DataReport1.ExportReport rptKeyHTML

Displaying a Dialog Box Is Optional

The programmer can determine whether or not a dialog box will be presented when exporting a report. For example, if the report is created automatically every morning, and written to the same file for distribution by an intranet, there is no need to display a dialog box. As long as a valid file path and key are supplied, and the Overwrite parameter is set to True, the dialog will not be displayed.

' Export a report as HTML, overwriting any existing file. Export
' all pages to the Daily_Report.htm file.
DataReport1.ExportReport rptKeyHTML, "C:\Temp\Daily_Report", True, , _
rptRangeAllPages

Note In the above code the second argument seems to be a directory but is actually the file name. "Daily_Report.htm" is the name of the written file. The ExportFormat object supplies the file extension (.htm), and thus there's no need to write it in the file name argument.

ExportFormat Supplies Dialog Information

The ExportFormat object also contains the information that is displayed when the user invokes the ExportReport method. In particular, the FileFormatString property sets the text that is displayed in the Export dialog box's Save As Type box. For example, imagine that a company has a standard ExportFormat object to be used with all reports. The following code would ensure that the ExportFormat is available from the list of format types on the Export dialog box:

Dim strTemplate As String

' First create the template for the ExportFormat object.
strTemplate = "MyCompany Daily Report" & vbCrLf & rptTagBody

' Add an ExportFormat object. The FileFormatString determines
' what will be displayed in the Export dialog box.
DataReport1.ExportFormats.Add _
   Key:="StandardReport", _
   FormatType:=rptFmtText, _
   FileFormatString:="Standard Report (*.txt)", _
   FileFilter:="*.txt", _
   Template:=strTemplate

' Invoke the ExportReport method specifying the ExportFormat
' object named StandardReport to use.
DataReport1.ExportReport "StandardReport", , False, True, _
rptRangeFromTo, 1, 10

When invoked, the Export dialog box resembles this:

Template Codes

The core of an ExportFormat object is its template. A template is simply a string containing both the text you want to appear along with constants that represent various parts of the data report. The constants, values, and descriptions are shown in the table below:

Constant | Value | Description
rptTagTitle | <!--MSDBRPT_Template_Title--> | Represents the title of the report, as found in the Title property.
rptTagBody | <!--MSDBRPT_Template_Body--> | Represents the body of the report.

To create a simple data report that includes only the name of the author followed by the body of the report, the template would resemble this:

Dim strT As String
strT = "Author: " & InputBox("Your name") & vbCrLf & rptTagBody
drpNwind.ExportFormats.Add "AuExp", rptFmtText, _
"Author Only Text File", "*.txt", strT


Printing a Data Report

Printing a data report can be accomplished in one of two ways. The user can click the Print button that appears on the data report in Print Preview mode (using the Show method), or you can programmatically enable printing using the PrintReport method. If an error occurs during printing, trap it in the Error event.

For More Information See "Data Report Events."

Choosing to Display a Print Dialog Box

When printing a report programmatically, you have two choices: to print by displaying the Print dialog box, or by printing without displaying the dialog box.

To display the Print dialog box

  1. Add a CommandButton to a Form.

  2. In the button's Click event, place the following code:
    DataReport1.PrintReport True

The Print dialog box allows the user to select a printer, print to file, select a range of pages to print, and specify the number of copies to print.

Note Printers must be installed on the computer in order to present a choice of printers.

Printing Without a Dialog Box

In some cases, you may wish to print the report without user intervention. The PrintReport method also gives you the option of selecting a range of pages to print, either all, or a specified range.

To print without displaying the dialog box

  1. Add a CommandButton to a Form.

  2. In the button's Click event, place the following code:
    DataReport1.PrintReport False

    Or, to specify a range of pages to print, use the code below:

    DataReport1.PrintReport False, rptRangeFromTo, 1, 2


Data Report Events

Like the standard Visual Basic form, the life of a Data Report designer is marked by certain key events. Those events, and the order in which they occur, are shown in the following table:

Event | Description
Initialize | Occurs after the query has completed, and controls are sited on the form.
Resize | Occurs when the designer is first displayed or when the window state of an object changes.
Activate | Occurs when the designer becomes the active window.
ProcessingTimeout | Occurs approximately once every second until all processing has ended. Use this event to determine if processing has taken too long, and to cancel the processing. Note This event will not occur until the query has completed. See below.
[Deactivate] | Occurs when the designer is no longer the active window. Use this event to determine if the user has clicked another form or designer.
QueryClose | Occurs before the designer is terminated. Set the Cancel argument to True to cancel termination. The CloseMode argument returns the type of action that is causing the termination.
Terminate | Occurs when all references to the designer have been set to 0.

Timeout and Asynchronous Call Events

In addition to the designer's lifetime events, the DataReport object also features events that allow you to trap errors and monitor synchronous and asynchronous function calls.

ExportReport and PrintReport: Query, Synchronous and Asynchronous Processes

When either the ExportReport or PrintReport method is invoked, the process is divided into three stages—the query, synchronous processing, and asynchronous printing or exportation:

  1. Query—When the data report is first created, a query is sent to the data provider.

  2. Processing—The data retrieved by the query is processed by Visual Basic to create the report. The data is cached in a temporary file on the computer. This process is synchronous.

  3. Asynchronous Printing or Exporting—After creating the report, the report is exported or printed. This process is asynchronous.

When the Show method is invoked, the data report executes the query and then processes the data in a synchronous process before displaying the report.

Because these methods combine both synchronous and asynchronous processes, there are separate events to monitor each kind of process.

The ProcessTimeOut Event—for Synchronous Functions

Processing a large data report may take some time. If you want to allow your users to cancel out of a lengthy operation (such as Show, ExportReport, or PrintReport), you can use the ProcessingTimeout event to monitor how many seconds have passed, and set the cancel argument to True at the user's command. The code below shows an example.

Private Sub DataReport_ProcessingTimeout(ByVal Seconds As Long, _
Cancel As Boolean, ByVal JobType As MSDataReportLib.AsyncTypeConstants, _
ByVal Cookie As Long)
   Select Case Seconds
   Case 30
      If MsgBox("This has taken " & Seconds & "seconds. Cancel?", _
         vbRetryCancel) = vbCancel Then
            Cancel = True
      End If
   Case 45
      If MsgBox("This has taken " & Seconds & "seconds. Cancel?", _
         vbRetryCancel) = vbCancel Then
            Cancel = True
      End If
   Case 60
      'Cancel automatically after 60 seconds.
      Cancel = True
   End Select
End Sub

Note It is not guaranteed that the ProcessingTimeout event will occur at the intervals specified above. For example, other Visual Basic code running in the background may prevent the event from occurring. In that case, set the Case statement to a range of values; when the event occurs, set a module-level flag to True, and check it on subsequent occurrences.

Error Event—For Asynchronous Functions

To trap errors that occur when no Visual Basic code is executing (that is, an asynchronous function), use the Error event. For example, if the PrintReport or ExportReport method fails in the asynchronous stage, the Error event will occur. The example below traps asynchronous errors:

Private Sub DataReport_Error(ByVal JobType As _
MSDataReportLib.AsyncTypeConstants, ByVal Cookie As Long, _
ByVal ErrObj As MSDataReportLib.RptError, ShowError As Boolean)
   Select Case JobType ' The JobType identifies the process.
   Case rptAsyncPrint
      ' Trap PrintReport errors here.
   Case rptAsyncReport
      ' Trap ExportReport errors here.
   End Select
End Sub

You can also use the Error event to trap specific cases, such as the lack of a printer on the computer, as shown in the code below:

Private Sub DataReport_Error(ByVal JobType As _
MSDataReportLib.AsyncTypeConstants, ByVal Cookie As Long, _
ByVal ErrObj As MSDataReportLib.RptError, ShowError As Boolean)
   Case rptErrPrinterInfo ' 8555
      MsgBox "A printing error has occurred. " & _
      "You may not have a Printer installed."
      ShowError = False
      Exit Sub
   Case Else
      ' handle other cases here.
      ShowError = True
   End Select
End Sub

The AsyncProgress Event

The AsyncProgress event is not designed to trap errors, but to allow you to monitor the state of the asynchronous function. By the time this event occurs, all of the data has been processed; thus two of the event's arguments are PagesCompleted and TotalPages. The event also includes arguments that identify the asynchronous operation: the JobType and Cookie arguments can then be used to monitor the progress of any process.


Understanding Control Placement on the Data Report Designer

Note You will understand this topic better if you read the Data Report tutorial first, beginning at Creating a Simple Data Report.

One way of creating a data report is to:

  1. Create a Data Environment designer containing a hierarchy of Command objects.

  2. Set the DataSource property of the Data Report designer to the Data Environment designer.

  3. Set the DataMember property to the topmost Command object.

  4. Right-click the Data Report designer and click Retrieve Structure.

    After retrieving the structure, an appropriate number of Group headers and footers will be created, and each header/footer pair is assigned a name that corresponds to a Command object.

  5. Drag Command objects from the Data Environment designer to the corresponding section on the Data Report designer.

    All of the data fields contained by a Command object are automatically created on the data report as TextBox controls on the section where the Command object is dropped. The DataMember and DataField properties of each TextBox are set in accordance with the Command object and its data fields.

  6. Drag TextBox controls from the section where each was created onto a different section of the Data Report designer.

  7. Add Function controls as needed to the report.

When the data report is bound to a data environment, the rules for placement of controls on the report are not immediately apparent. This topic explains how the hierarchy created in the data environment relates to the system of group headers and footers constructed in the data report.

Command Object Corresponds to Group Header/Footer or Details Section

With two exceptions, every Command object in the data environment corresponds either to a pair of group header and footer sections, or to the Detail section. The exceptions are discussed later in this topic.

Hierarchy Versus Headers, Footers, and Detail

The figure below shows a schematic view of the Data Environment designer with four Command objects, each in a parent/child relationship with at least one other command. The data fields that belong to the table are not shown.

The Data Report designer, on the other hand, is constructed as a series of sections. And each section can be categorized into one of four types: Report header/footers, Page header/footers, Group header/footers, and the Detail section. For the purposes of instruction, we can disregard the Report and Page header/footer pairs. This leaves the Group headers/footers and the Detail section.

The Detail section, the innermost section of the designer, corresponds to the lowest-level Command object. As you go up through the hierarchy, the Detail section is bracketed by pairs of sections, with each pair associated with a single Command object. The figure below correlates the Command objects with the sections:

Sections Corresponding to Command Objects

Thus the figure shows that the hierarchy of the data environment actually corresponds to an expanding series of brackets, with the innermost (Detail) section corresponding to the lowest level of the hierarchy, and the outermost corresponding to the highest-level Command object.

Controls Can Be Placed in Any Section in a Lower Level

The placement of a control is governed by what section (or pair of sections) it belongs to. In brief, a control can be placed in the section where it originates, and in all sections that are at a level lower than itself. For example, if a control belongs to the Command1 section pair, it can also be placed in the section pairs for Commands 2, 3, and 4. A second example: A control that originates in section 3 can also be placed in section 4, but not in sections 1 and 2. Taken to its conclusion, controls that originate in the Command 4 section (the Details section) cannot be placed anywhere else except in the Details section.

Placing the Function Control

The Function control has three exceptions to the above guidelines regarding control placement. The Function control is not directly bound to the recordset, as is the TextBox control. Instead, the Function control calculates its value as the report is generated. For this reason, a Function control can only be placed in Footer sections of the report.

A second exception for the Function control: it can only be placed in any section pair that is one level above its own. For example, if the Command 3 object contains a Quantity field, you can place a Function control that sums the Quantity values onto the footer section for Command 2, or the footer section for Command 1.

The third exception concerning the Function control is this: unlike the other data-bound control (the TextBox control), the Function control can be placed in the Report Footer section. When you do so, the scope of the control's calculation will be increased to include the whole report. For example, a Function control placed in the Report footer to calculate the sum of the Quantity field will calculate the sum of every Quantity control on the report.

The Exceptions: Grouping and Grand Total Aggregate

With two exceptions, every Command object in the Data Environment corresponds to a pair of group headers and footers. The first exception occurs when you use the Grouping feature of the Data Environment designer.

Grouping Fields

When you create grouping fields, the Data Environment designer creates two folders under a single Command object. The first contains the grouping fields, and the second contains the Detail fields. Even though a new Command object is not created for the grouping fields, you must create a new group header/footer pair on the data report and this requirement qualifies it as an exception.

Grand Total Aggregate Fields

The second exception occurs when you create a Grand Total aggregate field in the data environment. As happens when creating a grouping field, a new folder is created for the Command object. The new folder contains any Grand Total aggregate fields created, and you must add a new group header/footer pair to the data report.

For More Information Details about creating aggregate field can be found in "Creating Aggregates" in "Using the Data Environment Designer."

Checking the Hierarchy

If you are in doubt as to the hierarchy of the Data Environment Command objects, you have two ways of ensuring that the data report has the correct group header/footer structure:

Retrieve Structure—If you have not placed many controls onto the Data Report designer, and you can tolerate the restructuring of your data report, use the Retrieve Structure command to automatically create the right number of group headers and footers.

ADO Hierarchy Information—Right-click the topmost Command object in the Data Environment designer and click Hierarchy Info to display the Hierarchy Information dialog box. Click the View ADO Hierarchy tab to see a graphical representation of the Command objects' hierarchy.


Interacting with Data in a Microsoft Jet/Microsoft Access Database

Many data access applications created with earlier versions of Visual Basic store and manage data using the Microsoft Jet database engine, the engine used by Microsoft Access. These applications use Microsoft Data Access Objects (DAO) to access and manipulate data.

Now you can use Microsoft ActiveX Data Objects (ADO) to easily manipulate data in a variety of database formats, including Microsoft Jet format. You may still want to use DAO to work with your local Microsoft Jet databases, but for new applications you'll probably want to use ADO and the new data access features of Visual Basic.

Topics

This series of topics shows you how to use ADO and these new data access features to create a simple database application that interacts with data in the Northwind Traders sample database (Nwind.mdb). It demonstrates how to:

  • Create a connection to a Microsoft Access database file

  • Create a data environment Command object

  • Create a simple data-bound form

  • Create a data grid form based on a query

  • Create a master/detail form

  • Create a data-bound report


Create a Connection to a Microsoft Access Database File

When you work with data in a Microsoft Access database, you must first create a connection to a database file. The easiest way to create a connection to a Microsoft Access file is to create a data environment using the Data Environment designer.

This topic shows how to create a connection to the Northwind Traders sample database (Nwind.mdb). It assumes that the database file is located in the Samples folder on your computer.

To create a connection to a Microsoft Jet/Microsoft Access database file

  1. Add a Data Environment designer to your project.

  2. Set connection properties for the designer.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

Add a Data Environment Designer to Your Project

A Data Environment designer provides an easy way to create connections to many types of databases. To add a designer to your project, click Add Data Environment on the Project menu. Visual Basic loads a data environment and adds a Connection object to the data environment.

If the Data Environment designer is not available on the Project menu, add it to your Visual Basic environment. Click Components on the Project menu, click the Designers tab, and then click the check box next to its name in the list of designers.

Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

For More Information See Using the Data Environment Designer.

Set Connection Properties

You establish the connection to your data source by setting properties in the Data Link Properties dialog box. To display this dialog box, right-click the Connection object in your data environment and then choose Properties from the shortcut menu. Select an OLE DB provider on the Provider tab of the dialog box. Then click Next and enter connection information on the Connection tab of the dialog box. The layout of the Connection tab varies, depending on the OLE DB provider you select on the Provider tab.

For example, to connect to a Microsoft Jet/Microsoft Access database such as the Northwind Traders sample database, select the Microsoft Jet OLE DB provider on the Provider tab. On the Connection tab, enter the path to the database file; for example:

C:\Samples\Nwind.mdb

If the database file requires you to supply a user name and password, you can also enter those on the Connection tab. You set additional connection properties by clicking the Advanced or All tabs, or test the connection by clicking Test Connection.

For More Information For more information on connections and connection properties, see Connection Objects in "About the Data Environment Designer." And for more information on Data Links, see Data Link API Overview in "Database and Messaging Services" in the Platform SDK.

Step by Step

This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb.

To | See
Go to the next step | Create a Data Environment Command Object
Start from the beginning | Interacting with Data in a Microsoft Jet/Microsoft Access Database


Create a Data Environment Command Object

Once you've created a connection to your database, you can use the Data Environment designer to create Command objects that give you access to data. For example, you can create a simple Command object that gives you access to the data in a table, or a more complex Command object based on a query.

You can then use a data environment Command object as a data source in your application. First, you'll create a simple Command object based on the Customers table in the Northwind Traders sample database.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

To create a simple data environment Command object

  1. Open a Data Environment designer.

  2. Create the Command object.

Open a Data Environment Designer

You can open an existing Data Environment designer or create a new one. To open an existing one, click Designers in the Project window, and then double-click the name of the designer. To create a new one, follow the instructions in the "Create a Connection to a Microsoft Access Database File" topic.

Create the Command Object

Create a data environment Command object by clicking the Add Command button on the Data Environment designer toolbar, or by right-clicking the connection in the data environment window and choosing Add Command from the menu. You can then specify the Command object's name, the connection it uses, and the source of its data in the Command Properties dialog box. To display this dialog box, right-click the Command object in your data environment and then choose Properties from the shortcut menu.

For example, to create a Command object based on the Customers table in the Northwind Traders sample database, set the following properties:

Property | Setting
Command Name | CustomersTable
Connection | Connection1
Database Object | Table
Object Name | Customers

When you click OK, the Data Environment designer displays the Command object and its underlying fields in a hierarchical view in the Data Environment designer window.

The Data Environment designer also creates a Recordset object to represent the records returned by the Command object. The Recordset object uses the same name as the Command object but adds an "rs" prefix. For example, when the Data Environment designer creates the CustomersTable command, it also creates a Recordset object called "rsCustomersTable."

You can refer to the Command object in code as a method of the data environment, and refer to its recordset as a property of the data environment. For example, if you have created the CustomersTable command in a data environment called "MyDataEnvironment," you can refer to the command in code as follows:

MyDataEnvironment.CustomersTable

You can refer to the Command object's underlying recordset as follows:

MyDataEnvironment.rsCustomersTable

After you create the Command object, the Auto List Members feature and Object Browser display the command method and recordset property along with the other properties and methods of the data environment.

For More Information See How Commands are Exposed for Programmatic Access.

Step by Step

This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb.

To | See
Go to the next step | Create a Simple Data-Bound Form
Start from the beginning | Interacting with Data in a Microsoft Jet/Microsoft Access Database


Create a Simple Data-Bound Form

Once you've created a Command object in a Data Environment designer to serve as a data source, you can easily create a data-bound form by dragging the Command object onto a blank form. You can then add a mechanism for navigating through the records that are displayed.

In this topic, you'll create a form that displays records from the Customers table in the Northwind Traders sample database. Then you'll create command buttons for moving to the next or previous record.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

To create a simple data-bound form

  1. Drag a Command object from the Data Environment designer to a blank form.

  2. Create command buttons that let you navigate through records.

Drag a Command Object from the Data Environment Designer to a Blank Form

Much of the tedium of creating a data-bound form can be avoided in Visual Basic by dragging a Command object from the Data Environment designer to the form. Visual Basic automatically creates text box controls to display data from the Command object's recordset and sets data properties that bind the controls to fields in the recordset.

For example, to create a form that displays data from the Customers table in the Northwind Traders sample database, follow the steps in the "Create a Data Environment Command Object" topic to create a CustomersTable command. Then simply drag the Command object from the Data Environment designer onto a blank form. To view both the form and data environment simultaneously so you can perform the drag operation, select Tile Horizontally, Tile Vertically, or Cascade from the Window menu.

Create Command Buttons to Navigate through Records

By basing a data-bound form on a data environment Command object's recordset, you can easily create Next and Previous buttons that let you navigate through records. Each command button requires a single line of code.

For example, to create a Next button for the form that displays customer records, add a command button to the form and change its Caption and Name properties to Next. Then add the following line to the command button's Next_Click event procedure:

MyDataEnvironment.rsCustomersTable.MoveNext

The code uses the MoveNext method of the CustomersTable command's underlying recordset, rsCustomersTable. It refers to the recordset as a property of the command's data environment designer, MyDataEnvironment.

Similarly, you can create a Previous button by adding a command button to the form and change its Caption and Name properties to Previous. Then add the following code to the command button's Previous_Click event procedure:

MyDataEnvironment.rsCustomersTable.MovePrevious

When you run the form, Visual Basic displays the records in the Customers table and lets you move forward and backward through the recordset.

Step by Step

This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb.

To | See
Go to the next step | Create a Data Grid Form Based on a Query
Start from the beginning | Interacting with Data in a Microsoft Jet/Microsoft Access Database


Create a Data Grid Form Based on a Query

You can use the same drag-and-drop technique you used to create a simple data-bound form to create a data grid form based on a query. Simply create a Command object based on an SQL query, then drag the Command object onto a blank form. You can use the Query designer to create the SQL query.

In this topic, you'll create a form that displays orders for French customers in the Northwind Traders sample database in a data grid.

To create a data-bound form based on a query

  1. Create a data environment Command object based on a query.

  2. Drag the Command object from the data environment designer to a blank form.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

Create a Data Environment Command Object Based on a Query

You can easily create a data environment Command object based on a query by using the Query designer. First, follow the instructions in the previous steps in this scenario to open an existing Data Environment designer or create a new one.

Create the Command object by clicking the Add Command button on the Data Environment toolbar, or by right-clicking the connection in the Data Environment designer and selecting Add Command from the menu. You can then specify the Command object's name, the connection it uses, and the source of its data in the Command Properties dialog box. To display this dialog box, right-click the Command object in your data environment and then choose Properties from the shortcut menu.

For example, to create a Command objects based on a query of orders by French customers in the Northwind Traders sample database, set the following properties:

Property | Setting
Command Name | FrenchCustomersOrders
Connection | Connection1

Rather than select a specific database object as the basis for the command, as you did in the "Create a Data Environment Command Object" step, select SQL Statement as the source of data for the command. Then click the SQL Builder button to open the Query designer, where you specify the tables, fields, and criteria for the query.

For example, create a query that returns information about orders by French customers by dragging the Customers and Orders tables from the Data View window (available from the View menu) to the upper pane of the Query designer. (The designer automatically displays a line between the tables showing their related field, CustomerID.) Then click the check box next to the fields that you want to include in the query:

Table | Field
Orders | OrderID
Orders | CustomerID
Orders | OrderDate
Orders | ShippedDate
Customers | Country

Finally, specify the criteria for the query by entering "France" in the Country field's Criteria box in the Query designer grid. As you specify the fields and criteria, the Query designer automatically builds the query's underlying SQL statement:

SELECT Orders.OrderID, Orders.CustomerID, Orders.OrderDate, Orders.ShippedDate, Customers.Country
FROM Customers, Orders
WHERE Customers.CustomerID = Orders.CustomerID AND (Customers.Country = 'France')

To view the results of the query, right-click the Query designer and select Run. The Query designer displays the resulting recordset.

When you save the query, the Data Environment designer updates the Command object to use the SQL statement as its data source. To verify the statement, you can open the Command properties dialog box and see the statement is in the SQL Statement box.

For More Information See Designing Queries.

Drag the Command Object from the Data Environment to a Blank Form

In the "Create a Simple Data-Bound Form" topic, you created a data-bound form that displayed data in text boxes by dragging a Command object from the data environment designer to a blank form. You can also display the data in a data grid by dragging a data environment Command object.

For example, to create a data grid that displays the orders of French customers, select the FrenchCustomersOrders Command object you created in the previous step. Then while pressing the right mouse button, drag the Command object onto a blank form. Visual Basic displays a popup menu that lets you select whether to create a data grid or bound control. When you select Data Grid, Visual Basic automatically creates a data grid that displays records from the command's recordset.

When you run the form, Visual Basic displays the records returned by the FrenchCustomersOrders Command object in a data grid.

Step by Step

This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb.

To | See
Go to the next step | Create a Master/Detail Form
Start from the beginning | Interacting with Data in a Microsoft Jet/Microsoft Access Database


Create a Master Detail Report on a Form

Create a Master/Detail Form

In database applications, it's often useful to simultaneously view a record with a group of related records. For example, you may want to view a customer record and the current orders for the customer. A common way to accomplish this is to create a master/detail form.

To create a Master/Detail form

  1. Create a parent/child data environment Command object.

  2. Drag the Command object from the Data Environment designer to a blank form.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

Create a Parent/Child Data Environment Command Object

You've seen in the "Create a Data Environment Command Object" and "Create a Data Grid Form Based on a Query" topics how to create a data environment Command object based on a single table or query. You can also use a hierarchical parent/child Command object that lets you simultaneously view a record from one table or query with a group of related records in a second table or query.

For example, you can create a parent Command object that returns address information for all French customers in the Northwind Traders sample database. You can then add a related child Command object to the parent Command object that displays order information for each French customer.

Create the parent Command object by following the instructions in the previous steps in this scenario to open an existing Data Environment designer or create a new one. Then click the Add Command button on the Data Environment toolbar and set the following properties in the Command Properties dialog box:

Property | Setting
Command Name | FrenchCustomers
Connection | Connection1

Select SQL Statement as the source of data for the Command object. Click the SQL Builder button to open the Query designer, drag the Customers table from the Data View window to the upper pane of the Query designer, and then click the check box next to the CustomerID, CompanyName, Address, City, PostalCode, and Country fields. Specify the criteria for the query by entering "France" in the Country field's Criteria box. The Query designer builds the following SQL statement:

SELECT CustomerID, CompanyName, Address, City, PostalCode, Country
FROM Customers
WHERE (Country = 'France')

Add the child command to the parent command by right-clicking the FrenchCustomers command in the Data Environment designer, and then select Add Child Command from the popup menu. Set the following properties in the Command Properties dialog box:

Property | Setting
Command Name | OrderDates
Connection | Connection1

Select SQL Statement as the source of data for the Command object. Click the SQL Builder button to open the Query designer, drag the Orders table from the Data View window to the upper pane of the Query designer, and then click the check box next to the OrderID, CustomerID, OrderDate, and ShippedDate fields. The Query designer builds the following SQL statement:

SELECT OrderID, CustomerID, OrderDate, ShippedDate
FROM Orders

Finally, define the relationship between the parent and child Command objects. In the Data Environment designer, select the OrderDates Command object and click the Properties button on the toolbar. Click the Relation tab. Define a relationship between the CustomerID fields in the parent and child Command objects by selecting CustomerID in the Parent Fields and Child Fields/Parameters lists. Then click the Add button. The Data Environment designer adds the relation to the Relation Definition box.

When you've finished, the data environment displays a hierarchical view of the parent Command object and its fields along with the child Command object and its fields.

For More Information See How Commands are Exposed for Programmatic Access.

Drag the Command Object from the Data Environment to a Blank Form

As with the data-bound forms based on a single table or query, you can create a master/detail form based on a hierarchical parent/child Command object by simply dragging the Command object from the Data Environment designer to a blank form.

For example, create a master/detail form that displays address information and related order information for French customers by selecting the FrenchCustomers Command object you created in the previous step. Then while pressing the right mouse button, drag the Command object onto a blank form. Visual Basic displays a popup menu that lets you select whether to create a data grid, hierarchical flexgrid, or bound controls. When you select hierarchical flexgrid, Visual Basic automatically creates a master/detail form based on the parent/child Command object.

When you run the form, Visual Basic displays the address records returned by the FrenchCustomers Command object and related order records returned by the OrderDates Command object.

Step by Step

This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb.

To | See
Go to the next step | Create a Data-Bound Report
Start from the beginning | Interacting with Data in a Microsoft Jet/Microsoft Access Database


Create a Data-Bound Report

You can create a data-bound report using the same drag-and-drop technique you used to create data-bound forms. Simply create a Command object based on a table or query, then drag it onto a blank report in the Data Report designer.

In this topic, you'll create a report that displays orders for French customers in the Northwind Traders sample database.

To create a data-bound report

  1. Create a data environment Command object.

  2. Open a Data Report designer.

  3. Drag the Command object from the Data Environment designer to the Data Report designer.

  4. Set data properties for the report.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. It begins with the topic, Interacting with Data in a Microsoft Jet/Microsoft Access Database.

Create a Data Environment Command

You can follow the steps in the "Create a Data Environment Command Object" topic to create a command based on a table or in the "Create a Data Grid Form Based on a Query" to create a command based on a query. In this topic, you'll base the report on the FrenchCustomersOrders command you created in the "Create a Data Grid Form Based on a Query" topic.

Open a Data Report Designer

The Data Report designer lets you easily create reports from within the Visual Basic environment. You can use the Data Report designer to create a multiple-section data report, add controls to the report, and bind the report to data. You can also drag Command objects from the Data Environment designer to the Data Report designer.

To add a data report to your project, click Add Data Report on the Project menu. If the Data Report designer is not available on the Project menu, add it to your Visual Basic environment. Click Components on the Project menu, click the Designers tab, and then click the check box next to its name in the list of designers.

Note The first four kinds of ActiveX designers loaded for a project are listed on the Project menu. If more than four designers are loaded, the later ones will be available from the More ActiveX Designers submenu on the Project menu.

For More Information See Writing Reports with the Microsoft Data Report Designer.

Drag the Command from the Data Environment to the Data Report

Dragging a data environment Command object onto a Data Report designer is similar to dragging it onto a form. When you drop the Command object onto the report's Detail section, the Data Report designer automatically creates controls to display data from the Command object's recordset and sets data properties that bind the controls to fields in the recordset.

For example, to create a report that displays the orders of French customers, select the FrenchCustomersOrders Command object you created in the "Create a Data Grid Form Based on a Query" topic. Then drag the Command object onto the detail section of a blank report.

Set Data Properties for the Report

When you drag a Command objectfrom the Data Environment, the Data Report designer automatically sets data properties for each control that it creates. However, before the report can display data, you must also set the report's DataSource and DataMember properties.

For example, to display the orders of French Customers, set the properties as follows:

Property | Setting
DataMember | FrenchCustomersOrders
DataSource | MyDataEnvironment

When you run the report, the Data Report designer creates a report based on the records returned by the FrenchCustomersOrders Command object.

Step by Step

This topic concludes a series that walks you through creating a simple database application that interacts with data in Nwind.mdb. To start from the beginning, see Interacting with Data in a Microsoft Jet/Microsoft Access Database.


Creating a DHTML Application that Interacts with SQL Server Data

Visual Basic lets you easily create applications that use the Internet or a corporate intranet to interact with data in a distributed relational database system such as SQL Server. For example, you can create an application that uses an HTML page for data entry or to query data in your database.

Topics

This scenario shows you how to create a DHTML application that interacts with data in the Pubs sample database in SQL Server. It is designed for use on an intranet and requires that you view HTML pages using Microsoft Internet Explorer 4.x. The scenario demonstrates:

  • Adding a table to an SQL Server database

  • Creating a data entry HTML page

  • Creating an HTML page for viewing and updating data

  • Creating an HTML page that runs a stored procedure


Adding a Table to an SQL Server Database

When you create a DHTML application that interacts with a SQL Server database, you may want to add new tables to the database. You can use a data environment to create a connection to the SQL Server database, then create your tables in the Data View window.

This topic shows how to create a connection to the Pubs sample database in SQL Server, and how to add a new Customers table to the database.

To add a table to an SQL database

  1. Open a new DHTML Application DLL project.

  2. Create a connection to an SQL Server database.

  3. Add a table to the database.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in an SQL Server database. It begins with the topic Creating a DHTML Application that Interacts with SQL Server Data.

Open a DHTML Application DLL Project

The first step in creating a DHTML application is to open a new DHTML Application DLL project. A DHTML Application DLL project is an ActiveX DLL project template that automatically loads the DHTML Page designer and its HTML toolbox tab and HTML controls, which are known as elements. Select New Project from the File menu, then select DHTML Application in the New Project dialog box.

Visual Basic automatically adds a DHTML Page designer to your project. For every HTML page in your application, you need a corresponding designer. Open the Designers folder in the Project Explorer and double-click DHTMLPage1. The DHTML Page designer displays the page's interface in the right pane of the designer window, and a hierarchical list of the objects on the page in the left pane of the window.

For More Information For more information on creating a DHTML application, see Developing DHTML Applications in Building Internet Applications in the Component Tools Guide.

Create a Connection to an SQL Server Database

A data environment provides an easy way to create a connection to a database. To add a data environment to your project, select Add Data Environment from the Project menu. Visual Basic loads a data environment, and adds a Connection object to the data environment.

You establish the connection to your data source by setting properties in the Data Link Properties dialog box. To display this dialog box, right-click the Connection object in your data environment and then choose Properties from the menu. Select an OLE DB provider on the Provider tab of the dialog box. Then click Next and enter connection information on the Connection tab of the dialog box. The layout of the Connection tab varies, depending on the OLE DB provider you select on the Provider tab.

For example, to connect to an SQL Server database such as the Pubs sample database, select the Microsoft OLE DB Provider for SQL Server on the Provider tab. On the Connection tab, enter the server name, log on information, and the database name or SQL database file name.

You set additional connection properties by clicking the Advanced or All tabs, or test the connection by clicking Test Connection.

For More Information For more information on using the Data Environment, see Chapter 3, About the Data Environment Designer. For more information on connections and connection properties, see Connection Objects in "About the Data Environment Designer." And for more information on Data Links, see Data Link API Overview in "Database and Messaging Services" in the Platform SDK.

Add a Table to the Database

The Data View window lets you easily add a table, view, or stored procedure to your database. Open the Data View window by clicking the Data View Window button on the Standard toolbar. The Data View window displays a hierarchical view of any database connections you've created. For example, if you've created a connection to the Pubs sample database, the Data View window shows the database and its diagrams, tables, views, and stored procedures.

Add a table to the database by right-clicking the Tables folder in the Data View window and selecting the New Table command. The Data View window displays the Choose Name dialog box, where you enter a name for the table. When you click OK, the Data View window displays the New Table window, where you enter the table definition — the name of each column in the table, its datatype, and other information about the column. To specify a primary key for the table, disable the Allow Nulls box, right-click the cell, and select Set Primary Key from the shortcut menu.

For example, to add a Customers table to the Pubs database that stores customer address information, enter Customers in the Choose Name dialog box and enter the following table definition:

Column Name | Datatype | Length
CustomerID | char | 5
CompanyName | varchar | 20
Address | varchar | 40
City | varchar | 20
Region | varchar | 10
PostalCode | varchar | 10
Country | varchar | 20

Set CustomerID as the primary key. When you save the table definition, the table is added to the database.

For More Information For more information on using the Data View window, see Managing Databases in Data View. For more information on using the Data View window to create a table, see Tables.

Step by Step

This topic is part of a series that walks you through creating a simple DHTML application that interacts with data in an SQL Server database.

To | See
Go to the next step | Create a Data Entry HTML Page
Start from the beginning | Creating a DHTML Application that Interacts with SQL Server Data


Creating a Data Entry HTML Page

Visual Basic lets you easily create an HTML page that you can use to enter records in an SQL Server database. You can use the DHTML Page designer to create the HTML page and a data environment Command object to establish a connection to the database. You can write code that uses ActiveX Data Objects (ADO) to handle data processing.

This topic shows how to create an HTML page to enter data in the Customers table you created in the "Adding a Table to an SQL Server Database" topic. It shows how to use the data environment and ADO code to pass data from the HTML page to the table.

To create a data entry HTML page

  1. Add labels and TextField elements to an HTML page.

  2. Create a data environment Command object.

  3. Add a Button element to the page to append records to a table.

  4. Load the HTML page.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in an SQL Server database. It begins with the topic Creating a DHTML Application that Interacts with SQL Server Data.

Add Labels and TextField Elements to an HTML Page

You can use the DHTML Page designer to design your HTML page. Double-click the DHTML Page designer icon in the Project Explorer to bring up the designer. To add a label to a page, simply click in the right pane of the designer window and type the label text. To add a TextField element to the page, select the element from the toolbox and drag it to the right pane of the designer window.

For example, you can create a data entry HTML page that lets you enter address information in the new Customers table in the Pubs sample database. First, type "Customer ID:" in the right pane of the designer window to serve as a label for a Customer ID TextField element, then drag a TextField element next to the label, set its Name and ID properties to CustomerID, and clear its Value property. Repeat the same process for CompanyName, Address, City, Region, PostalCode, and Country elements.

For More Information For more information on using the DHTML Page designer, see Designing Pages for DHTML Applications in Chapter 2, "Developing DHTML Applications" of Building Internet Applications in the Component Tools Guide.

Create a Data Environment Command Object

As you saw in the "Interact with Data in a Microsoft Jet/Microsoft Access Database" scenario, a data environment Command object can give you access to a table's data. In a DHTML application, a data environment Command object can serve as a logical "middle tier," linking a data source with data displayed on an HTML page.

Create the Command object by clicking the Add Command button on the Data Environment toolbar, or by right-clicking the connection in the Data Environment designer and selecting Add Command from the shortcut menu. You can then specify the Command object's name, the connection it uses, and the source of its data in the Command Properties dialog box. To display this dialog box, right-click the Command object in your data environment and then choose Properties from the menu..

For example, you can create a data environment Command object that provides access to the new Customers table in the Pubs sample database. Set the following properties, using the connection to the Pubs sample database that you created in the "Adding a Table to an SQL Server Database" topic:

Property | Setting
Command Name | Customers
Connection | Connection1
Database Object | Table
Object Name | dbo.customers

To enable updating of records in the Customers table, change the default lock type used by the command. Click the Advanced tab in the Command Properties dialog box and set the Lock Type property to Optimistic.

For More Information For more information on creating a data environment Command object, see Command Objects in "About the Data Environment Designer."

Add a Button Element to the Page to Append Records to a Table

To provide a way to append the data entered on the HTML page to a table, you can add a Button element to the HTML page by dragging a Button element to the right pane of the DHTML Page designer. For example, you can add a Button element that appends address information to the Customers table. First, drag a Button element to the right pane of the designer window, and then set its name and id properties to AddCustomer.

Using the data environment and ADO, you can easily append records to a table in an SQL database. Refer to the data entered in an HTML page's TextField elements using the element's Value property, then use ADO code to append the data to the corresponding field in the table. The recordset underlying a data environment Command object can provide the "middle tier" between the table and the HTML page.

For example, to enable the data entry HTML page for the new Customers table in the Pubs sample database, add the following code to the onclick event procedure for the DHTML Page object's AddCustomer Button element:

Private Function AddCustomer_onclick() As Boolean

   ' Run the Customers Command object.
   MyDataEnvironment.Customers

   ' Append values from the DHTML Page object's elements
   ' to the command's underlying recordset.
   With MyDataEnvironment.rsCustomers
      .AddNew
      !CustomerID = CustomerID.Value
      !CompanyName = CompanyName.Value
      !Address = Address.Value
      !City = City.Value
      !Region = Region.Value
      !PostalCode = PostalCode.Value
      !Country = Country.Value
      .Update
      .Close
   End With

   ' Clear the elements.
   CustomerID.Value = ""
   CompanyName.Value = ""
   Address.Value = ""
   City.Value = ""
   Region.Value = ""
   PostalCode.Value = ""
   Country.Value = ""

End Function

For More Information For more information on using ADO, see ADO, DAO and RDO in Visual Basic.

Load the HTML Page

To view the finished page in Internet Explorer, press F5 or click the Start button on the toolbar. You can also explicitly make a dynamic-link library and HTML page for the project by setting properties on the Make tab of the Project Properties dialog box, then selecting Make .dll from the File menu.

For More Information For more information on testing your DHTML applications, see Testing Your DHTML Application in Chapter 2, "Developing DHTML Applications" in Building Internet Applications in the Component Tools Guide.

Step by Step

This topic is part of a series that walks you through creating a simple DHTML application that interacts with data in an SQL Server database.

To | See
Go to the next step | Creating an HTML Page for Viewing and Updating Data
Start from the beginning | Creating a DHTML Application that Interacts with SQL Server Data


Creating an HTML Page for Viewing and Updating Data

To work with existing data in a SQL Server database, you can create an HTML page that lets you view and edit the data. You can use the DHTML Page designer to create a new HTML page or modify an existing page. Then write code that uses ADO and the BindingsCollection object to handle data processing.

This topic shows how to create an HTML page to view and edit data in the Customers table you created and populated in the previous topic in this scenario. It shows how to use a data environment Command object, ADO code, and the BindingsCollection object to directly update SQL Server data from the HTML page.

To create an HTML page for viewing and update data

  1. Add labels and TextField elements to a new HTML page.

  2. Modify the data environment Command object to allow data updating.

  3. Add code to display records on the HTML page.

  4. Add Button elements to navigate through records.

  5. Load the HTML page.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in an SQL Server database. It begins with the topic Creating a DHTML Application that Interacts with SQL Server Data.

Add Labels and TextField Elements to a New HTML Page

The first step in creating an HTML page for viewing and updating data is to open a new HTML page, by selecting Add DHTML Page from the Project menu.

You can use the DHTML Page designer to add labels and TextField elements to the HTML page. For example, to create an HTML page for viewing and updating data in the Customers table in the Pubs sample database, follow the steps in the Creating a Data Entry HTML Page topic.

Modify the Data Environment Command Object to Allow Data Updating

When you create a Command object using the Data Environment designer, the Command object by default allows you to read but not update the Command object's data source. To allow data updating, you must change the Command object's CursorType, CursorLocation, and LockType properties.

For example, you can change the Customers Command object's CursorType, CursorLocation, and LockType properties to allow data in the Customers table to be updated directly from your HTML page. In the Data Environment designer, select the Customers Command object and click the Properties button on the Data Environment toolbar. Click the Advanced tab, and then set the CursorLocation property to Use server-side cursors, the CursorType property to Keyset, and the LockType property to Optimistic.

Add Code to Display Records on the HTML Page

Using the data environment, ADO, and the BindingCollection object, you can bind elements on an HTML page to SQL Server data when the page loads. You can then edit the data, and add code to update the underlying recordset or navigate through the records. For example, you can use a BindingCollection object with an ADO Recordset object to bind the TextField elements on your HTML Page to fields in the Customers table.

First, add a reference to the BindingCollection object's type library to your project. To add the reference, select References on the Project menu, then select Microsoft Data Binding Collection in the References dialog box. Then declare a BindingCollection object variable in the HTML Page object's Declarations section:

Dim colBind As BindingCollection

To bind the elements to the table fields when the HTML page loads in your browser, add code to the DHTML Page object's Load event procedure. As you saw in the "Creating a Data Entry HTML Page" topic, your data environment can provide the "middle tier" between the table and the HTML page.

Use the BindingCollection object to bind the elements on the HTML page to fields in the recordset:

Private Sub DHTMLPage_Load()

   ' Create a BindingCollection object, then set its
   ' DataSource property to your data environment and its
   ' DataMember property to the Customers command object.
   Set colBind = New BindingCollection
   With colBind
      Set .DataSource = MyDataEnvironment
      .DataMember = "Customers"

      ' Bind the Value property of elements on the HTML page
      ' to fields in the Customers recordset.
      .Add CustomerID, "Value", "CustomerID"
      .Add CompanyName, "Value", "CompanyName"
      .Add Address, "Value", "Address"
      .Add City, "Value", "City"
      .Add Region, "Value", "Region"
      .Add PostalCode, "Value", "PostalCode"
      .Add Country, "Value", "Country"
   End With

End Sub

For More Information For more information see BindingCollection object in the Language Reference.

Add Button Elements to Navigate through Records

After you've established data binding between your data environment and the object on the HTML page, you can easily create Button elements for navigating through records by using the recordset's MoveNext and MovePrevious methods. For example, to create a Next button for your HTML page, use the DHTML Page designer to add a Button element to the HTML page. Change its id and Name properties to Next. Then add the following line to the Button element's Next_onclick event procedure:

MyDataEnvironment.rsCustomers.MoveNext

Similarly, you can create a Previous button by adding a Button element to the HTML page and change its id and Name properties to Previous. Then add the following code to the Button element's Previous_onclick event procedure:

MyDataEnvironment.rsCustomers.MovePrevious

When you load the HTML page, Visual Basic displays the records in the Customers table and lets you move forward and backward through the recordset.

Load the HTML Page

To view the finished page in Internet Explorer, press the F5 key or click the Start button on the Standard toolbar. You can also explicitly make a dynamic-link library and HTML page for the project by setting properties on the Make tab of the Project Properties dialog box, then selecting Make .dll from the File menu.

Step by Step

This topic is part of a series that walks you through creating a simple DHTML application that interacts with data in an SQL Server database.

To | See
Go to the next step | Creating an HTML Page that Runs a Stored Procedure
Start from the beginning | Creating a DHTML Application that Interacts with SQL Server Data


Creating an HTML Page that Runs a Stored Procedure

You can increase the power and flexibility of your HTML applications by enabling them to run SQL Server stored procedures. You can create a stored procedure in the Data View window, then create Command objects and elements that use the stored procedure to view or update data.

This topic shows how to create a stored procedure that returns customer address records by country from the Customers table created earlier in this scenario. It shows how to add a Select element to the HTML page you created in the previous topic. The Select element uses the stored procedure to display address records based on the selected country.

To create an HTML page that runs a stored procedure

  1. Add a stored procedure to the database.

  2. Create a data environment Command object based on the stored procedure.

  3. Add Labels and TextField Elements to a new HTML Page.

  4. Add a Select element to the HTML page.

  5. Add code to run the stored procedure and display the results.

  6. Load the HTML page.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in an SQL Server database. It begins with the topic Creating a DHTML Application that Interacts with SQL Server Data.

Add a Stored Procedure to the Database

You can add a stored procedure to the database by right-clicking the Stored Procedures folder in the Data View window and selecting the New Stored Procedure command. The Data View window displays the New Stored Procedure window, where you enter the SQL Create Procedure statement that defines the stored procedure.

For example, you can add a spCustByCountry stored procedure to the Pubs database that accepts an input parameter, Country, and returns customer address information for the specified country. Enter the following Create Procedure statement in the New Stored Procedure window:

CREATE PROCEDURE spCustByCountry @Country varchar(20)
AS
SELECT * FROM Customers
WHERE Country = @Country

When you save the stored procedure, it is added to the database.

For More Information For more information on using the Data View window to create a stored procedure, see Stored Procedures in the SQL Editor.

Create a Data Environment Command Object Based on the Stored Procedure

In the "Create a Data Entry HTML Page" topic, you created a data environment Command object based on the Customers table in the Pubs sample database. You can also create a data environment Command object based on a stored procedure.

For example, you can create a data environment Command object based on the spCustByCountry stored procedure. Follow the steps in the "Interacting with Data in a Microsoft Jet/Microsoft Access Database" scenario, Chapter 5 of the Data Access Guide, to create the Command object, setting the following properties:

Property | Setting
Command Name | CustByCountry
Connection | Connection1
Database Object | Stored Procedure
Object Name | dbo.spCustByCountry

Add Labels and TextField Elements to a New HTML Page

After you've created the command that will provide the data source for your HTML page, you can create the page. Select Add DHTML Page from the Project menu. As you saw in the previous topics in this scenario, you can use the DHTML Page designer to add labels and TextField elements to the HTML page.

Add a Select Element to an HTML Page

You can use the DHTML Page designer to add a Select element to your HTML page. A Select element, similar to a combo box, lets you select an item from a list.

For example, to add a Select element called SelectCountry to the page you created in the previous topic, drag the element onto the page. Then set the element's Name and id properties to SelectCountry.

You can specify the list items to be displayed in the Select element by adding code to the HTML page's Load event procedure. For example, to display a list of countries included in the address information from the Customers table, add the following code:

Private Sub DHTMLPage_Load()
   ' Declare Select element, connection, recordset, and string variables.
   Dim selElement As HTMLSelectElement
   Dim cnn As ADODB.Connection
   Dim rsCountries As ADODB.Recordset
   Dim strSQL As String

   ' Assign the list of countries from the Customers table
   ' to the recordset variable.
   strSQL = "SELECT DISTINCT Country FROM Customers;"
   Set cnn = New ADODB.Connection
   cnn.Open MyDataEnvironment.Connection1
   Set rsCountries = New ADODB.Recordset
   rsCountries.Open strSQL, cnn1

   ' Fill the Select element's list with countries from the recordset,    ' setting each list element's Text property.   With rsCountries
   Do While Not .EOF
      Set selElement = Me.Document.createElement("OPTION")
      selElement.Text = !Country
      SelectCountry.Options.Add selElement
      .MoveNext
   Loop
   End With

End Sub

For More Information For more information on creating a Select element, see Working with Lists in the Page Designer in Building Internet Applications in the Component Tools Guide.

Add Code to Run the Stored Procedure and Display the Results

Using the data environment, ADO, and the BindingCollection object, you can easily add code to a Select element's onafterupdate event procedure to run a Command object that is based on a stored procedure. You can then update the data displayed in an HTML page.

For example, you can display address records on your HTML page for customers from a country selected in the SelectCountry element. First, declare a BindingCollection object variable in the HTML Page object's Declarations section:

Dim colBind As BindingCollection

Then add code to the element's onchange event procedure to run the cmdCustByCountry Command object using the value of the element as an input parameter:

Private Sub SelectCountry_onchange()

   Dim rsResults As ADODB.Recordset
   Dim intIndex As Integer

   ' Run the CustByCountry Command object using the value
   ' of the SelectCountry element as an input parameter.
   intIndex = SelectCountry.selectedIndex
   MyDataEnvironment.CustByCountry SelectCountry.Options(intIndex).Text

   ' Create a BindingCollection object, then set its
   ' DataSource property to your data environment and its
   ' DataMember property to the CustByCountry Command object.
   Set colBind = New BindingCollection
   With colBind
      Set .DataSource = MyDataEnvironment
      .DataMember = "CustByCountry"

      ' Bind the Value property of elements on the HTML page
      ' to fields in the CustByCountry recordset.
      .Add CustomerID, "Value", "CustomerID"
      .Add CompanyName, "Value", "CompanyName"
      .Add Address, "Value", "Address"
      .Add City, "Value", "City"
      .Add Region, "Value", "Region"
      .Add PostalCode, "Value", "PostalCode"
      .Add Country, "Value", "Country"
   End With

   ' Assign the recordset to the rsResults recordset variable.   Set rsResults = MyDataEnvironment.rsCustByCountry      ' Add HTML code to dynamically create a table on the page
' showing each customer record.
Me.Document.body.insertAdjacentHTML "BeforeEnd", "<DIV><HR SIZE=2></DIV>"   With rsResults      Do While Not .EOF         Me.Document.body.insertAdjacentHTML "BeforeEnd", _         "<TABLE BORDER CELLSPACING=1 CELLPADDING=7 ><TR HEIGHT=17>" & _
         "<TD WIDTH=70>" & rsResults!CustomerID & "</TD>" & _
         "<TD WIDTH=200>" & rsResults!CompanyName & "</TD>" & _
         "<TD WIDTH=300>" & rsResults!Address & "</TD>" & _
         "<TD WIDTH=100>" & rsResults!City & "</TD>" & _
         "<TD WIDTH=50>" & rsResults!Region & "</TD>" & _
         "<TD WIDTH=50>" & rsResults!PostalCode & "</TD>" & _
         "<TD WIDTH=50>" & rsResults!Country & "</TD></TR>" & _
         "</TABLE>"
         .MoveNext
      Loop
      .MoveFirst
      .Close
   End With

End Sub

Load the HTML Page

To view the finished page in Internet Explorer, press F5 or click the Start button on the Standard toolbar. You can also explicitly make a dynamic-link library and HTML page for the project by setting properties on the Make tab of the Project Properties dialog box, then clicking Make .dll on the File menu.

Step by Step

This topic concludes a series that walks you through creating a simple DHTML application that interacts with data in an SQL Server database. To start from the beginning, see Creating a DHTML Application that Interacts with SQL Server Data.


Interacting with Data in an ASCII Text File

The data your application needs to interact with may not always be stored in a relational database. It may be stored in delimited or fixed-length fields in an ASCII text file. For example, your application's data source may be data downloaded into a text file from a mainframe computer, a file converted from an unsupported database format or operating system, or an "ad hoc" data file created with a text editor.

By creating a data-aware class and using Microsoft ActiveX Data Objects (ADO), you can use Visual Basic to create applications that interact with data in an ASCII text file. Your application can read data from a text file, update fields, add new records, and write data back to the text file as if the data were stored in a relational database.

Topics

This series of topics shows you how to use a data-aware class and ADO to create a simple database application that interacts with data in a tab-delimited text file. It demonstrates:

  • Creating a data-aware class that reads records from a delimited text file

  • Creating a form that lets you view and update data from a data-aware class

  • Modifying the form to let you add new records

  • Modifying the class and form to write records back to the delimited text file


Creating a Data-Aware Class that Reads Records from a Delimited Text File

By creating a data-aware class, you can read data from a delimited text file into an ADO recordset and use the features of ADO to manipulate the data. You can then use the class as a data source in your application, binding controls on a form to fields in the recordset.

This topic shows how to create a data-aware class that reads data in a tab-delimited text file and provides methods for navigating through the data.

To create a data-aware class that reads data from a delimited text file

  1. Create a class that acts as a data source.

  2. Add code to read data from the text file into an ADO recordset.

  3. Set the data source for the class.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in a tab-delimited text file. It begins with the topic Interacting with Data in an ASCII Text File.

Create a Class that Acts as a Data Source

You can create a class that acts as a data source by inserting a class module in your project and specifying its data source behavior. First, insert a class module in your project by selecting Add Class Module from the Project menu. Then set the Name and DataSourceBehavior properties for the class.

For example, to create a CustomerDataSource class that can act as a data source, set the following properties:

Property | Setting
Name | CustomerDataSource
DataSourceBehavior | vbDataSource

For More Information Data-aware classes are covered in depth in Creating Data-Aware Classes in the Programmer's Guide.

Add Code to Read Data from the Text File into an ADO Recordset

By reading data from a text file into an ADO recordset, you can use the features of ADO to manipulate the data. First, add a reference to the ADO object library by selecting References on the Project menu, then selecting Microsoft ActiveX Data Objects 2.0 Library in the References dialog box.

Then declare a Recordset object variable in the Declarations section for the class. For example, to declare a Recordset object variable for working with customer records from the Customers.txt file, add the following to the Declarations section:

Public rsCustomers As ADODB.Recordset

By declaring the variable as a public variable, you can use the built-in methods of the Recordset object in applications that use the data-aware class.

Finally, add code to the Class_Initialize event procedure for the class to read data from the text file. For example, add the following code to the Class_Initialize event procedure for the CustomerDataSource class to read data from the Customers.txt file into a recordset:

Private Sub Class_Initialize()

   Dim fld As ADODB.Field
   Dim strRow As String
   Dim strField As String
   Dim intPos As Integer

   Set rsCustomers = New ADODB.Recordset

   With rsCustomers
      ' Set CustomerID as the primary key.
      .Fields.Append "CustomerID", adChar, 5, adFldRowID
      .Fields.Append "CompanyName", adChar, 40, adFldUpdatable
      .Fields.Append "ContactName", adChar, 30, adFldUpdatable
      .Fields.Append "ContactTitle", adChar, 30, adFldUpdatable
      .Fields.Append "Address", adChar, 60, adFldUpdatable
      .Fields.Append "City", adChar, 15, adFldUpdatable
      .Fields.Append "Region", adChar, 15, adFldMayBeNull
      .Fields.Append "PostalCode", adChar, 10, adFldMayBeNull
      .Fields.Append "Country", adChar, 15, adFldUpdatable
      .Fields.Append "Phone", adChar, 24, adFldUpdatable
      .Fields.Append "Fax", adChar, 24, adFldMayBeNull
      ' Use Keyset cursor type to allow updating records.
      .CursorType = adOpenKeyset
      .LockType = adLockOptimistic
      .Open
   End With

   Open "Customers.txt" For Input As #1

   Do Until EOF(1)
      Line Input #1, strRow
      With rsCustomers
         .AddNew
         For Each fld In .Fields
            ' If a tab delimiter is found, field text is to the
            ' left of the delimiter.
            If InStr(strRow, Chr(9)) <> 0 Then
               ' Move position to tab delimiter.
               intPos = InStr(strRow, Chr(9))
               ' Assign field text to strField variable.
               strField = Left(strRow, intPos - 1)
            Else
               ' If a tab delimiter isn't found, field text is the
               ' last field in the row.
               strField = strRow
            End If

            ' Strip off quotation marks.
            If Left(strField, 1) = Chr(34) Then
               strField = Left(strField, Len(strField) - 1)
               strField = Right(strField, Len(strField) - 1)
            End If

            fld.Value = strField

            ' Strip off field value text from text row.
            strRow = Right(strRow, Len(strRow) - intPos)
            intPos = 0

         Next
         .Update
         .MoveFirst
      End With
   Loop
   Close

End Sub

Set the Data Source for the Class

When you specify a class as a data source by setting its DataSourceBehavior to vbDataSource, Visual Basic automatically adds a GetDataMember event to the class. The Class_GetDataMember event procedure is where you set the data source for the class by assigning it to the Data object for the class.

For example, to set the rsCustomers recordset as the data source for the CustomerDataSource class, add the following to the Class_GetDataMember event procedure:

Private Sub Class_GetDataMember(DataMember As String, Data As Object)   Set Data = rsCustomersEnd Sub

For More Information For a discussion of data sources, see Creating a Data Source in the Programmer's Guide.

Step by Step

This topic is part of a series that walks you through using a data-aware class and ADO to create a simple database application that interacts with data in a tab-delimited text file.

To | See
Go to the next step | Creating a Form that Lets You View and Update Data from a Data-Aware Class
Start from the beginning | Interacting with Data in an ASCII Text File


Creating a Form that Lets You View and Update Data from a Data-Aware Class

Once you've created a class that can act as a data source, you can easily create applications that let you view and update records from the data source. You can use ActiveX Data Objects (ADO) and the BindingsCollection object to bind the data source to controls on the form, and add command buttons to navigate through records.

This topic shows how to create a form that lets you view and edit customer address records from the data-aware class you created in the previous topic.

To create a form that lets you view and update data from a data-aware class

  1. Add text box and label controls to a form.

  2. Add code to bind the text box controls to the data source.

  3. Add command button controls to navigate through records.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in a tab-delimited text file. It begins with the topic Interacting with Data in an ASCII Text File.

Add Text Box and Label Controls to a Form

The first step in viewing and updating data from a data-aware class is to create an interface for interacting with records from the data source. The easiest way to create an interface is to open a new Standard EXE project, then add TextBox and Label controls to a form.

For example, you can create an interface for viewing customer address information from the Customers.txt file. First, add a "Customer ID:" label to the form, then add a text box control next to the label and set its Name property to txtCustomerID. Repeat the same process for CompanyName, Address, City, Region, PostalCode, and Country controls.

Add Code to Bind the Text Box Controls to the Data Source

Using a data-aware class, ActiveX Data Objects (ADO), and the BindingCollection object, you can bind controls to a data source when the page loads. You can then edit the data and add code to navigate through the records. For example, you can use an instance of your data-aware class and the BindingCollection object to bind the text box controls on your form to fields from the Customers.txt file.

First, add a reference to the BindingCollection object's type library to your project. To add the reference, select References on the Project menu, then select Microsoft Data Binding Collection in the References dialog box.

Then declare variables for the data-aware class and a BindingCollection object in your form's Declarations section:

Private objDataSource As CustomerDataSource
Private colBind As BindingCollection

To bind the text box controls to fields from the Customers.txt file when the form loads, add code to the form's Load event procedure. An instance of the CustomerDataSource class reads records from the text file into an ADO recordset, and the BindingCollection object binds the text box controls to fields in the recordset:

Private Sub Form_Load()

   Set objDataSource = New CustomerDataSource
   Set colBind = New BindingCollection

   Set colBind.DataSource = objDataSource
   colBind.Add txtCustomerID, "Text", "CustomerID"
   colBind.Add txtCompanyName, "Text", "CompanyName"
   colBind.Add txtAddress, "Text", "Address"
   colBind.Add txtCity, "Text", "City"
   colBind.Add txtRegion, "Text", "Region"
   colBind.Add txtPostalCode, "Text", "PostalCode"
   colBind.Add txtCountry, "Text", "Country"

End Sub

Add Command Button Controls to Navigate through Records

By binding controls on your form to a public recordset in your data-aware class, you can easily create Next and Previous buttons that let you navigate through records. Each command button requires a single line of code.

For example, to create a Next button for the form that displays customer records, add a command button to the form and change its Caption and Name properties to Next. Then add the following line to the command button's Next_Click event procedure:

objDataSource.rsCustomers.MoveNext

The code uses the MoveNext method of the rsCustomers recordset that serves as the data source for the form's controls. It refers to the recordset as a property of the object variable that represents an instance of the CustomerDataSource class.

Similarly, you can create Previous, First, and Last buttons by adding command buttons to the form and changing their Caption and Name properties to Previous, First, and Last, respectively. Then add code to the Click event procedure for each command button that invokes the MovePrevious, MoveFirst, and MoveLast methods.

When you run the form, Visual Basic lets you view and update records from the Customers.txt file and lets you navigate through the recordset.

Step by Step

This topic is part of a series that walks you through using a data-aware class and ADO to create a simple database application that interacts with data in a tab-delimited text file.

To | See
Go to the next step | Modifying the Form to Let You Add New Records
Start from the beginning | Interacting with Data in an ASCII Text File


Modifying the Form to Let You Add New Records

In addition to viewing and updating existing records, you may want to add new records to your data source. You can modify your form so that it can serve as a data entry form by creating command buttons that use ActiveX Data Objects (ADO) and the BindingsCollection object to clear the display of data, manage the data binding of controls, and add a new record to the underlying recordset.

This topic shows how to modify the form you created in the previous topic so that it also serves as a data entry form for adding new customer records.

To modify the form to let you add new records

  1. Create command buttons for allowing data entry, adding new customer records, and returning to viewing data.

  2. Add code to enable data entry.

  3. Add code to enable saving the data you enter as a new record.

  4. Add code to enable returning the form to viewing data.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in a tab-delimited text file. It begins with the topic Interacting with Data in an ASCII Text File.

Create Command Buttons for Allowing Data Entry, Adding New Customer Records, and Returning to Viewing Data

The first step in modifying the form is to create the interface for the tasks that you want to accomplish. For example, to allow data entry on the customer address form you created in the previous topic, you could add the following:

  • A DataEntry command button to clear the existing data displayed on the form and disable data binding.

  • An AddCustomer command button to add new data entered on the form as a new record in the underlying recordset.

  • A ViewData command button to re-enable data binding, returning the form to its original state.

First, add a command button to the form and change its Caption and Name properties to DataEntry. Add a second command button to the form and change its Caption and Name properties to AddCustomer. Then add a third command button to the form and change its Caption and Name properties to ViewData.

Because the AddCustomer and ViewData command buttons should only be displayed when the form is being used for data entry, set the Visible property for these controls to False.

Add Code to Enable Data Entry

You can make a data-bound form also serve as a data entry form by disabling data binding and clearing the existing data displayed on the form. You may also want to show hidden command button controls that apply only to adding new records, and hide command button controls that apply only to viewing existing records.

For example, to enable the DataEntry command button on the customer address form, add the following code to the DataEntry_Click event procedure:

Private Sub DataEntry_Click()
   ' Disable data binding.
   Set colBind = Nothing

   ' Clear the text box controls.
   Me.txtCustomerID = ""
   Me.txtCompanyName = ""
   Me.txtAddress = ""
   Me.txtCity = ""
   Me.txtRegion = ""
   Me.txtPostalCode = ""
   Me.txtCountry = ""

   ' Hide the command buttons used for viewing
   ' existing data.
   Me.Next.Visible = False
   Me.Previous.Visible = False
   Me.First.Visible = False
   Me.Last.Visible = False
   Me.DataEntry.Visible = False

   ' Show the command buttons used for entering new data.
   Me.AddCustomer.Visible = True
   Me.ViewData.Visible = True

End Sub

Add Code to Enable Saving the Data You Enter as a New Record

After you've entered record data in a data entry form, you can use ADO to add the record to a recordset. For example, to enable the AddCustomer command button on the customer address form, add the following code to the AddCustomer_Click event procedure:

Private Sub AddCustomer_Click()

   ' Add the record to the rsCustomers recordset
   ' in your data-aware class.
   With objDataSource.rsCustomers
      .AddNew
      !CustomerID = Me.txtCustomerID.Text
      !CompanyName = Me.txtCompanyName.Text
      !Address = Me.txtAddress.Text
      !City = Me.txtCity.Text
      !Region = Me.txtRegion.Text
      !PostalCode = Me.txtPostalCode.Text
      !Country = Me.txtCountry.Text
      .Update

   End With

   ' Clear the controls for additional data entry,
   ' if desired.
   Me.txtCustomerID = ""
   Me.txtCompanyName = ""
   Me.txtAddress = ""
   Me.txtCity = ""
   Me.txtRegion = ""
   Me.txtPostalCode = ""
   Me.txtCountry = ""

End Sub

Add Code to Enable Returning the Form to Viewing Data

When you've finished using your form as a data entry form, you can return it to its original use for viewing and editing existing records by re-enabling data binding. Any new records you've entered will now be displayed when you move through records on the form. You can also hide command button controls that apply only to adding new records, and show hidden command button controls that apply to viewing existing records.

For example, to enable the ViewData command button on the customer address form, add the following code to the ViewData_Click event procedure:

Private Sub ViewData_Click()

   ' Bind text box controls to the data source
   ' of your data-aware class.
   Set colBind = New BindingCollection

   Set colBind.DataSource = objDataSource
   colBind.Add txtCustomerID, "Text", "CustomerID"
   colBind.Add txtCompanyName, "Text", "CompanyName"
   colBind.Add txtAddress, "Text", "Address"
   colBind.Add txtCity, "Text", "City"
   colBind.Add txtRegion, "Text", "Region"
   colBind.Add txtPostalCode, "Text", "PostalCode"
   colBind.Add txtCountry, "Text", "Country"

   ' Show the command buttons used for viewing
   ' existing data.
   Me.Next.Visible = True
   Me.Previous.Visible = True
   Me.First.Visible = True
   Me.Last.Visible = True
   Me.DataEntry.Visible = True

   ' Hide the command buttons used for entering new data.
   Me.AddCustomer.Visible = False
   Me.ViewData.Visible = False

End Sub

For More Information To read about the BindingCollection object, see Creating Data-Aware Classes in "Programming with Objects" in the Programmer's Guide.

Step by Step

This topic is part of a series that walks you through using a data-aware class and ADO to create a simple database application that interacts with data in a tab-delimited text file.

To | See
Go to the next step | Modifying the Class and Form to Write Records Back to the Delimited Text File
Start from the beginning | Interacting with Data in an ASCII Text File


Modifying the Class and Form to Write Records Back to the Delimited Text File

After you've updated records or added new records, you can write the changes back to the delimited text file that serves as the data source for your data-aware class. You can add a public method to the class that writes records to a file, then invoke the method in your applications.

This topic shows how to modify the CustomerDataSource class to provide a public method to write records back to the Customers.txt file, and how to invoke the method on your customer address form.

To modify the class and form to write all records back to the delimited text file

  1. Create a public method in the class that writes records to a file.

  2. Create a command button on your form that writes records to a file.

Note This topic is part of a series that walks you through creating a simple database application that interacts with data in a tab-delimited text file. It begins with the topic Interacting with Data in an ASCII Text File.

Create a Public Method in the Class that Writes Records to a File

By adding Sub procedures to your class, you can provide public methods to applications that use your class as a data source. For example, you can create a public method in your CustomerDataSource class that writes current records from the rsCustomers recordset to the Customers.txt delimited text file. The text file will then include any changes or additions you've made to the recordset.

To create a public WriteToFile method, add the following code to the CustomerDataSource class:

Public Sub WriteToFile()

   Dim fld As ADODB.Field
   Dim strRow As String
   Dim strField As String

   Open "Customers.txt" For Output As #1

   With rsCustomers

      .MoveFirst
      Do While Not .EOF
         For Each fld In .Fields
            ' If a field has a value, add quotation marks.
            If Len(fld.Value) > 0 Then
               strField = Chr(34) & fld.Value & Chr(34)
            Else
               strField = ""
            End If
            ' Add the field value and a tab delimeter
            ' to the output string.
            strRow = strRow & strField & Chr(9)
         Next
         ' Strip off the end tab character.
         strRow = Left(strRow, Len(strRow) - 1)
         ' Print the output string.
         Print #1, strRow
         strRow = ""
      .MoveNext
      Loop
   End With
   Close

End Sub

Create a Command Button on Your Form that Writes Records to a File

Once you've created a public method in your data-aware class, you can use it in any application that requires the same functionality. For example, by creating a public method for writing records to a delimited text file, you can easily create a WriteToFile button on your customer address form. The command button requires a single line of code.

To create a WriteToFile button, add a command button to the form and change its Caption and Name properties to WriteToFile. Then add the following line to the command button's WriteToFile_Click event procedure:

objDataSource.WriteToFile

The code uses the WriteToFile method you created for the CustomerDataSource class.

You may want to hide the WriteToFile button while users enter data on the form. To hide the command button during data entry, add the following code to the DataEntry_Click event procedure:

Me.WriteToFile.Visible = False

And if you hide the WriteToFile button while users enter data, make it visible again while users view data. To do so, add the following code to the ViewData_Click event procedure:

Me.WriteToFile.Visible = True

Step by Step

This topic concludes a series that walks you through creating a simple database application that interacts with data in a tab-delimited text file. To start from the beginning, see Interacting with Data in an ASCII Text File.

For More Information For information on data sources, see Creating Data Sources in the Component Tools Guide.


Converting from RDO to ADO

Converting from RDO 2.0 to ADO 2.0

One of Microsoft's newest data access technologies is ActiveX Data Objects (ADO). ADO is a replacement for the older DAO and, in particular, RDO data access interfaces, and gives you additional features not found in either.

This chapter assumes that you're currently using RDO for data access and that you're considering migrating to ADO, and it provides you with information on how to convert your application to use ADO.

Topics

  • ADO Compared with RDO and DAO

  • How to Reference ADO 2.0 in Visual Basic

  • The ADO 2.0 Object Model

  • RDO Versus ADO in Common Data Access Scenarios

  • Advanced RDO/ADO Data Access Scenarios


ADO Compared with RDO and DAO

ADO isn't automatically code-compatible with your existing data access applications. While ADO encapsulates the functionality of DAO and RDO, you must convert many of the language elements over to ADO syntax. In some cases, this will mean only a simple conversion of some functions of your existing code. In other cases, it might be best to rewrite the application using ADO's new features.

DAO (Data Access Objects) was the first object-oriented interface that exposed the Microsoft Jet database engine (used by Microsoft Access) and allowed Visual Basic developers to directly connect to Access tables - as well as other databases - through ODBC. DAO is suited best for either single-system applications or for small, local deployments.

RDO (Remote Data Objects) is an object-oriented data access interface to ODBC combined with the easy-to-use style of DAO, providing an interface that exposes virtually all of ODBC’s low-level power and flexibility. RDO is limited, though, in that it doesn't access Jet or ISAM databases very well, and that it can access relational databases only through existing ODBC drivers. However, RDO has proven to be the interface of choice for a large number of SQL Server, Oracle, and other large relational database developers. RDO provides the objects, properties, and methods needed to access the more complex aspects of stored procedures and complex resultsets.

ADO is the successor to DAO/RDO. Functionally ADO 2.0 is most similar to RDO, and there's generally a similar mapping between the two models. ADO "flattens" the object model used by DAO and RDO, meaning that it contains fewer objects and more properties, methods (and arguments), and events. For example, ADO has no equivalents to the rdoEngine and rdoEnvironment objects, which exposed the ODBC driver manager and hEnv interfaces. Nor can you currently create ODBC data sources from ADO, despite the fact that your interface might be through the ODBC OLE DB service provider.

Much of the functionality contained in the DAO and RDO models was consolidated into single objects, making for a much simpler object model. Because of this, however, you might initially find it difficult to find the appropriate ADO object, collection, property, method, or event. Unlike DAO and RDO, although ADO objects are hierarchical, they are also creatable outside the scope of the hierarchy.

It should be noted, however, that ADO currently doesn't support all of DAO's functionality. ADO mostly includes RDO-style functionality to interact with OLE DB data sources, plus remoting and DHTML technology.

In general, it's probably too early in the evolution of ADO to migrate most DAO applications (except possibly ones using ODBCDirect) to ADO right now, since ADO doesn't currently support data definition (DDL), users, groups, and so forth. If you use DAO only for client-server applications and don't rely on the Jet database engine or use DDL, however, then you can probably migrate to ADO now. Eventually, Microsoft will provide an ADO DDL component to aid in DAO-to-ADO migration and provide generic DDL support for OLE DB providers.


How to Reference ADO in Visual Basic

How to Reference ADO 2.0 in Visual Basic

To gain access to the ADO 2.0 objects in Visual Basic, set a reference to the appropriate ADO type library. There are two ADO type libraries. One is called ADODB and is contained in MSADO15.DLL. It appears in the References dialog box (available from the Project menu) as "Microsoft ActiveX Data Objects 2.0 Library". The other is called ADOR and is contained in MSADOR15.DLL. It appears in the References dialog as "Microsoft ActiveX Data Objects Recordset 2.0 Library".

The first type library (ADODB) is the larger and more feature-rich of the two; it contains the main ADO objects and is the one you'll probably want to use in most circumstances. The second is a "lightweight" subset of the ADODB type library that supports only recordsets. You may choose to use that library instead if you plan to manipulate only recordsets.


The ADO Object Model

The ADO 2.0 Object Model

The ADO 2.0 object model is composed of eight objects, most of them similar in functionality to RDO objects, only with enhanced functionality. Spend some time browsing the object model in the Object Browser (F2) to become familiar with the location of the various properties, methods, events, collections, and so forth.

Note All objects followed by an asterisk (*) are objects that also apply to the ADO Recordset type library (ADOR).

  • Command object Contains information about a command, such as a query string, parameter definition, and so forth. The Command object is similar in functionality to RDO's rdoQuery object.

  • Connection object Contains information about a data provider. The Connection object is similar in functionality to RDO's rdoConnection object, and it contains the information on schema. It also contains some of the functionality of the RDOEnvironment object, such as transaction control.

  • Error object Contains extended information when an error occurs with a data provider. The Error object is similar in functionality to RDO's rdoError object. In comparison to RDO, however, the Errors collection is on the Connection object, whereas the rdoErrors collection is on the rdoEngine object in RDO 2.0.

  • Field object* Contains information about a single column of data in a recordset. The Field object is similar in functionality to RDO's rdoColumn object.

  • Parameter object Contains a single parameter for a parameterized Command object. The Command object has a Parameters collection to contain all of its Parameter objects. The Parameter object is similar in functionality to RDO's rdoParameter object.

  • Property object* Contains a provider-defined characteristic of an ADO object. There is no RDO equivalent to this object, but DAO has a similar object. ADO objects can have two kinds of properties:
    • Built-In Properties: Properties which are "native" to ADO. That is, properties in ADO that are immediately available to any new object using the familiar MyObject.Property syntax. Built-in properties do not appear as Property objects in an object’s Properties collection, so while you can change their values, you can't modify their characteristics or delete them.

    • Dynamic properties: Properties which are not native to ADO and are defined by the underlying data provider. They appear in the Properties collection of the appropriate ADO object.

      For example, a property specific to the data provider may indicate if a Recordset object supports transactions or updating. These additional properties appear as Property objects in the Recordset’s Properties collection. Dynamic properties can be referred to only through the collection using the MyObject.Properties(0) or MyObject.Properties("Name") syntax. Different data providers may implement one or more special properties to deal with special provider-specific operations.

  • Recordset object* The Recordset object contains records returned from a query as well as a cursor into those records. The Recordset object is similar in functionality to RDO's rdoResultset object. You can open a Recordset (for example, perform a query) without explicitly opening a Connection object. If, however, you choose to create a Connection object, you can open multiple Recordset objects on the same connection.


RDO Versus ADO in Common Data Access Scenarios

Three basic data access scenarios are listed below, and each one is discussed in a succeeding topic. In each scenario, an RDO approach to the problem is shown, followed by an ADO approach.

  • Establishing a Connection to a Database

  • Running a Basic Query

  • Displaying a Resultset in an MSHFlexGrid control

Note Some of these examples refer to additional controls on a form, such as an MSHFlexGrid control or a TextBox control. Add these controls to your project to make the projects work correctly.


Establishing a Connection to a Database

RDO

To open a connection, you must supply a connection string with parameters. Note that a connection is not required by RDO to create an rdoQuery object, but is required to initially create an rdoResultset object:

Dim cn As New rdoConnection
Dim cnB As New rdoConnection
Const ConnectionString = "uid=myname;pwd=mypw;driver={SQLServer}; _
 server=myserver;database=pubs;dsn=''"

This connect string accesses a specific SQL Server and permits ODBC to open a connection without a DSN. This is a typical ODBC connect string with all of the standard arguments.

The next section, in the form's Load event, establishes the type of cursor driver and the login timeout. By default, RDO uses the rdUseIfNeeded cursor type, which invokes server-side cursors on SQL Server. This default is overridden in the example below by specifying rdUseNone. The rdDriverNoPrompt flag means that the application generates an error if the user ID and password do not match.

Private Sub Form_Load()
   With cn
      cn.Connect = ConnectString
      cn.LoginTimeout = 10
      cn.CursorDriver = rdUseNone
      cn.EstablishConnection rdDriverNoPrompt
   End With

This second connection performs any client-batch updates:

   With cnB
      cnB.Connect = ConnectString
      cnB.CursorDriver = rdUseClientBatch
      cnB.EstablishConnection
   End With
End Sub

The last event occurs when the connection operation completes and it handles any errors that occur when the connection is opened. With it, you can test to see if the connection worked, and if so, enable any buttons that rely on an open connection.

Private Sub cn_Connect(ByVal ErrorOccurred As Boolean)
   If ErrorOccurred Then
      MsgBox "Could not open connection", vbCritical
   Else
      RunOKFrame.Enabled = True
   End If
End Sub

ADO

To establish a database connection in ADO, first create a set of ADO objects referenced from the ADODB object. These are used later to set specific properties that open connections and generate resultsets:

Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim cnB As New ADODB.Connection
Dim Qy As New ADODB.Command

The next line creates a connect string, just like the one you created in the previous RDO example. In both cases, you are using ODBC’s "non-DSN" connection strategy to save time and increase performance:

Const ConnectionString = "uid=myname;pwd=mypw;driver={SQL Server}; _
 server=myserver;database=pubs;dsn=",,connection=adConnectAsync"

The following declarations initialize the variables used in this example. (Note the creation of a variant array to hold the resultset):

Dim sql As String
Dim rc As Integer
Dim i As Integer
Dim Changes As Integer
Dim bms() As Variant

Next, open an ADO connection to a database in the Form_Load event. Note that this code is very similar to the RDO code except that the constants are prefaced with "ad" rather than "rd". To see all available constants, look at the ADODB type library.

Note There's no need to specify the prompting behavior since ADO defaults to "no prompt". If you elect to change this, however, use the ADO Properties collection to deal with the desired prompt behavior. In RDO, you can set the behavior using the OpenConnection argument. In ADO, you must set the Properties ("Prompt") property.

Also, there's no need to specify a cursor driver if you don't want to use one (such as the RDO CursorDriver = rdUseNone), since ADO defaults to no cursor driver by default.

Private Sub Form_Load()
   With cn
      ' Establish DSN-less connection
      .ConnectionString = ConnectString
      .ConnectionTimeout = 10
      '.Properties("Prompt") = adPromptNever
      ' This is the default prompting mode in ADO.
      .Open
   End With
   With cnB
      .ConnectionString = ConnectString
      .CursorLocation = adUseClient
      .Open
   End With
End Sub


Running a Basic Query

RDO

This event procedure returns a resultset based on an SQL statement. It performs a restricted query and passes the resultset to a control that inserts the resultant data into an MSHFlexGrid control. Note that building a resultset requires an open connection.

Private Sub RunButton_Click()
   Dim rs As rdoResultset
   Set rs = cn.OpenResultset("select * from titles where title _
    like '%h'")
   rdoGrid1.ShowData rs
   rs.Close
End Sub

ADO

Once the database connection is open, you can run a query against it. The following event procedure is very similar to the previous RDO code. In this case, however, you use the new ADO Open method that takes the SQL query and the ADO Connection object as arguments, rather than using the rdoConnection object's OpenResultset method. You can also opt to use the ADO Connection object’s Execute method, just as you could in RDO (as long as it didn’t return a rowset).

A primary difference of ADO2 compared with RDO2 is that ADO2 allows you to create a recordset and set its properties before you open the recordset.

Private Sub RunButton_Click()
   Dim rs As New ADODB.Recordset
   rs.Open "select * from titles where title like '%h'", cn
   ADOGrid1.ShowData rs
   rs.Close
End Sub

You can run this query and process its resultset asynchronously in ADO. That is, when you specify the adFetchAsynch option on rs.Open, ADO causes the cursor provider to automatically populate the resultset in the background.


Displaying a Resultset in an MSHFlexGrid Control

RDO

The following code uses the ShowData method of a custom ActiveX control to display data from a resultset in an MSHFlexGrid control. The code sets up the grid based on the names in the rdoColumns property and initializes the grid, preparing it for the data. Note the use of the OrdinalPosition property to index the resultset's rdoColumns property.

There are two sets of code to extract data from the rdoResultset, one that uses GetClipString, and another that uses the GetRows method. This helps you compare different approaches to the situation.

Public Function ShowData(Resultset As rdoResultset) As Variant
   Dim cl As rdoColumn
   Static GridSetup As Boolean
   Dim MaxL As Integer
   Dim rsl As rdoResultset
   Dim Rows As Variant
   On Error GoTo ShowDataEH
   Set rsl = Resultset
   If GridSetup Then
      FGrid1.Rows = 51
      FGrid1.Cols = rsl.rdoColumns.Count
      FGrid1.Row = 0
      For Each cl In rsl.rdoColumns
         FGrid1.Col = cl.OrdinalPosition - 1
         FGrid1 = cl.Name
         If rsl.rdoColumns(cl.OrdinalPosition - 1).ChunkRequired Then
            MaxL = 1
         Else
            MaxL = rsl.rdoColumns(cl.OrdinalPosition - 1).Size + 4
         End If
         If MaxL > 20 Then MaxL = 20
         FGrid1.ColWidth(FGrid1.Col) = TextWidth(String(MaxL, "n"))
      Next cl
      GridSetup = True
   End If
   FGrid1.Rows = 1     'Clear Grid of data (except titles)
   FGrid1.Rows = 51
   FGrid1.Row = 1
   FGrid1.Col = 0
   FGrid1.RowSel = FGrid1.Rows - 1
   FGrid1.ColSel = FGrid1.Cols - 1
   FGrid1.Clip = rsl.GetClipString(50, , , "-")

ExitShowData:
   FGrid1.RowSel = 1
   FGrid1.ColSel = 0
   Exit Function

ShowDataEH:
   Select Case Err
      Case 40022:
         FGrid1.Clear
         Resume ExitShowData
      Case 13
         FGrid1.Text = "< >"
         Resume Next
      Case Else
         MsgBox "Could not display data: " & Err & vbCrLf & Error$
         Resume ' ExitShowData
   End Select
End Function

ADO

The following code implements the ShowData method of a custom ActiveX control adapted from an RDO control. Note that the RDO GetClipString method is superseded in ADO by the GetString method. Since you then have to parse the resulting Variant array, the routine is noticeably slower. If you have problems getting the GetRows function to work, you should convert to a more conservative (and slower) approach that loops through the rows. However, you should avoid using this technique if possible because it is far less efficient than using GetRows or binding directly to a control.

Note how the OrdinalPosition property can no longer be used as an index on the Fields collection to pull out the column titles as you could in RDO. To handle this, substitute a new integer counter to address the column being manipulated. Use the DefinedSize and ActualSize properties to find the TEXT and IMAGE datatype fields that would not fit in a column. These new properties make it easier to determine the details of specific field values. Also added is code to deal with BLOB types if they're encountered while working through the data columns.

Public Function ShowData(Resultset As Recordset) As Variant
   Dim cl As Field
   Static GridSetup As Boolean
   Dim MaxL As Integer
   Dim Op As Integer
   Dim rsl As Recordset
   Dim rows As Variant
   On Error GoTo ShowDataEH
   Set rsl = Resultset

   If GridSetup Then
      Else
      FGrid1.rows = 51
      FGrid1.Cols = rsl.Fields.Count
      FGrid1.Row = 0
      Op = 0
      For Each cl In rsl.Fields
         FGrid1.Col = Op
         FGrid1 = cl.Name
         If rsl.Fields(Op).DefinedSize > 255 Then
            MaxL = 1
         Else
            MaxL = rsl.Fields(Op).ActualSize + 4
         End If
         If MaxL > 20 Then MaxL = 20
         FGrid1.ColWidth(FGrid1.Col) = TextWidth(String(MaxL, "n"))
         Op = Op + 1
      Next cl
      GridSetup = True
   End If
   FGrid1.rows = 1
   FGrid1.rows = 51
   FGrid1.Row = 1
   FGrid1.Col = 0
   FGrid1.RowSel = FGrid1.rows - 1
   FGrid1.ColSel = FGrid1.Cols - 1
   With FGrid1
   ' You can also use the ADO2 GetString method here in lieu of the
   ' following.
        FGrid1.Clip = rsl.GetString(adClipString, 50, , , "-")
   End With

ExitShowData:
   FGrid1.RowSel = 1
   FGrid1.ColSel = 0
   Exit Function

ShowDataEH:
   Select Case Err
      Case 40022, 3021:
         FGrid1.Clear
         Resume ExitShowData
      Case 13, Is < 0
         rows(j, i) = "< >"
         Resume 'Next
      Case Else
         MsgBox "Could not display data: " & Err & vbCrLf & Error$
         Resume ' ExitShowData
   End Select
End Function


Advanced RDO/ADO Data Access Scenarios

Following are five data access scenarios that are more complex than simple Select queries. Each one is discussed in a succeeding topic. First, an RDO approach to the problem is shown, followed by the ADO solution so that you can compare how each is accomplished.

  • Executing a Parameter Query

  • Performing a Parameter-Driven Stored Procedure

  • Running a Stored Procedure That Returns Multiple Resultsets

  • Performing an Action Query

  • Running an Optimistic Batch Query


Performing a Parameter Query

Executing a Parameter Query

RDO

The procedure below illustrates a method for performing a parameterized Select query: that is, a SELECT statement that requires one or more parameters. This process is done in two steps:

  1. Build a query that expects a parameter, pass in the first parameter, then perform the query.

  2. Perform the query again with new parameters.

The first time the query is called, RDO attempts to create a new RDO query object. Since the object is appended to the rdoConnection object’s rdoQueries collection, you can reference it each time the procedure is called. Each subsequent time the procedure is called, the Refresh method re-executes the query. This technique builds a temporary stored procedure (SP) behind the scenes that is referenced by the Requery method. The temporary SP is dropped when the connection is closed.

Private Sub ParmQueryButton_Click()
   Dim Qy As New rdoQuery
   Dim rs As rdoResultset
   Static FirstTime As Boolean
   If cn.rdoQueries.Count = 0 Then
      FirstTime = True
      sql = "select * from authors where year_born = ?"
      Set Qy = cn.CreateQuery("Pq", sql)
   End If
   Qy(0) = QueryParam.Text
   If FirstTime Then
      Set rs = Qy.OpenResultset()
      FirstTime = False
   Else
      rs.Requery
   End If
   rdoGrid1.ShowData rs
   rs.Close
End Sub

ADO

This procedure is designed to perform a table-access query that accepts a parameter. You use the "?" character (as in the previous RDO example) to indicate where the parameter is to be placed. In this case, though, you don't create an rdoQuery object that is kept in a collection off the rdoConnection object; you instead use a stand-alone ADO Command object created (and scoped) earlier. The first time through, you set up the Command properties, and each time thereafter, you simply execute the command after having changed the parameter.

ADO gives you a lot of flexibility here—more, in some cases than RDO. If you tell ADO everything it needs to know about a query, it won't have to perform informational queries against the database to get missing information, so queries run faster.

Note You don’t have to build the ADO Parameters collection in code, since it's automatically created for you just like when you use RDO. However, it is possible to do so, and doing so can improve performance, at the cost of a little code complexity. If you elect to do it, make sure that the Command is associated with an open connection so ADO can query the service provider (and the server) for the parameter's description.

To run the query and create the resultset, use the Execute method on the Command object.

Private Sub ParmQueryButton_Click()
   If Cmd.CommandText = "" Then
      Cmd.ActiveConnection = cn
      With Cmd
         .CommandText = "select * from authors where year_born = ?"
         .CommandType = adCmdText
         .CommandTimeout = 15
      End With
'
'   The following section of code is not required,
'    but can make execution faster. It eliminates the need
'   for ADO to fetch the parameter metrics from the server.
'
      With Parm
         .Type = adInteger
         .Size = 4
         .Direction = adParamInput
         .Value = QueryParam.Text
         Cmd.Parameters.Append Parm
      End With
   End If

   Cmd.Parameters(0).Value = QueryParam.Text
   Set rs = Cmd.Execute()
   ADOGrid1.ShowData rs
   rs.Close
End Sub


Performing a Parameter-Driven Stored Procedure

RDO

Since many client/server applications depend heavily on stored procedures (SP), a data access interface should be able to perform them quickly and efficiently. Stored procedures, however, can be tricky to handle. In some cases, stored procedures require management of OUTPUT and return status values and other, more conventional arguments. In addition, an SP can return several complex resultsets, including PRINT or RAISERROR statement return values. In some cases, it's better to create complex SPs, while in others it’s better and easier to keep SPs simpler and more modular and use Visual Basic to tie them together.

The code below shows the RDO approach to these problems. First, it performs a simple parameter-based SP and shows the results in the grid. Some accommodations are made to the subsequent ADO design, and these are noted. It uses the same connections established in the earlier examples.

Note that the code requires us to include a correct ODBC "Call" statement. Again, this is not necessary in the UserConnection designer, but it’s essential in the RDO code-based approach. Here, you use the stand-alone rdoQuery object and assign the already open Connection to it. The rdoQuery object can then be used in subsequent calls to handle a parameter query.

Note also that the code does not attempt to refer to the return status argument. This value is not available until the resultset is fully populated; only then does SQL Server return this value.

Private Sub RunSPButton_Click()
   Dim Qy As New rdoQuery
   Dim rs As rdoResultset
   sql = "{? = Call AuthorByYearBorn (?,?)}"
   Set Qy.ActiveConnection = cn
   Qy.sql = sql
   Qy.rdoParameters(0).Direction = rdParamReturnValue
   Qy(1) = "1947"
   Qy(2) = "1948"
   Set rs = Qy.OpenResultset()
   rdoGrid1.ShowData rs
   ShowRows = rs.RowCount
   rs.Close
End Sub

ADO

ADO has a lot of flexibility when it comes to performing stored procedures. But this flexibility comes at the cost of more code. As with the previous example ("Performing a Parameter Query"), it's possible to build your own ADODB Parameters collection. In this case, you’re performing a simple two-argument SP, "AuthorByYearBorn", that returns a small resultset. Note that although ADO allows you to create your own parameters, it's not necessary to do so.

Note ADO collections are 0-based to match DAO. RDO collections are 1-based.

Private Sub RunSPButton_Click()
   Dim Qy As New ADODB.Command
   Dim Parm As New ADODB.Parameter
   Dim Parm2 As New ADODB.Parameter

   Set Qy.ActiveConnection = cn
   Qy(0)="1947"
   Qy(1)="1948"
   Qy.CommandType = adCmdStoredProc
   Qy.CommandText = "AuthorByYearBorn"

   Set rs = Qy.Execute(ShowRows)
   ADOGrid1.ShowData rs
End Sub


Performing a Stored Procedure That Returns Multiple Resultsets

Running a Stored Procedure That Returns Multiple Resultsets

RDO

This example illustrates how to perform a query that returns more than one resultset. It’s common for a stored procedure to return more than a single set of rows or a resultset that contains results from an action query. As a result, your code must deal with each of the resultsets individually, or else you won’t be able to use the product of your query. In RDO, you use the MoreResults method to step through the resultsets one at a time. Each call to MoreResults closes the current resultset and moves to the next (if there is one).

Private Sub MultipleRSButton_Click()
   sql = "Select * from Authors Where year_born is not null; " _
    & "Select * from Authors where year_born is  null"
   Set rs = cn.OpenResultset(sql)
   rdoGrid1.ShowData rs

   i = MsgBox("Ready for next results?", vbYesNoCancel)
   If i = vbYes Then
      If rs.MoreResults Then
         rdoGrid1.ShowData rs
      End If
   End If
End Sub

ADO

The following code illustrates how to handle SPs that return multiple resultsets in ADO. ADO’s approach is different from the RDO approach in that ADO uses the NextRecordset method in which you assign the next recordset in the batch to an ADO Recordset object. The next recordset read doesn't overwrite the first one, as it does in RDO. ADO also allows for multiple recordsets, if the data provider supports it.

Private Sub MultipleRSButton_Click()
   Dim rs As New ADODB.Recordset
   sql = "Select * from Authors Where year_born is not null; " _
    & "Select * from Authors where year_born is  null"
   rs.Open sql, cn
   Do
      i = MsgBox("Ready for results?", vbYesNoCancel)
      If i = vbYes Then
         ADOGrid1.ShowData rs
         Set rs = rs.NextRecordset
      End If
   Loop Until rs.State = adStateClosed
End Sub


Performing an Action Query

RDO

In case your application needs to manipulate tables directly, or perform a maintenance operation (like SQL Server’s DBCC functions), you can use the Execute method to run the query directly. In this case, you don’t need ODBC or SQL Server to create a temporary SP to run the query as it’s only being done once. (If this were a regular operation, it would be more efficient to create an SP to do it.) Note that you can use the RowsAffected property to find out the number of rows affected by this query.

Private Sub ExecuteButton_Click()
   sql = "Begin Transaction " _
    & " Update Authors " _
    & " set Year_Born = 1900 where year_born is null" _
    & " rollback transaction"
   Screen.MousePointer = vbHourglass
   cn.Execute sql, rdExecDirect
   ShowRows = cn.RowsAffected
   Screen.MousePointer = vbDefault
End Sub

ADO

When you need to perform an action query, you can take advantage of the Execute method in ADO. In this case, you have to set a few more properties than you do in RDO, but these properties improve data access performance. This is because ADO doesn’t have to poll the server to determine what to do, or how to handle the query. Note that the new output argument for the Execute method returns the number of rows affected. Generally, you don’t see Visual Basic using arguments passed back to the application; just arguments passed to the object interface.

Private Sub ExecuteButton_Click()
   Dim Qy As New ADODB.Command
   Dim Rows As Long
   sql = "Begin Transaction " _
    & " Update Authors " _
    & " set Year_Born = 1900 where year_born is null" _
    & " rollback transaction"
   Qy.ActiveConnection = cn
   Qy.CommandText = sql
   Qy.CommandType = adCmdText
   Qy.Execute Rows
   MsgBox Rows & " rows would have been affected", vbInformation
End Sub


Performing an Optimistic Batch Query

Running an Optimistic Batch Query

RDO

The following code demonstrates a query that can be used to drive a subsequent "optimistic batch update" operation. In this case, you fetch a resultset using the ClientBatch cursor library and save the bookmarks for each row fetched. When the user chooses a row in the grid (where the rows are displayed), the code prompts the user for a new value and writes it to the resultset. The changes are not made to the data, however, until you perform the BatchUpdate method.

Private Sub BatchOpsButton_Click()
   Dim rs As rdoResultset
   sql = "Select * from Authors where year_born is null"
   rdoEnvironment.CursorDriver = rdUseClientBatch
   cnB.QueryTimeout = 45
   Set rs = cnB.OpenResultset(sql, rdOpenStatic, rdConcurBatch)
   rs.MoveLast: rs.MoveFirst
   ReDim bms(rs.RowCount + 1) As Variant
   Do Until rs.EOF
      bms(i) = rs.Bookmark
      i = i + 1
      rs.MoveNext
   Loop
   rs.MoveFirst
   rdoGrid1.ShowData rs
End Sub

Performing an Update Operation Based on User Input

The following code demonstrates how to gather user input (in this case, author age), and then use this information to update the database in a batch update operation:

Private Sub rdoGrid1_Click()
   Dim rs As rdoResultset
   Dim NewValue As Integer
   NewValue = InputBox("Enter new age -- 1900 to 1997", "Author Age", _
    "1960")
   rs.Bookmark = bms(rdoGrid1.Row)
   rs.Edit
   rs!Year_Born = NewValue
   rs.Update
   Changes = Changes + 1
   i = MsgBox("Commit all " & Changes & " changes?", vbYesNoCancel)
   Select Case i
      Case vbYes
         rs.BatchUpdate
         Changes = 0
      Case vbNo
         Exit Sub
      Case vbCancel
         Changes = 0
         i = MsgBox("Cancel just this change (Yes) or all " & Changes & _
          " made so far (No)?", vbYesNo)
         If i = vbYes Then
            rs.CancelBatch (True)
         Else
            rs.CancelBatch
         End If
   End Select
End Sub

ADO

In this batch operation, note that the routine used to change the chosen row in the R/W resultset doesn't require starting an "Edit" session. To achieve the same effect as the previous RDO example, simply change the contents of a field and use the Update method to make the changes to the database.

Private Sub BatchOpsButton_Click()
   sql = "Select * from Authors where year_born is null"
   rs.Open sql, cnB, adOpenStatic, adLockBatchOptimistic
   rs.MoveLast: rs.MoveFirst
   ReDim bms(rs.RecordCount + 1) As Variant
   Do Until rs.EOF
      bms(i) = rs.Bookmark
      i = i + 1
      rs.MoveNext
   Loop
   rs.MoveFirst
   ADOGrid1.ShowData rs
   rs.Close
End Sub

Private Sub ADOGrid1_Click()
   Dim NewValue As Integer
   NewValue = InputBox("Enter new age -- 1900 to 1997", "Author Age", _
    "1960")
   rs.Bookmark = bms(ADOGrid1.Row)
   rs!Year_Born = NewValue
   rs.Update
   Changes = Changes + 1
   i = MsgBox("Commit all " & Changes & " changes?", vbYesNoCancel)
   Select Case i
      Case vbYes
         rs.UpdateBatch
         Changes = 0
      Case vbNo
         Exit Sub
      Case vbCancel
         Changes = 0
         i = MsgBox("Cancel just this change (Yes) or all " & Changes & _
          " made so far (No)?", vbYesNo)
         If i = vbYes Then
            rs.CancelBatch (True)
         Else
            rs.CancelBatch
         End If
   End Select
End Sub

Note In ADO2, you can use a batch filter to see rows that conflicted with an update rather than iterating through an array of conflicted rows, as was required in RDO2.


Using Data Access Objects with Remote Databases

Data Access Objects (DAO) can be used either with the Microsoft Jet database engine or, using the ODBCDirect option, without it. This chapter discusses design and implementation issues that arise when using the Data Access Objects (DAO) to access remote databases.

You may find the Jet Database Engine Programmer's Guide and Hitchhiker's Guide to Visual Basic and SQL Server (both published by Microsoft Press) useful sources of information on using DAO to access ODBC data sources using Jet.

Topics

DAO Remote Data Access Using Jet

Provides a brief overview of remote data access with DAO using the Jet engine.
Managing DAO ODBC Users

Briefly summarizes issues such as capacity, security, and maintenance.
Managing DAO Network Traffic

Outlines designing and debugging with network capacity in mind.
Managing DAO ODBC Connections with Jet

Outlines issues relevant to opening a connection to an ODBC data source with the Jet engine.
Choosing a DAO Query Processor for Use with Jet

Overviews issues relevant to performing queries on remote databases with the Jet engine.
Building DAO Cursors with Jet

Lists and describes cursors supported by the Jet engine.
Using DAO to Share Remote Data

Briefly describes issues relevant to sharing a database among users and applications.
Handling Remote DAO Messages and Errors

Outlines issues involved in handling errors during DAO remote database access.
Remote Data Access Using DAO and ODBCDirect

Briefly describes the ODBCDirect option, which maps DAO to Remote Data Objects.


DAO Remote Data Access Using Jet

This section discusses DAO functionality when it is connected to the Jet engine. The Microsoft Jet database engine is a stand-alone database management system that is capable of both processing queries and routing queries to remote servers as needed. Accessing Jet through DAO adds to Microsoft Visual Basic’s ease of development by providing an object-oriented development paradigm and accessibility to data-aware bound controls.

By using the Data control, DAO, or Microsoft Access, you can create code that is virtually database-independent, because Jet automatically performs all syntax and data manipulation translations for you. For example, you can write an application that accesses different types of data sources without making reference to specific remote server features. These data sources could be Open Database Connectivity (ODBC) databases, such as Microsoft SQL Server; Index Sequential Access Method (ISAM) databases, such as Microsoft FoxPro, Paradox, or dBASE; or other Jet databases.

Unlike most stand-alone database engines, Jet can perform heterogeneous joins across several dissimilar databases. If you are working with departmental data stored in ISAM format, and need to merge it with data on a centralized server, this is an essential feature.


Managing DAO ODBC Users

Each instance of your database application uses some number of connections and data page (or row) locks on the server, and creates a measurable load on the network. Since each additional user contends for many of the same resources, the number of users the system can support is directly proportional to the number of resources each instance of your application requires.

To reduce the number of locks, users should not be permitted to “sit” on unpopulated Recordset objects. The application should populate the recordset as quickly as possible using DAO, the Data control, or one of the background population techniques.

Your design should also include management of user logon IDs and passwords. If your design uses a shared Jet (.mdb) database, you must also address Jet security systems.

Because all users must disconnect from Jet databases (that contain data) for periodic maintenance, you should include a way to notify users to disconnect from the shared Jet database or provide a way to signal applications to disconnect automatically on their own. If the maintenance operations are executed during nonpeak hours, and applications automatically disconnect from the Jet database after a length of idle time, maintenance programs can execute without disturbing uncompleted result sets or pending updates.

For More Information See “Managing DAO ODBC Connections With Jet” later in this chapter.


Managing DAO Network Traffic

When using DAO with the Jet engine, a primary consideration is the amount of data that your network is required to carry. This is especially true if your design includes a shared Jet database that contains local, unattached data. In this case, the network will carry all disk I/O traffic as multiple users compete for shared data pages. If the shared database is simply a repository for one or more attached tables, in most cases only the query results need to be transmitted over the network.

Your application can control network traffic indirectly, through judicious use of Recordset object size and choice of query processor. In many cases, the Jet query processor can create Recordset objects with comparatively little network traffic. However, some designs may not accommodate its use, and may consequently create more network traffic than would occur with other programming models. By tuning the SQL query passed to the Jet query processor, you can often make better use of its power while improving network performance.

When accessing attached tables with the Jet engine, only the linkage information and the results of the query need to be transmitted over the network. If, however, the query processor is forced to download part or all of a remote table, network load increases dramatically.

Using a wide area network (WAN) with a Jet database is possible, and with careful error management, WAN applications can be implemented with a degree of security. Your design should, however, take additional precautions and include extremely robust error management that anticipates the loss of network access to the remote server and often dramatically longer response times. Since WAN networks can be significantly slower than conventional local area networks, special care should be given to the amount of network traffic generated and DAO timeout values. Examine the SQL trace logs for a better understanding of the number and complexity of the queries generated to remote ODBC servers. It is always good design practice to use more robust error handling for all network operations regardless of the topology.

Enabling Trace Logs

One of the most helpful debugging and tuning tools you have at your disposal is the ability of the ODBC Driver Manager to log all ODBC operations to an external file. You can enable this file using the ODBCDirect LogMessages property or by selecting the associated option in the Windows control panel ODBC Administration dialog. Be sure to turn off logging before your application goes into production, as the logging process can significantly impact performance.

Another option available to Microsoft SQL Server developers is the new SQLTrace utility that can let developers interactively view the queries submitted by all applications against a SQL Server. Once started, the SQLTrace utility exposes a window that displays each query or other operational request made.

For More Information See “Choosing a DAO Query Processor for Use with Jet” and “Managing DAO ODBC Connections with Jet.”


Managing DAO ODBC Connections with Jet

When you access an ODBC database through DAO or the Data control using Jet, the Microsoft Jet database engine and its connection management routines are automatically invoked. Jet will make every attempt to open as few connections as possible and share connections whenever possible.

Generally, the Jet database engine attempts to share or cache any connections established by your application. As a rule of thumb, if the Data Source Name (DSN) you request is referenced by more than one OpenDatabase call, then DAO and Jet attempt to re-use that connection. That is, if you already have a connection open to a specific DSN, Jet will not attempt to open another if it thinks it can share the existing connection. When you use the Close method on a DAO Database object, Jet does not close the connection immediately if it is the last connection to the specific DSN — it is cached in case you try to open the connection again.

Opening Connections

There are two primary ways to access remote ODBC data sources using DAO and Jet. Both techniques involve the OpenDatabase method against:

  • An ODBC data source. In this case, the connect string argument of the OpenDatabase method specifies a DSN that points to the ODBC data source.

  • A Jet database containing linked (attached) ODBC data source tables or views. In this case, the OpenDatabase dbName argument points to the path of a Jet .mdb database. The Jet database contains the DSN and, optionally, the user name and password.

Each technique has its advantages and disadvantages. However, using the OpenDatabase method against an ODBC DSN requires that Jet download a significant amount of descriptive information, as data definition language (DDL), about the target database. Because this information is not cached, this DDL fetch operation must be repeated each time the Database object is opened. This is one reason why Jet attempts to cache connections — so this process does not have to be repeated.

The process of attaching or linking to a table from an ODBC data source caches the DDL information in the Jet database, so it is not necessary to perform the DDL query when the connection is established or the database is opened. This can dramatically improve performance when opening a connection and creating a DAO Database object.

For More Information For details about connection strategies, see "Caching DAO ODBC Connections with Jet" in this chapter.


Managing DAO Data Source Name Entries

Generally, an ODBC connection requires a Data Source Name (DSN) entry. Depending on the operating system, these entries are either kept in the ODBC.ini file (16-bit systems) or in the system registry (32-bit systems). You should not attempt to change these entries manually. Instead, use the Windows Control Panel ODBC Administration applet or the RegisterDatabase method.

Note When using the DAO/Jet model, ODBCDirect, Remote Data Objects, or the ODBC API, it is not always necessary to create or reference a registered DSN if enough information about the remote server is provided in the connect string.

Setting the Default Database

Your code should ensure that the correct default database is set during the connection process. The user may specify a user ID that does not have permission to access the database your application expects to use, or uses a different default database. The default database can be established by:

  • Including the default database name in the DSN entry.

  • Including the DATABASE= argument in the connect string.

  • Establishing a default database on the server based on the user name.

  • Submitting an action query that changes the default database once the connection is open.


Establishing DAO ODBC Connections with Jet

Jet requires at least one connection when fetching data from a remote data source. If you indicate that the result set is to be updated, Jet attempts to open an additional connection unless an existing connection can be used. That is, one connection is used to populate the result set, and another to update it. However, once the result set is fully populated — as when you use the MoveLast method — the first connection can be closed or returned to the connection cache.

Providing User ID and Password

To gain access to a remote ODBC data source, you usually have to provide a valid user ID and password combination. These values can be provided in the Connect property of the Data control or in the connect string that is supplied as an argument to the OpenDatabase method. If these values are not supplied, the ODBC Driver Manager exposes a dialog to collect the user name, password, and other missing information needed to establish the connection. There is no way to disable this dialog with DAO and Jet. However, by using the ODBCDirect or RDO prompt arguments, this dialog can be disabled and your code can intercept a trappable error.

Opening Connections Directly

Jet follows these guidelines when managing connections to Microsoft SQL Server:

  • When using the OpenDatabase method, Jet opens a new connection, or attempts to re-use an existing connection if an identical DSN exists in the cache. The connection remains open after the Database object is closed (in anticipation of later use), unless there is already a cached connection to that server available. Only one connection for each DSN remains open in the cache.

  • When you open a connection directly using the OpenDatabase method, the DAO/Jet model is forced to query the database to determine the name of each available table there. This information is cached in the Database object and exists (does not have to be refetched) as long as the Database object remains instantiated.

  • With OpenRecordset, Jet tries to share an existing connection, reuse a cached connection, or, failing both of those, opens a new connection to the server and executes a query based on the source argument (or the SQL property of a QueryDef object). As soon as a MoveNext method is executed, Jet fetches the first 100 rows. If this does not complete the query, an additional connection is opened to support updates. The first connection must remain open until the recordset is fully populated or closed to support updates.

Opening Connections Indirectly

It usually more efficient to open connections to a remote data source by having Jet perform the operation. This is accomplished by simply opening a Jet database that contains linkages to remote database tables or views. When you access these attached (linked) objects, Jet establishes the connection using cached connection information that you provided when creating the attachments. However, if Jet is unable to complete the connection for whatever reason, the ODBC driver manager exposes a series of dialogs to attempt to collect logon and DSN information so the connection can be established. Using DAO with Jet, there is no way to disable these dialogs.

Another alternative is to provide the needed connection information to DAO and Jet by setting the Connect property on an open DAO/Jet Database object. Using this technique, you can then use SQL PassThrough queries just as if you had opened the connection directly.

For More Information For code examples illustrating the OpenDatabase and OpenRecordset methods, see “OpenDatabase” and “OpenRecordset” in the Language Reference".


Caching DAO ODBC Connections with Jet

A key part of connection management is that Jet caches either one or two connections, depending on the server. For servers such as Oracle, which allow pending results on a connection, Jet caches one connection. For servers such as Microsoft SQL Server, which do not allow pending results on a connection, Jet caches two connections.

Note SQL Server's server-side cursors support multiple operations on a single connection as implemented with ODBCDirect and RDO. However, there is no support for server-side cursors with Jet.

When Jet needs to open a connection, it first checks its internal connection cache. If there is a connection in the cache that uses the same DSN and database parameters, and there are no uncompleted queries pending on the connection, it is reused. Back-end database systems that support pending results on a single connection may not need additional connections to perform simultaneous read/write operations.

Note Jet caches the user ID and password along with the connection, so that you’re not repeatedly prompted. This means that if your application needs to log on to the server with a different user ID and password, you will be unable to do so unless you force the closure of any existing connections.

Jet ages each connection based on elapsed time and its activity. After a configurable connection timeout period (which defaults to 10 minutes), Jet automatically closes and drops any dormant connections. For a connection to be considered dormant, it must have no open Database or Workspace objects associated with it. Jet will not close connections if there are uncommitted transactions, or queries with unfetched results. Since Jet automatically closes connections, this implies that Jet automatically re-opens connections as needed.

Note The ConnectionTimeout setting can be adjusted by accessing the Windows system registry.

If your application needs access to a connection that Jet has timed out and closed, the connection is automatically reopened. Assuming that the connection is re-established, this should not cause a problem with your application.

In some cases, if a shared DSN is identical, queries against a second Database object might be blocked while Jet waits for the DSN to become available.


Closing DAO ODBC Connections with Jet

When you close a Recordset or Database object, or when these objects are no longer in scope, the connections they use are released to the connection cache. For example, if you declare a Recordset object in a procedure, and that procedure ends, the recordset is automatically closed and any connections needed to support it are released to the cache.

When your code visits the last record of a Recordset object, as when you execute the MoveLast method, the connection used to populate the recordset is released to the cache. A single connection is maintained to perform updates or other action queries against all open Recordset objects.

When your code closes a Database object or the object loses scope, Jet closes the Database and any associated Recordset objects. Any connections associated with those objects are released to the cache.

Each Data control functions like an OpenRecordset method. That is, each Data control creates one or two connections (depending on the size of the result set and the functionality of the server being accessed) when they are initialized. Visual Basic automatically populates Recordset objects created by the Data control to release connections as quickly as possible. This happens during idle time, and at a configurable rate determined by the MSysConf table settings. Generally, Jet maintains a single connection to perform updates, but until the result set associated with each Data control is fully populated, a second connection must remain open to return the rows. When your code positions the recordset to the last row, as when you execute a MoveLast method, this extra connection is no longer needed and is returned to the pool.


Choosing a DAO Query Processor with Jet

Choosing a DAO Query Processor for Use with Jet

Once the database engine has been initialized and the connection established, your application can begin to fetch information from the database. When working with remote database engines that are capable of executing SQL queries, you can usually choose how and where the query is processed. The query used to build a cursor can be run by either the Jet query processor on the client machine, or by the remote database query processor on the server. In some cases you have no choice, because the type of query (or another consideration) forces you to use a particular option.

The query processor you use can have a significant impact on the performance of your application. For most queries the Jet query processor breaks down your query and submits intelligent subqueries to the remote server for processing. These subqueries return subsets of the data used to complete the query on the workstation. However, when Jet is unable to perform this type of query management, it might try to bring entire tables (or large portions of those tables) back to the workstation for further processing. If this happens, your query might exhaust all workstation resources or exceed query timeout limitations.

By forcing Jet to perform queries entirely on the remote server, you can prevent resource-constrained queries from overwhelming your application. However, in many cases, a redesign of your application's approach to the query might prove to be just as beneficial for performance. For example, if your query returns more than a few hundred rows from the server, running the query on the server might improve performance, but the resource demands on the workstation and the network might still be fairly significant.


Using DAO to Select the Jet Query Processor

All queries executed by the Jet query processor must be written using Jet SQL syntax. However, Jet’s SQL syntax is not always the same as the SQL syntax used on your server database. Jet’s SQL dialect is the same, however, regardless of the database it needs to access. This feature can provide significant portability in your code and the ability to seamlessly access heterogeneous data.

By default, the Jet query processor is invoked when any DAO query is executed. In other words, unless you use the dbSQLPassThrough option with the Execute or OpenRecordset methods or create a SQLPassThrough QueryDef object, the Jet query processor will parse and execute the query's SQL syntax, and attempt to perform whatever operations are needed on the workstation and the remote server to carry out the request.


Using DAO to Select a Remote Query Processor

Sometimes you need to force the remote query processor to execute the query. As discussed in "Managing DAO ODBC Connections with Jet," earlier in this chapter, opening a Database object directly can be very costly in time and network traffic because the structure of the remote database and its tables must be determined by sending a number of queries to the remote database.

In cases where you must use remote database-specific SQL syntax, or you want to use the remote database engine's query processor, you must bypass the Jet query processor by using the dbSQLPassthrough option with the DAO Execute or OpenRecordset methods. It is also possible to create DAO QueryDef objects that bypass the Jet query processor.

To use QueryDef objects, your application will need access to a Jet .mdb database. There are two kinds of QueryDef objects:

  • QueryDef objects used for pass-through queries, which use the server’s syntax and can’t refer to attached tables.

  • QueryDef objects used for non-pass-through queries, which use attached tables and attempt to translate and send as much of the query to the server as possible.


Using SQL PassThrough Queries with DAO

In many applications, you’ll use both Jet queries (that is, queries executed by the Jet database engine) based on attached remote tables and SQL pass-through queries. With a Jet query, the query engine determines which parts of the query can be sent to the server and which parts must be processed locally, thereby combining the power of the server with the capabilities of the Jet database engine. With a DAO Jet SQL pass-through query, your code provides a SQL statement that Jet sends directly to the server without stopping to compile the query. Once the SQL pass-through query is complete, if it creates a result set, the Jet recordset processor creates a snapshot-type Recordset object to manage it.


Managing System Resources

Managing DAO Query Result Set Size

The Jet database engine is capable of retrieving data from databases of any size. However, as the number of records processed increases, be aware of the increased amount of required TEMP storage space. Any design that opens tables directly, without benefit of a SQL query that limits scope, should be reconsidered. Jet will generate a trappable error if local disk space is exhausted while Jet is building a keyset. This is not really a limitation in Jet but of system resources, and it is characteristic of poor application design. Any application design that requires the database engine to create a physical pointer to each row of the result set data, such as when a keyset is created, has a theoretical upper limit set by the capacity of the media where the keyset is stored.

In some cursor models, only a subset of the keyset is maintained on the client machine, or the keysets are built on the server. Although Jet supports a table-type recordset that permits browsing tables without impacting client resources, this is not available when accessing remote (ODBC) data. Instead, only the dynaset-type and snapshot-type Recordset objects are supported.

Both of these build the keyset on the client system, overflowing to TEMP space on disk if necessary. The snapshot-type recordset also downloads data, which may further limit the size of the recordset that can be built. In any case, your data access strategy should involve restraining the result set scope. That is, you should limit the number of rows returned by the query.

For More Information See "Choosing a DAO Query Processor for Use with Jet" later in this chapter.


Building DAO Cursors with Jet

Whenever you execute a query that returns rows, you must decide which type of cursor is to be created to manage the result set. Cursors are simply a set of rows returned from the data source that meet the criteria you specify in a SQL statement. The SQL statement indicates which table(s) and column(s) are to be returned and limits the rows based on criteria in a WHERE clause.

When using DAO and Jet to fetch information from the database, you must use the Recordset object to build a cursor. This can be accomplished using attached tables, QueryDef objects with Connect strings and SQL queries, or a combination of the two.

The following terms are used when describing a cursor:

  • Scrolling is the ability to position to a specific row of the result set using one or more DAO methods.

  • Updatability indicates whether a cursor permits changes to the underlying database rows.

  • Membership in the result set is determined by the SQL query. As you use the AddNew or Delete methods to add or remove rows or as others sharing the database make changes, the membership of a cursor might or might not reflect those changes.

  • Static data values in static cursors have been copied to local memory where the cursor library makes no attempt to keep the data current. Keyset or dynamic cursors requery the database to fetch data from the database as you position over specific rows of the result set.

The DAO/Jet model supports a variety of cursors against remote data sources including:

  • Dynaset cursors are fully scrollable, updatable cursors with fixed membership and dynamic data values.

  • Snapshot cursors are fully scrolling, read-only cursors with fixed membership and static data values.

  • Forward-only cursors are nonscrolling, read-only cursors with fixed membership and static data values.

ODBCDirect adds Dynamic cursors to this list as implemented by RDO.

It always makes sense to create a cursor that has the least impact on your workstation and provides the best performance. The cursors created by DAO when using Jet can be expensive, in that the default dynaset-type Recordset object cursor is read/write, is fully scrolling, and supports updatable heterogeneous joins. By choosing the DAO Jet forward-only, read-only cursor, you can dramatically improve cursor performance. In contrast, the default ODBCDirect cursor is the most efficient, but offers fewer features because it defaults to a read-only forward-only recordset.


Accessing SQL Views Using DAO

If the remote database only exposes SQL views, you can access this data by attaching those views to a Jet database and creating pseudo indexes to the view using a DAO action query. Although not actually an index, a pseudo index allows Jet to create an updatable recordset on the view. You don’t need to create a pseudo index if you are not updating server data.

SQL views can also be accessed by DAO through ODBCDirect. In some cases these views are updatable using the indexes already available on the remote server.


Accessing Stored Procedures Using DAO

In some environments, access to server data is limited to a set of server-based stored procedures. In this case, some or all data requests and updates are carried out through these stored procedures — especially when you have no direct access to the remote tables. In such an environment, you must use SQL pass-through queries exclusively or use ODBCDirect if you choose to use DAO. If your server forces all queries and updates to be executed through stored procedures, then you can use SQL pass-through queries to execute the UPDATE stored procedures as well as the SELECT stored procedures. You can then base other Jet QueryDef objects on these queries as if they were attached tables.


Using DAO to Share Remote Data

In any client/server application, one of your primary design concerns will be how to best share the data resources. When using either the Jet or remote engine query processor, your design must include code that deals with conflicts caused by instances of your database application and other applications trying to access the same database. Any design must include robust error handling to deal with a variety of contingencies caused by conflicts that arise as multiple applications vie for the same server resources.

If your design calls for a centrally shared Jet database that includes attached tables, each client system must also contain DSNs that are compatible with your database. Since the DSN is maintained on the client and only referenced by name in the shared database attachments, your setup routines must ensure the client DSN description is correct — and remains so — to eliminate any chance that the user might change one or more parameters.

Although the remote database engine is responsible for managing its own page locks and resources, any application, including those that use the Jet query processor, can lock pages on the server for indefinite periods of time.


Handling Remote DAO Messages and Errors

Remote server systems generate their own litany of errors and messages. The database itself may contain procedures that generate user-defined messages or errors. Once DAO and the Jet database engine receive any error, regardless of the cause, the query that triggered the error is terminated. For those databases that use the SQL Server RaisError function to indicate warning-level messages, this may be problematic.

When using QueryDef objects to execute SQL pass-through queries, other messages received from ODBC and the remote server can be trapped. For example, SQL Server SQL PRINT statements generate a message that can be trapped by your code. To enable message trapping, your code must create a property named “LogMessages” for a specific QueryDef object, and set this property to True. Once set, messages generated by the selected QueryDef are recorded in a Jet table.

Each SQL query that Jet or the remote query processor executes can generate one or more ODBC or other remote-engine errors. All of these errors are stored in the Errors collection, which is accessible either during break mode or at run time. Documentation is available for some of these messages, especially those mapped by Jet to its own error numbers. Most ODBC operations will generate a generic ODBC trappable error that is explained more fully in messages found in other members of the Errors collection.

For More Information See "Errors Collection" and "LogMessages Property" in the Language Reference.


Remote Data Access Using DAO and ODBCDirect

Visual Basic version offers an additional option that can be used with DAO to access remote database engines: ODBCDirect. This DAO option permits your application to choose the database engine and interface used by DAO. Basically, you have two choices:

  • The Microsoft Jet database engine. By default, DAO uses Jet to perform all data access operations.

  • ODBCDirect. When this option is enabled, DAO loads the Remote Data Objects (RDO) 2.0 libraries and delegates all data access operations to the ODBC data source.

Basically, ODBCDirect maps each of the Data Access Objects to an equivalent Remote Data Object. While not all of the RDO functionality is implemented with ODBCDirect, this approach permits you to leverage existing DAO-based applications using a familiar object model when accessing remote database systems.

For More Information Information about ODBCDirect’s relationship to RDO is also discussed throughout "Using Remote Data Objects and the Remote Data Control."


Using Remote Data Objects and the RemoteData Control

This chapter provides an overview of the features of the Remote Data Objects (RDO) programming model and the RemoteData control. The information in this chapter can help you make a decision regarding which data access programming model to use in your client/server application.

Remote Data Objects implement a set of objects to deal with the special requirements of remote data access. RDO implements a thin code layer over the ODBC API and driver manager that establishes connections, creates result sets and cursors, and executes complex procedures using minimal workstation resources. Although RDO is also accessed by DAO when your code creates an ODBCDirect Workspace object, documentation for this implementation can be found by searching for specific ODBCDirect topics.

Note RDO and the RemoteData control are designed to be used only in 32-bit operating systems such as Microsoft Windows 95 or Microsoft Windows NT.

Topics

Features of Remote Data Objects

Introduces key capabilities of RDO.
Programming with Remote Data Objects

Introduces the RDO object model.
RDO Compared to Microsoft Jet/DAO

Discusses what features are provided by RDO and how they map to more familiar DAO features.
Initializing the rdoEngine Object

An overview of the rdoEngine object and how to setup its properties.
Creating an rdoEnvironment Object

An overview of the rdoEnvironment object and how to apply its properties to data access issues.
Establishing an RDO Connection

How to get connected to a remote data source.
Using RDO to Submit Queries

How to build and submit queries including parameter queries.
Using RDO to Execute Stored Procedures

How to work with the special requirements of stored procedures.
Creating RDO Cursors

Introduces techniques for creating cursors with RDO.
Working with RDO Result Sets

How to deal with the data and parameters returned from queries.
Executing Optimistic Batch Updates

How to use the optimistic batch update feature.
Using ODBC API Functions

Introduces the ODBC handles supported by RDO objects.
Using the RemoteData Control

How to retrieve data through bound controls using the RemoteData control.


Features of Remote Data Objects

With RDO and the RemoteData control, your applications can access ODBC data sources without using a local query processor. This can mean significantly higher performance and more flexibility when accessing remote database engines.

By using RDO, you can:

  • Create simple cursorless result sets, or more complex cursors.

  • Run queries and process any number of result sets.

  • Execute stored procedures that return result sets with or without output parameters and return values.

  • Execute action queries that perform data manipulation or data definition operations.

  • Limit the number of rows returned or processed.

  • Monitor all of the messages and errors generated by the remote data source without compromising the executing query.

  • Enable synchronous, asynchronous, or event-driven asynchronous processing so your application isn't blocked while lengthy queries are executed or the current row pointer is repositioned.

About the User Connection Designer

The User Connection Designer uses Microsoft Visual Basic’s new class designer architecture to provide design-time support for programmatic data access. It allows you to create connection and query objects (based on Remote Data Objects) at design time. These connections and queries are persisted as project-level objects. You can preset properties, define new properties and methods, and write code behind the objects to trap events.

This provides a simplified method for responding to events raised from connections and queries as well as for calling stored procedures and client-defined queries at run time.


RDO Configuration Requirements

Although you can access any ODBC data source with RDO and the RemoteData control, RDO is designed to take advantage of intelligent database servers, like Microsoft SQL Server and Oracle, that use sophisticated query engines. If you connect to ODBC drivers that are not fully ODBC Level II compliant or to a less sophisticated data source, many RDO features cannot be enabled.

RDO 2.0 has relaxed many of the ODBC compliance requirements imposed by RDO 1.0. Specifically, RDO 2.0 now requires that your ODBC driver support only one Level II compliant operation that enables the SQLNumParams ODBC API function. Without support for this ODBC Level II function, RDO cannot create the rdoParameters collection and parse parameter markers in SQL statements. However, if the driver does not support the SQLProcedureColumns and SQLDescribeParam functions, then your code must supply the direction and data type for each parameter in the rdoParameters collection.

While some drivers can be used to create and execute queries, if your driver does not support creation of the rdoParameters collection, RDO fails quietly and simply does not create the collection. As a result, any reference to the collection results in a trappable error.

If you use RDO to access Microsoft Jet engine databases, the Jet engine must be loaded into memory and all ODBC requests are routed through Jet operations. Not only can this require more RAM and CPU resources than using DAO, but many RDO functions are not supported by the fairly limited Jet ODBC drivers.

RDO is implemented as a 32-bit ActiveX interface, so your target system must be running Windows 95 or Windows NT.

In addition, RDO is only licensed to Visual Basic Enterprise Edition developers. While an RDO program can be compiled and distributed along with the RDO components to an unlimited number of machines, the distributed version is only licensed for run-time operation. Other Microsoft Office platforms do not have a run-time mode or a license to act as an RDO development platform. However, ODBCDirect is a licensed interface to RDO that can be used on Microsoft Office platforms.

Note RDO and the RemoteData control are 32-bit features of the Visual Basic, Enterprise Edition. You cannot develop code or use the RDO object library or RemoteData control in the Professional or Learning Editions of Visual Basic or on 16-bit platforms.


RDO and Client/Server Design Goals

RDO and the RemoteData control can help you meet a specific set of client/server requirements. By using these remote data access features, you can:

  • Gain high-performance data access against remote ODBC data sources. The ability to quickly retrieve the results from complex queries is a goal of every data access application. RDO provides a level of performance rivaled only by the ODBC and VBSQL API programming models. By leveraging the remote data engine, RDO greatly improves response time and user productivity.

  • Manage return codes and both input and output parameters from stored procedures. Output parameters are the only way to extract information from an Oracle stored procedure, and are used heavily for singleton queries and many administrative functions. In many cases, you cannot determine if a stored procedure completed successfully without accessing the procedure’s return value. RDO supports access to each of these parameters through the rdoParameter object.

  • Manage multiple result sets. By using a single query that returns several sets of results, you can use the query processor and system resources more efficiently. You can improve performance by running a single query to gather data to fill multiple data-driven list boxes and menus. In addition, by combining a row-count query with a SELECT query, you can accurately set up scroll bars and progress status bars.

  • Submit multiple action queries in a single batch. In many cases, your application can submit a set of INSERT, DELETE or UPDATE operations as a single SQL statement. This increases performance, as it reduces network and remote processing overhead and lets you manage transactions more easily.

  • Limit the number of returned or processed rows. In situations where users might select more rows than it is practical to handle, RDO implements a query governor to limit the number of rows returned from any data source. This way, you can predict query response time and more easily manage the workstation or server resources required to maintain cursor keysets. Using the same mechanism, you can limit the number of rows affected by a data-modification query.

  • Utilize server-side cursors. Some servers, such as Microsoft SQL Server, support cursor keysets that are created on the server, instead of on the workstation. Under the right conditions, this type of cursor management can significantly improve performance and reduce network load and workstation resource requirements.

  • Execute queries asynchronously. If a query takes an extended period of time to run, you should have the option of either executing code while the query is being processed or canceling the query. RDO provides an asynchronous query option that you can use when executing any query, as well as a way to cancel an asynchronous query. RDO also provides a unique event-driven asynchronous programming interface that eliminates the need for polling loops. Asynchronous processing extends to opening connections and when using the MoveLast method.

  • Continue queries after initial timeout period. When a query exhausts the period set in the QueryTimeout property, RDO permits you to override query cancellation and continue waiting for another timeout period.

  • Support improved polymorphism and "free-standing" object creation. RDO now supports creation of rdoConnection and rdoQuery objects using the Dim statement. These free-standing objects can be associated with other objects to perform actions. For example, you can create an rdoQuery object independent of any rdoConnection object and associate it with an open connection at a later time.

  • Support dissociated result sets. RDO permits you to create a static read-write cursor and break the connection with the remote server. The data in the rdoResultset object remains available for access or changes. Once you re-associate the result set with another rdoConnection object, you can use the BatchUpdate method to post these offline changes to the database.

  • Create and manage optimistic batch updates. While the ODBC cursor library supports the concept of optimistic updates, it does so on a row by row basis rather than a batch basis. This type of update operation consumes considerable network and server bandwidth. RDO leverages the new Client Batch cursor library that groups sets of rows to be inserted, updated, or deleted. This way, only one round trip to the server is needed, thus improving update performance and decreasing network traffic.

  • Make the use of stored procedures easier. RDO permits you to express parameterized queries and stored procedures as methods of a parent rdoConnection object. You can pass parameters just as you would to any Visual Basic function without having to manipulate any rdoParameter objects.

  • Expose underlying ODBC handles. In cases where you need more flexibility or control than is available in the object model, you should have a way to directly access the data source. RDO provides access to the ODBC environment, connection, and statement handles.

  • Reduce memory footprint. In many cases, the system that hosts a client/server front-end application is limited in RAM capacity. Because of this, it is important that applications designed for these systems economize their use of RAM and other workstation system resources. The RDO memory footprint is dramatically smaller than other programming models, and it does not require the use of local memory or disk space for its lowest-level cursors.


Programming with Remote Data Objects

RDO objects and collections provide a framework for using code to create and manipulate components of a remote ODBC database system. Objects and collections have properties that describe the characteristics of database components and methods that you use to manipulate them. Using the containment framework, you create relationships among objects and collections, and these relationships represent the logical structure of your database system.

Figure 11.1 The RDO 2.0 object model

With the exception of the rdoEngine object, each of these objects is maintained in an associated collection. When RDO is initialized on first access, RDO automatically creates an instance of the rdoEngine and the default rdoEnvironments(0).

The Remote Data Object programming model is similar to the Data Access Objects (DAO) programming model in many respects. However, far more emphasis is focused on handling stored procedures and their result sets, and less emphasis is placed on data access retrieval methods used solely by ISAM programming models. The following table describes each of the objects.

RDO object | Description
rdoEngine | The base object. Created automatically when you first access RDO in your application.
rdoError | Used to handle all ODBC errors and messages generated by RDO. Created automatically.
rdoEnvironment | Defines a logical set of connections and transaction scope for a particular user name. Contains both open and allocated (but unopened) connections, provides mechanisms for simultaneous transactions, and provides a security context for data manipulation language (DML) operations on the database. rdoEnvironments(0) created automatically.
rdoConnection | Represents an open connection to a remote data source and a specific database on that data source, or an allocated but as yet unconnected object, which can be used to subsequently establish a connection.
rdoTable | Represents the stored definition of a base table or an SQL view.
rdoResultset | Represents the rows that result from running a query.
rdoColumn | Represents a column of data with a common data type and a common set of properties.
rdoQuery | An SQL query definition that can include zero or more parameters.
rdoParameter | Represents a parameter associated with an rdoQuery object. Query parameters can be input, output, or both.

Note True distributed transactions can only be done between database management systems that support the Distributed Transaction Coordinator (DTC). At this time, only Microsoft SQL Server 6.5 supports this functionality. For more information about DTC, see "Using the rdoEnvironment Object to Manage Transactions" in this chapter.

Note The RDO 1.0 rdoPreparedStatement object and rdoPreparedStatements collection are supported by RDO 2.0, but only for backward compatibility. You should convert your code to use the rdoQuery object and rdoQueries collection instead.


Creating Remote Data Objects

You can use the RemoteData control to create RDO objects, just as you can use the Data control to create DAO objects. You can also create result sets using RDO methods and pass them to the RemoteData control for management and editing by associated bound controls.

Remote Data Objects are also created by using methods on the parent object, or by declaring the objects with the Dim statement. If you use the Dim as New statement to instantiate a new object, the object is not appended to the associated collection. The following table shows how to create each of the primary Remote Data Objects:

Remote Data Object | How created
rdoEngine | Created automatically.
rdoEnvironment | First instance created automatically. rdoEngine.rdoCreateEnvironment
rdoConnection rdoEnvironment(x).OpenConnection
(can also be created by the RemoteData control)
Dim MyCn as New rdoConnection
rdoQuery rdoConnections(x).CreateQuery
Dim MyQry as New rdoQuery
rdoResultset rdoQuery(x).OpenResultset
rdoConnection(x).OpenResultset
(can also be created by the RemoteData control)
rdoParameter | Created automatically for parameter queries.
rdoTable | Created automatically when referenced.
rdoError | Created automatically when ODBC errors or messages are returned.


RDO Compared to Microsoft Jet/DAO

Basically, you can use Remote Data Objects similarly to the way you use the Microsoft Jet database engine Data Access Objects (DAO), and the RemoteData control is similar to the Data control. With RDO, you can submit queries, create a result set or cursor, and process the results from the query using database-independent object-oriented code.

Using the RemoteData control, you can create a form containing the same bound controls recognized by the Data control and process a result set with little or no code.

You can take your existing applications that use DAO and the Data control and convert them to use RDO and the RemoteData control with a few changes. There are some differences, however, because RDO is implemented and designed for use with strictly relational databases. RDO has no query processor of its own; it depends on the data source to process all queries and create the result sets. The data objects themselves are built from the result sets and cursors returned by the ODBC driver.

It might be unnecessary to convert an existing DAO/Jet application over to RDO because of ODBCDirect, which routes DAO through RDO instead of Jet. If your application does not use the DAO ISAM objects and methods (like the table-type Recordset object and the Seek method) or other ISAM programming methodologies, you should be able to convert over to ODBCDirect with very few changes — even fewer than are required when converting to RDO.

For More Information For more information about ODBCDirect, see "RDO Compared to Microsoft Jet ODBCDirect" and "Remote Data Access Using DAO and ODBCDirect" in "Using Data Access Objects"

The following table lists RDO 2.0 objects and their equivalent DAO/Jet objects.

Remote Data Objects and their DAO/Jet Equivalents

RDO object | Equivalent DAO/Jet object
rdoEngine | DBEngine
rdoError | Error
rdoEnvironment | Workspace
rdoConnection | Database
rdoTable | TableDef
Not Implemented | Index
rdoResultset | Recordset
Not implemented | Table-type
Keyset-type | Dynaset-type
Static-type (r/w) | Snapshot-type (r/o)
Dynamic-type | (none)
Forward-only – type | Forward-only-type
(cursorless) | (none)
rdoColumn | Field
rdoQuery | QueryDef
rdoParameter | Parameter
Not Implemented | Relation
Not Implemented | Group
Not implemented | User

Remote Data Objects refer to table rows instead of records and columns instead of fields — the generally accepted terminology for relational databases. The data returned from a query is in the form of result sets, which can contain zero or more data rows composed of one or more columns. Whereas DAO requires cursors to access data, RDO permits you to create a cursorless result set that has far fewer resource demands than a cursor.

Some DAO objects, methods, and properties are designed to implement and support the ISAM structure of Jet and installable ISAM databases. For example, you can use the Index object and the Seek method to manage ISAM indexes and locate rows based on those indexes. Because the RDO and relational databases manage indexes in an entirely different manner, those objects and methods are not needed.

DAO also supports the creation and modification of the database schema, referential integrity (RI), and security through DAO methods and properties. RDO does not support any type of RI, security, or schema modification because they are fully supported in the tools and utilities provided with the server systems.

You can still run RDO make-table queries or execute action queries that create, modify, or delete databases and tables using native SQL statements. You can also execute complex stored procedures that manage the database schema or perform maintenance operations that are not possible with DAO.


RDO Collection Management

RDO uses a collection to manage each of the RDO objects except the RDO engine itself. Because Visual Basic version 6.0 now supports the creation of some stand-alone objects like rdoConnection and rdoQuery objects, not all objects are automatically appended to their parent collection — but in most cases, this is done automatically. Stand-alone rdoConnection and rdoQuery objects can be appended to their parent collection using the Add method and can be removed from the collection using the Remove method.

The rdoParameters collection is automatically created when using the CreateQuery method. However, if the ODBC interface cannot parse the SQL syntax for any reason, this collection used to manage a query's parameters is not created. Therefore, any reference to the rdoParameters collection will result in a trappable error unless RDO successfully parses the SQL parameter query as specified in the SQL property of the rdoQuery object. This collection is also not populated if the ODBC data source driver does not support the SQLNumParams function or if this function returns an error.

The following table lists the collections managed by RDO:

RDO collection | Description
rdoErrors | Contains all stored rdoError objects which pertain to a single operation involving Remote Data Objects (RDO).
rdoEnvironments | Contains all active rdoEnvironment objects of the rdoEngine object. rdoEnvironments(0) is created automatically.
rdoConnections | Contains all rdoConnection objects opened or created in an rdoEnvironment object, or allocated and appended to the rdoConnections collection using the Add method.
rdoTables | Contains all stored rdoTable objects in a database.
rdoResultsets | Contains all open rdoResultset objects in an rdoConnection.
rdoColumns | Contains all rdoColumn objects of an rdoResultset or rdoTable object.
rdoQueries | Contains rdoQuery objects that have been added to the rdoQueries collection either automatically via the CreateQuery method, or with the Add method.
rdoParameters | Contains all the rdoParameter objects of an rdoQuery object once the SQL statement is successfully parsed. Contains an rdoParameter object for each marked parameter in the query.

RDO 1.0 Collections

RDO 1.0 objects and their collections are managed differently than RDO 2.0 objects and collections. When you open or create a new RDO 1.0 object, it is automatically appended to the collection associated with those objects, even when you set an existing variable to the newly created object.

For example, the following RDO 1.0 code creates two independent rdoConnection objects:

Dim Cn as rdoConnection
Set Cn = rdoEnvironments(0).OpenConnection(  _
      dsname:="MyDSN", _
      prompt:=rdDriverNoPrompt,  _
      connect:="UID=;PWD=;")
Set Cn = rdoEnvironments(0).OpenConnection(  _
      dsname:="MyOtherDSN", _
      prompt:=rdDriverNoPrompt,  _
      connect:="UID=;PWD=;")

After this code is executed, both RDO 1.0 rdoConnection objects are appended as members of the rdoConnections collection. With this behavior in mind, your code must use the Close method on objects that are no longer needed as they are not automatically closed — even when assigned to the same variable. This same behavior applies to rdoResultset objects as well.

RDO 2.0 Collections

In RDO 2.0, setting an existing variable to a newly created object releases or closes the existing RDO object before replacing it with the newly created RDO object. This change makes RDO work more like DAO objects.

When the code shown above is executed in Visual Basic using RDO 2.0, only one rdoConnection object is created: The first rdoConnection object is closed and removed from the rdoConnections collection. If your Visual Basic version 4.0 code explicitly closes RDO connections and rdoResultset objects, you need not be concerned with backward compatibility. However, if you depend on these objects to be maintained, then you must assign new RDO objects to new variables or the original object will be lost.


RDO Compared to Microsoft Jet ODBCDirect

DAO supports an object interface option that links the DAO object model to Remote Data Objects. This new interface, referred to as ODBCDirect, vectors DAO references to equivalent RDO objects once the DBEngine object's DefaultType property is set. If ODBCDirect is enabled, DAO does not load the Microsoft Jet database engine — it loads RDO 2.0 instead.

Generally, this interface is designed for developers who need to maintain a degree of source-code compatibility between DAO/Jet and DAO/ODBCDirect applications. For example, in cases where a single application must be written to access both native Jet and ODBC-accessed databases, ODBCDirect can provide a similar programming paradigm.

Note While the object names used in ODBCDirect are roughly the same as those used in DAO/Jet, some properties and methods that can be applied to DAO/Jet objects cannot be used with ODBCDirect Recordset objects. Similarly, some properties and methods exposed by ODBCDirect Recordset objects cannot be used with DAO/Jet Recordset objects. In addition, not all RDO 2.0 functionality is exposed in ODBCDirect.

For More Information For more information about ODBCDirect, see "Remote Data Access Using DAO and ODBCDirect" in "Using Data Access Objects."


Initializing the rdoEngine Object

The rdoEngine object represents the remote data source and is created automatically when you first make reference to an RDO object or the RemoteData control. As the top-level object, it contains all the other objects in the hierarchy of Remote Data Objects. The rdoEngine object is predefined, therefore you can’t create additional rdoEngine objects, and it isn’t a member of any collection. You can use the rdoEngine to set data source parameters and create additional rdoEnvironment objects.

Even though the rdoEngine is shared among the various applications that use it, the rdoEngine default properties are not shared between applications. Each instance of your application is provided with its own set of default values that have no effect on other applications that also use RDO or the RemoteData control.

Note Adding the RemoteData control to your Toolbox does not automatically set a reference to the Microsoft Remote Data Object library. To use the rdoEngine and Remote Data Objects from code, you must set a reference to the Microsoft Remote Data Object 2.0 object library in the References dialog box (available from the Projects menu); otherwise, you will get a compilation error when the first RDO object is referenced. Also, If you have Visual Basic version 4.0 or Microsoft Office installed on your computer, the RDO 1.0 library might appear in the list of object libraries exposed by the Project References dialog. While using this library is possible with Visual Basic version 5.0 or 6.0, it is not recommended.


Initializing rdoEngine Default Properties

When you create new rdoEnvironment, rdoConnection, or rdoResultset objects, the characteristics of the new objects are determined by the arguments in the rdoCreateEnvironment or OpenResultset method, or by the default values in the default properties of the rdoEngine. These properties are listed in the following table.

Property | Specifies | Default
rdoDefaultCursorDriver | Cursor library (ODBC, client batch, server-side, or none) | rdUseIfNeeded (server-side cursors)
rdoDefaultPassword | User password | "" (empty string)
rdoDefaultUser | User ID | "" (empty string)
rdoDefaultErrorThreshold | Error severity above which errors are fatal | -1 (disabled)
rdoDefaultLoginTimeout | Time to wait before abandoning connection attempt | 15 seconds

You can change any of these default value properties before creating new rdoEnvironment, rdoConnection, or rdoResultset objects. However, rdoEnvironments(0) is created using the defaults shown in the preceding table because it is created automatically on the first reference to RDO or the RemoteData control. If the default values are not appropriate for your application, you must change the properties of rdoEnvironments(0) or the rdoEngine before creating a new rdoEnvironment object or opening a connection.

Trapping the InfoMessage Event

The rdoEngine fires the InfoMessage event when the ODBC interface receives an informational message from the remote server. This event is raised after RDO receives a SQL_SUCCESS_WITH_INFO return code and populates the rdoErrors collection with the informational messages. If an RDO method call generates many informational messages, this event will be raised only once, after the last informational message has been added to the collection. You can trap this event and examine the contents of the rdoErrors collection and then decide what action is appropriate.

For example, if you submit a query that enables statistics or query plan information from SQL Server, this information is returned through the rdoErrors collection, and your application is notified that these messages are available when the InfoMessage event is fired.


Managing RDO Errors and Messages

Each time the ODBC driver manager is used to carry out an RDO request, an error can be generated by Visual Basic, the ODBC interface, or the remote server. These errors can be of varying severity and in some cases may force the query to be canceled or abandoned.

Any errors generated by Visual Basic trigger a trappable error that you can trap using the On Error statement. Errors generated by the ODBC middleware or by the remote server are placed in the rdoErrors collection. Whenever the ODBC function being called by RDO returns a SQL_ERROR result code, Visual Basic fires a trappable error indicating that an ODBC error has occurred. At this point your code can examine the individual members of the rdoErrors collection for details on what caused the error.

Whenever the return code from the ODBC API function is set to SQL_SUCCESS_WITH_INFO, an Informational message is returned from the data source that does not trigger a trappable error. These messages are placed in the rdoErrors collection.

Visual Basic also produces a trappable error when an error occurs that is not directly related to an ODBC operation. For example, if you execute the CreateQuery method and then try to set the rdoParameters collection, you might trip a trappable error indicating that the object does not exist. This can be caused by RDO's inability to build an rdoParameters collection based on the query's syntax. You can examine the Err and Error properties in your On Error handler to determine what action should be taken.

Once you examine the rdoErrors collection, you can clear its contents by using the Clear method. This prevents the confusion that can occur if you subsequently check the rdoErrors collection for errors, only to discover residual messages from an earlier operation that could have no bearing on the current problem.

When you execute a query, the QueryComplete event fires — whether or not the query executed successfully. You can examine the ErrorOccurred Boolean flag to check for successful completion of the query. In a similar fashion, you can check the ErrorOccurred flag in the Connected event to check for the success or failure of a connection operation.

For More Information See "rdoError Object" or "rdoDefaultErrorThreshold Property" in the Language Reference".


Creating an rdoEnvironment Object

In most cases, your application need not create an additional rdoEnvironment object — the default rdoEnvironments(0) should suffice for most operations. However, if your application expects to support more than one transaction scope, or separate user name and password contexts, you should use the rdoCreateEnvironment method to create a new rdoEnvironment object with specific user name and password values. This method accepts a unique name, a user name, and password. If the name you choose matches the name of an existing member of the rdoEnvironments collection, a trappable error results.

The default rdoEnvironments(0) is created automatically when the RemoteData control is initialized, or when the first RDO object is referenced in code. The Name property of rdoEnvironments(0) is "Default_Environment." The user name and password for rdoEnvironments(0) are both zero-length strings ("").

Newly created rdoEnvironment objects are automatically appended to the rdoEnvironments collection if you provide a unique name. You can also use a zero-length string for the name argument of the rdoCreateEnvironment method. In this case, the new rdoEnvironment is not appended to the rdoEnvironments collection.

Cached Logon Information

The user name and password information from the rdoEnvironment is used to establish the connection if these values are not supplied in the connect argument of the OpenConnection method, or in the Connect property of the RemoteData control.

For example, the default user name (Fred) and password (Blond) can be used to establish a connection in the En environment:

Dim En As rdoEnvironment
Set En = rdoCreateEnvironment("", "Fred", "Blond")

An rdoEnvironment object logically corresponds to an ODBC environment. You can refer to an rdoEnvironment object using the ODBC API functions, by referencing an rdoEnvironment object's hEnv property. However, because ODBC implementations only allow one environment handle per application, the actual lifetime of the ODBC environment handle is tied to the lifetime of the rdoEngine.


Using the rdoEnvironment Object to Manage Transactions

The BeginTrans, CommitTrans, and RollbackTrans methods are all supported on the rdoEnvironment object to begin, commit, or roll back ODBC distributed transactions. These methods are implemented using the Distributed Transaction Coordinator (DTC) on systems that support this technology. At this time, only Microsoft SQL Server 6.5 supports DTC. All connections in the rdoConnections collection for the rdoEnvironment object participate in the distributed transaction.

The Microsoft Distributed Transaction Coordinator (MS DTC)

This new feature of Microsoft SQL Server coordinates transactions across a network of Microsoft Windows NT– and Microsoft Windows 95–based systems. With MS DTC, SQL Server can:

  • Update data that resides on two or more SQL Server 6.5 systems.

  • Participate in transactions that are controlled by X/Open DTP XA-compliant transaction-processing monitors, such as Transarc's Encina, AT&T Global Information Solutions Company's Top End, and Tuxedo.

  • Provide an easy-to-use graphical user interface for distributed transactions.

You can also use MS DTC by using the Distributed Transaction Coordinator command from the Server menu.

Microsoft Distributed Transaction Coordinator addresses the challenges of processing transactions over a distributed set of software components that exist on a system of networked computers. MS DTC is fully integrated with Microsoft SQL Server 6.5 and provides a transaction manager in SQL Enterprise Manager at each computer that manages distributed transactions.

RDO DTC-Independent Transaction Management

If the DTC is not available, or if the current ODBC driver cannot handle a distributed transaction, these methods force RDO to perform a serialized transaction across all connections in the rdoConnections collection. But these transactions are not atomic — that is, operations executed against one rdoConnection could complete, but those against another might not. In this case, the committed operations are not rolled back. Because of this, their success or failure is not interdependent.

Because the rdoEnvironment object determines transaction scope in your application, committing an rdoEnvironment transaction commits all pending transactions on all open rdoConnection databases (which are opened on that rdoEnvironment object and their corresponding open rdoResultset objects). This does not imply a two-phase commit operation. It simply means that individual rdoConnection objects are instructed to commit any pending transactions — one at a time.

Note The ODBC transaction model does not support nested transactions. That is, you cannot execute a second BeginTrans method before the previous transaction is either committed or rolled back. However, if your ODBC data source supports it, you can use SQL statements to execute nested transactions. This is supported by all Microsoft SQL Server systems.

When you use ODBC transactions, transactions do not span connections: Transactions begun and committed on one connection do not affect transactions pending on other connections, even if both connections are made to the same server and database.

Trapping rdoEnvironment Transaction Events

The rdoEnvironment object fires an event whenever a transaction operation has completed. These events can be used to synchronize other processes with the transaction state. Basically, the BeginTrans, CommitTrans, and RollbackTrans event fire after the corresponding method has fired.


Managing Connections with the rdoEnvironment Object

You can use the rdoEnvironment object's OpenConnection method to open connections to the data source and create rdoConnection objects to manage and reference these connections. In an rdoEnvironment, you can open multiple connections (each accessing its own database), manage transactions, and establish security based on user names and passwords. For example, you can:

  • Create an rdoEnvironment object using the Name, Password, and UserName properties to establish a named, password-protected environment. The environment creates a scope in which you can open multiple connections and manage multiple ODBC transactions.

  • Use the OpenConnection method to establish one or more connections in that rdoEnvironment.

  • Use the BeginTrans, CommitTrans, and RollbackTrans methods to manage ODBC transaction processing within an rdoEnvironment.
  • Use several rdoEnvironment objects to conduct multiple, simultaneous, independent, and overlapping transactions.

  • Use the Dim X as New rdoConnection to create a stand-alone rdoConnection object and use the Add method to append it to the rdoConnections collection of an rdoEnvironment object.

  • Use the Remove method to remove a specified rdoConnection object from its parent rdoConnections collection.

  • Use the Close method to terminate an environment and the connection.

Note It is no longer always necessary to reference the rdoEnvironment object because rdoConnection objects can be created as stand-alone objects.


Establishing an RDO Connection

Before you can reference the data in a remote database, you must establish a connection to the data source. The source could be a remote database server, like SQL Server or Oracle, or another database that has a suitable ODBC driver.

There are a number of ways to establish connections with RDO, as described in the following topics. However, unlike DAO, RDO does not manage connections for your application — it simply collects parameters to be passed to the SQLDriverConnect function and calls the SQLDisconnect function to close the connection. RDO does not cache connections or attempt to share them based on similar DSN entries. When you use the RDO Close method to close a connection, it is closed immediately.

When you are ready to open a connection, the options available to you are as follows:

  • Use the RemoteData Control to establish a connection based on its properties and create an rdoConnection object as referenced by its Connection property.

  • Declare an rdoConnection object and use the rdoEnvironment object's OpenConnection method.

  • Create a stand-alone rdoConnection object using the Dim x As New syntax, set its properties, and use the EstablishConnection method.

  • Use the EstablishConnection on an existing rdoConnection object after having either created a stand-alone rdoConnection object, or after having used the Close method on an existing rdoConnection object.

Each of these techniques is designed to address specific programming requirements. For example, in situations where you need to submit the same query to a number of remote databases, you might choose to create a stand-alone rdoConnection object that can be assigned to a number of rdoQuery objects via the ActiveConnection property. In other situations, you might prefer the simplicity of the RemoteData control.

All of these techniques establish a physical link to a data source, like a specific SQL Server or Oracle database server. To establish a connection, you must provide the network location of the data source, the driver type, and a number of optional parameters used to identify the user to the data source.

Once you have established a connection, you can use it to:

  • Execute a query that returns one or more result sets using the OpenResultset method.

  • Execute an action query using the Execute method.

  • Create an rdoQuery object to subsequently execute a parameter query or stored procedure.

  • Define a query that exposes one or more stored procedures as methods of the rdoConnection object.

  • Associate the rdoConnection object with a specific rdoResultset object by setting the ActiveConnection property.

  • Add the rdoConnection object to a chosen rdoConnections collection by using the Add method.

  • Remove an rdoConnection object from its parent rdoConnections collection using the Remove method.


Providing Connection Strings to RDO

In order to tell the ODBC Driver Manager which driver to use and to tell the selected driver which data source to use, your code must provide a number of arguments in the form of a connect string or RemoteData Control property settings. In most cases, a connect string is used to direct RDO to a specific server, database, and user.

The connect string contains a series of semi-colon delimited arguments as defined by the ODBC interface — including the ODBC driver itself. That is, all ODBC drivers have specific argument requirements so you should consult the documentation included with the driver for specific information. This connect string is passed to the ODBC API SQLDriverConnect function along with the hEnv for the associated rdoEnvironment object.

Note If you are converting existing DAO or ODBCDirect code, be sure to remove the ODBC argument from the front of the connect string. In addition, the LOGINTIMEOUT argument is not supported — you have to use the LoginTimeout property of the rdoEnvironment object instead.

Generally, the connect string arguments include, but are not limited to, the following:

ODBC Connect Strings

ODBC connect string arguments | What the argument specifies
DSN= | Registered ODBC Data Source Name. The DSN keyword is not used if DRIVER is used.
UID= | User name as established on the server. In SQL Server this is the logon name.
PWD= | Password that corresponds with the logon name.
DATABASE= | Requested default database (optional).
SERVER= | The network name of the data source server. On a Microsoft Windows NT computer, "(local)" can be entered as the server. In this case, a local copy of SQL Server can be used, even when this is a non-networked version.
DRIVER= | Name of data source driver. Microsoft SQL Server uses {SQL Server. The DRIVER keyword is not used if DSN is used.
APP= | The name of the application (optional).
WSID= | The workstation ID. Typically, this is the network name of the computer on which the application resides (optional).
LANGUAGE= | The national language to be used by SQL Server (optional).

Note Some of these arguments are specific to SQL Server. For Oracle or other ODBC data sources, other arguments might be required or optional.

What's Not in the Connect String

Note that the connect string does not include arguments to set the following options:

Option | Default behavior
Network Address | Not needed. Handled by server name.
Network Protocol | Defaults to Named Pipes on SQL Server drivers.
OEMTOANSI switch | Defaults to "Off"
Use Trusted Connection Option | Defaults to "Off"
Generate Stored Procedure for Prepared Statement | Defaults to "Off"

All of these and other driver-dependent options are set in the Data Source Name dialog launched from the Windows Control Panel. Unless you use a DSN to establish your connection, you must accept the default settings for each of these arguments.

Providing the User ID and Password Values

To gain access to most ODBC data sources, you must provide a valid User ID and corresponding password. These values are initially registered by the system administrator but can be changed by your application. By setting appropriate prompt arguments of the OpenConnection or EstablishConnection methods, you can simply prompt the user for these values. However, this approach often leads to security problems because users are permitted any number of attempts to enter a correct user ID/password combination.

Another approach uses a special User ID and password assigned specifically to your application. This technique assumes that access to the application is limited to specific users who have permission to access the data source or workstation.

If you choose to use domain-managed security, you should use empty arguments for the UID and PWD parameters of the OpenConnection or EstablishConnection connect argument. This type of security passes your Windows NT logon ID and password to the data source. These values are collected from the user when they log in to Windows 95 or Windows NT. If your database administrator has implemented integrated or mixed security, this technique should permit you to log on to the data source — assuming you have been granted permission to do so. For example, using the preceding sample code, a domain-managed security connect argument is coded as follows:

Conn$ = "DSN=MyRemote;UID=;PWD=;"

Specifying the Default Database

The database associated with an rdoConnection object is determined initially by the DATABASE connect string argument. If this is not supplied, the default database assigned to the user by the server administrator or specified in the DSN is used.

You can also change the default database after the connection is made by executing a Transact SQL (TSQL) USE database query. In any case, you must have permission to access the selected database. If this permission is denied, a trappable error is raised.

For example, the following code changes the default database to "Pubs" on the cn connection:

cn.Execute "Use Pubs"

Passing the Connect String to RDO or the RemoteData Control

Once complete, the connect string is passed as one of the following:

  • The Connect argument of the OpenDatabase method.

  • The Connect property of the RemoteData Control.

  • The Connect property of the rdoConnection object when used with the EstablishConnection method.

The RemoteData control provides several specific properties to pass this information to RDO, but they are not needed if a connect string is passed in the RemoteData control Connect property. Values provided in the connect string override values provided in RemoteData control properties.


Using a Registered Data Source Name (DSN)

The simplest way to establish a connection is to use a registered Data Source Name (DSN) to hold information about the server. Since a DSN contains no information about the user or application, you need to provide that information either in the connect string or by using one of the prompt options that gathers the information from the user.

DSN entries are created either through the Windows Control Panel or the rdoRegisterDataSource method. However, using the Windows Data Sources dialog box is the preferred way to create, modify, or delete data source names. To open this dialog box, double-click 32-bit ODBC in the Windows Control Panel.

Registered DSN entries are referred to by name in the:

  • OpenConnection method's dsName argument.

  • Database property of the RemoteData control.

  • Connect string passed to the OpenConnection method as the connect argument.

  • Connect property of the rdoConnection object used with the EstablishConnection method.

  • Connect property of the RemoteData control.

The connect string always takes precedence.

Note that the DSN contains a number of arguments that can only be set using ODBC API functions programmatically, using the rdoRegisterDataSource function, or using the Windows Control Panel. These options cannot be set via connect string arguments. For example, the OEMTOANSI and NETWORK settings can't be set with connect string arguments.

You cannot use an ODBC driver that has not been installed and registered. This process is already part of the Visual Basic setup procedure but also needs to be included in your application's setup procedure.

The following example registers a SQL Server data source named Example, on a server named SEQUEL, and then opens the database WorkDB on that server.

Private Sub RegisterDataSource()
Dim en As rdoEnvironment
Dim cnTest As rdoConnection
Dim strAttribs As String
' Build keywords string.
strAttribs = "Description=" _
      & "SQL Server on server SEQUEL" _
   & Chr$(13) & "OemToAnsi=No" _
   & Chr$(13) & "SERVER=SEQUEL" _
   & Chr$(13) & "Network=DBNMPNTW" _
   & Chr$(13) & "Database=WorkDB" _
   & Chr$(13) & "Address=\\SEQUEL\PIPE\SQL\QUERY"

' Create new registered DSN.
rdoEngine.rdoRegisterDataSource "Example", _
      "SQL Server", True, strAttribs
' Open the database.
Set en = rdoEngine.rdoEnvironments(0)
Set cnTest = en.OpenConnection( _
   dsname:="Example", _
   Prompt:=rdDriverNoPrompt, _
   Connect:="UID=;PWD=;")
End Sub


Creating DSN-less RDO Connections

Another approach to consider when establishing RDO connections is to provide all required information in the Connect argument of the OpenConnection method. This approach affords a number of advantages:

  • This can make the creation of a DSN unnecessary, which can simplify setup and installation of client applications.

  • The DSN does not have to be located in the system registry, which results in faster connect times.

  • You have more control of the specific server and other connection parameters, which can increase application and system security.

If you choose to use a DSN-less connection, you must include the name of the server and driver along with any other user-specific information in the connect string. However, if you choose this option, you can only use the default OEMTOANSI (Off), NETWORK (named pipes), and other settings that can only be set when creating a DSN.

If you choose to create a DSN-less connection, you must pass a zero-length string as the dsName argument of the OpenConnection method, or the Database property of the RemoteData control. This notifies the ODBC driver that you want to create a DSN-less connection. In addition, note that the order of the connect string arguments is significant. The DSN argument must be provided after the SERVER and DRIVER arguments, as shown below.

The following example illustrates creation of a DSN-less connection to establish a connection to a Microsoft SQL Server database named MyServer:

Dim Cn As rdoConnection
Dim En as rdoEnvironment, Conn As String
Set En = rdoEnvironments(0)
Conn$ = "UID=Holly;PWD=Huskador;" _
   & "DATABASE=MyDb;" _
   & "SERVER=MyServer;" _
   & "DRIVER={SQL SERVER};DSN='';"
Set Cn = En.OpenConnection(dsName:="", _
   prompt:=rdDriverNoPrompt, _
   connect:=Conn$)


Setting rdoConnection Options

Before you use the OpenConnection or EstablishConnection methods, you can set a number of properties and options that affect how the connection is made and how queries using these connections are processed. Generally, these options are determined by rdoEnvironment properties. However, if you create a stand-alone rdoConnection, you must preset the following rdoConnection properties with any variations from the default values before making the connection.

The following list details rdoConnection options that must be set before a connection is attempted, their default values, and what function they perform.

rdoConnection Options

rdoConnection property | Default | Determines
LoginTimeout | 15 | How many seconds to wait before abandoning the connection attempt.
CursorDriver | rdUseIfNeeded | Which cursor library (if any) is used for all queries associated with the rdoConnection object.
Connect | "" | The ODBC parameters used to make the connection — the connect string.
Name | "" | The DSN name to be used for the connection.


Using rdoConnection Object Events

The rdoConnection object triggers a series of event procedures that can be used to make the job of managing connections and the queries associated with the connections easier. These events are triggered under the following conditions:

rdoConnection events | When fired
BeforeConnect | Before the call to the ODBC SQLDriverConnect function.
Connect | After the connection operation completes — successfully or not.
Disconnect | After the connection is disconnected.
QueryComplete | After an asynchronous query completes.
QueryTimeout | After any query's timeout period has exceeded the value of the rdoConnection object's QueryTimeout property.
WillExecute | Before RDO attempts to execute a query.

The QueryComplete, QueryTimeout, and WillExecute events fire for all queries executed on the associated rdoConnection. This includes those queries executed via the OpenResultset or Execute methods, as well as those executed from an associated rdoQuery object. The Query argument is an object reference indicating which query triggered the event. Using this argument, you can write a single event handler for all queries on the connection, but still customize the handler for specific queries. When executing queries against the rdoConnection object itself, RDO creates an rdoQuery object internally, and a reference to this internal rdoQuery is passed as the Query argument.

Using the BeforeConnect Event Handler

The BeforeConnect event gives you the opportunity to alter the connect string being passed to the ODBC SQLDriverConnect function. Your code can add a workstation ID or other parameter that might not have been supplied earlier. By filtering the connect arguments, you can prevent users from browsing unauthorized data sources or user names.

Using the Connect Event Handler

The Connect event permits your code to know when the connection operation has been completed for a specific connection object — that is, this event fires whether or not the connection operation succeeds. Each rdoConnection object can be declared to expose this event and all other connection-specific events.

The Connect event is especially helpful in situations where connections can take awhile to connect — as in wide area networks. Using this event is recommended over polling the rdoConnection object's StillConnecting property when using the rdAsyncEnable option of the OpenConnection or EstablishConnection methods. If the connect operation fails to establish a connection, the ErrorOccurred argument is set to True. At this point your code needs to examine the rdoErrors collection to determine the cause of the failure.

Using the QueryComplete Event Handler

When using asynchronous queries, you should always set up an event handler for the QueryComplete event that is fired when the query is complete. This event handler is established on a connection basis so all queries on the connection fire the same QueryComplete event.

While most ODBC drivers do not support more than one operation at a time, some do, so your QueryComplete event handler needs to be cognizant of which query has completed. The QueryComplete event is passed an object reference to the rdoQuery, which can be examined to indicate the query's name or other properties. If the query fails to establish a connection, the ErrorOccurred argument is set to True. At this point your code needs to examine the rdoErrors collection to determine the cause of the failure.

Using the QueryTimeout Event Handler

Not all queries complete immediately or even within a few minutes. However, unless you indicate the expected length of the query in seconds by setting the QueryTimeout property, the QueryTimeout event will fire after a set number of seconds; the default is 30 seconds. If your code passes False to the Cancel argument of the QueryTimeout event, RDO restarts the query timeout clock using the number of seconds in the QueryTimeout property. This feature can be especially useful when working with wide area networks that do not always execute queries in the same length of time.

Note The QueryTimeout property is passed to the ODBC drivers when a query is started, so changing the property has no effect until you start another query.

Using the WillExecute Event Handler

This event is fired before the execution of a query, regardless of whether it is an action or row-returning query. You can trap this event to disallow the execution of certain queries, or to make last-minute adjustments to the SQL string.

The Cancel argument lets you disallow the query. For example, you can prescreen the query to make sure it has enough in the WHERE clause to avoid a table scan. In addition, you might wish to prohibit users from searching for customers with the last name of "Smith" without also providing a first name or street address. The Cancel argument defaults to False, but if you set it to True, the query will not be executed and RDO generates a trappable error indicating that the query was canceled.

An important aspect of this event is the ability to substitute or augment the operation with your own code. That is, you could execute your own set of operations including stored procedures to carry out the functions being performed. For example, if your remote database only permits updates through use of protected stored procedures, you can trap the WillExecute event to fire specific parameterized stored procedures to perform the update and use the Cancel argument to defeat the automatic operation being initiated.


Opening Connections Asynchronously

In some cases, especially when working with Internet, Wide Area Network (WAN), or Remote Access Service (RAS) connections, the time required to establish a connection can increase dramatically. While a few seconds should be enough for a typical LAN connection, a WAN might take several minutes or more to connect. There are several ways to pre-empt problems when attempting to make these connections:

  • Set the LoginTimeout property to a high-enough value to account for the delay.

  • Use the rdAsyncEnable option to return control to your application before the connection is established so that the user can be informed that the connection is being attempted without blocking the application.

  • Establish an event procedure to capture the Connect event instead of polling with the StillConnecting property.

  • Use the BeforeConnect event to warn the user that the connection might take an exceptionally long time.

The most important aspect of these techniques is to keep the user informed and to prevent the application from blocking. If your application, through no fault of its own, seems to lock up while attempting to establish a connection, the user will have a tendency to try to cancel the operation in any way that presents itself — including rebooting the computer.


Working with Stand-Alone rdoConnection Objects

It is not necessary to create a connection using an rdoEnvironment object, because you can simply declare a free-standing rdoConnection object using the Dim As New syntax. For example, the following code creates a stand-alone rdoConnection object named MyCn.

Dim MyCn As New rdoConnection

By using the User Connection Designer, you can also create your own Visual Basic class that inherits from the rdoConnection object to extend the object with functionality of your own design.

Once you create a stand-alone connection, you can set the Connect property of your new rdoConnection object to address a specific server, cursor driver, and other properties as described below. Once these properties are initialized, you can use the EstablishConnection method to connect with the specified server.

For example, to create a stand-alone rdoConnection object and establish a connection to a server specified by the CustomerDB DSN, you can use the following code:

Dim MyCn As New rdoConnection
With MyCn
   .Connect = "DSN=CustomerDB;DATABASE=MAILING;" _
            & "UID=;PWD=;"
   .CursorDriver = rdUseNone
   .LoginTimeout = 5
   .EstablishConnection (rdDriverNoPrompt)
End With

If you want to associate your new stand-alone rdoConnection with a specific rdoEnvironment, you must use the Add method to add it to the rdoConnections collection associated with a selected rdoEnvironment object. For example, the following code adds the MyCn connection to the default rdoEnvironment object:

rdoEnvironments(0).rdoConnections.Add MyCn

You can remove members from the rdoEnvironments collection by using the Remove method.

For More Information See "EstablishConnection Method" in the Language Reference" for details on using stand-alone connections.


Using an rdoConnection Object

Once the rdoConnection object is created, you can use it to:

  • Add stand-alone rdoConnection objects to a specific rdoEnvironment transaction scope by using the Add method against a selected rdoConnections collection.

  • Create rdoResultset or rdoQuery objects using the OpenResultset or CreateQuery method, respectively.

  • Execute action queries to update database data, modify the schema, or perform administrative functions.

  • Execute stored procedures and manage their result sets, output parameters, and return values.

  • Disconnect from the data source and free its resources using the Close method against the rdoConnection object.

Note that the rdoConnection object does not set the scope for transactions; this is maintained at the rdoEnvironment level. You cannot use the transaction methods like BeginTrans, CommitTrans, or RollbackTrans against the rdoConnection object — just a specific rdoEnvironment object.


Failing to Connect to an RDO Data Source

You might be unable to establish a connection for a variety of reasons, including:

  • Lack of permission on the data source or network.

  • Improper network connection or permissions.

  • Missing or disabled data source.

  • Missing or improperly installed or registered drivers. Both ODBC and network protocol drivers must be installed and registered.

  • Too many connections have already been established by your application or other applications.

  • The network or remote server is down or too busy to respond before the time specified by the LoginTimeout property has expired.

For example, when connecting to Microsoft SQL Server, Oracle, or other data sources, the number of simultaneous connections permitted might be limited by license agreements, resource constraints, or by database settings. Check with your server administrator if you suspect that all available connections are allocated.

In any case, your code should include comprehensive error handlers to deal with all of these and other contingencies that might apply to your situation. While some of these problems are due to setup issues, some are transitory — that is, they will correct themselves once resources are freed on the remote system or the amount of LAN or WAN traffic subsides.

Errors trapped by RDO are usually returned as generic ODBC errors that tell you very little about the root cause of the errors. However, details about the errors are saved in the rdoErrors collection. Each rdoError in the rdoErrors collection contains specific information about what went wrong. Examining the rdoError object properties can yield vital information about the specific error, including a description string, a native error number (as would be returned by SQL Server), and other values specific to the ODBC API error management scheme.

For More Information See the Language Reference for details on the structure and properties of the rdoError object.


Using the rdoTable Object

You can use the rdoTable object to map the tables and columns of a data source or create rdoResultset objects from all rows in the table — which is not recommended with RDO. There are very few cases where it is necessary to retrieve all rows from a database table into workstation memory. Many remote database tables are far too large to be downloaded to the workstation.

If you need to examine the table structure exposed by a data source or examine column detail, you can use the rdoTables collection. However, to improve performance, no table meta data is requested from the data source until the rdoTables collection is referenced. Once an rdoConnection is open, you can enumerate the tables and place their names in a ListBox control, as in the following example (which assumes that cn is an open rdoConnection):

Dim tb As rdoTable
For Each tb in cn.rdoTables
   List1.AddItem tb.Name
Next

Each rdoTable object contains an rdoColumns collection that contains details about the data type and size of each column. While it is possible to create an rdoResultset against the rdoTable object, this query can only return all of the rows and is not recommended.


Closing Unneeded RDO Connections

When you no longer need access to the remote server, you should consider closing the connection with the Close method. While it does not always make sense to close a connection before an application is terminated, schemes used to maximize the number of simultaneous users connected to a system often close connections when they become idle or are no longer needed. These applications subsequently reopen the connections as required.

When your connection is dropped, any instance-owned objects on the server are released. For example, server-side cursors or any objects created in TempDB are dropped by the server. If you must maintain access to these objects, you cannot close your connection.

RDO does not manage connections for you in the sense that it does not cache recently closed connections in anticipation of their use at a later time. When you use the Close method to close a connection, or the last reference to an rdoConnection object goes out of scope, the connection is closed — permanently and immediately.


Using RDO to Submit Queries

The primary reason for creating most database front-ends is to retrieve data, manipulate it, display it, and submit changes. Most of the RDO code you write involves building queries and processing the result sets to perform these basic operations.

When you need data from the remote database, you submit a query that specifically calls for a set of data rows. When it comes time to update your data, you submit more queries that pass along new data or changes to the remote database. If your application needs to perform administrative functions, you submit queries that contain high-level instructions to run server-side utilities to establish new users, or run database repair or configuration procedures. In essence, all of the interactive aspects of RDO involve submitting queries. You are basically asking questions and working with the answers that are returned in the form of result sets.

Generally, RDO query processing can be broken down into two parts:

  • Submitting a query composed of an SQL statement that describes a result set, or references a stored procedure.

  • Working with the resulting rows, parameters and return values — the result sets.

Query submission involves building an SQL statement, including any needed parameters, and sending the query to the remote server for processing. When deciding how to submit your query, you must decide what form it is to take when it is returned. Depending on the needs of your application, you should consider the following questions:

  • Does your application need to scroll or browse through the data? If you must scroll, do you need to scroll in both directions?

  • Will you need to update the data? If so, can you use action queries to make the changes? Does your user have permission to change data?

  • How many other users will be attempting to work on the same data at the same time? Do you have control over these other applications? Will they cooperate with your data sharing scheme?

  • How many rows will return from the query? Does your system have the capacity to store these rows? Does the network have the bandwidth to transmit these rows?

  • How are the rows returned — as bookmarks or as static data?

  • Must you pass parameters to the query?

  • Does the query return rows, output parameters, or return values, or does it simply perform an action?

  • Is the query likely to be repeated several times over the course of the application or executed only a few times?

The following topics address these questions to help you choose the right strategy to fetch your data in a variety of situations.

Managing RDO Queries

Once you have opened a connection, you can submit queries to the remote server for execution. No matter which technique you use, RDO will execute your queries and retrieve the result sets with one of two ODBC API function sets as described below:

  • If you use queries that are run only once, use the OpenResultset or Execute method to execute fixed SQL queries against the rdoConnection object to create rdoResultset objects or execute action queries. This option routes your queries to the ODBC API SQLExecDirect function. You can also force the use of the SQLExecDirect function by using the rdExecDirect option with the Execute or OpenResultset methods.

  • If you use queries that are run more than once and might require parameters, use the CreateQuery method to instantiate an rdoQuery object that can be reused as needed and that allows changes to one or more parameters each time the query is executed. Once the rdoQuery is created, use the OpenResultset or Execute method against the rdoQuery to create an rdoResultset or execute an action query. To change the query parameters, change the rdoParameter object settings. This option routes your queries to the ODBC API SQLPrepare and SQLExecute functions.

  • If you use stored procedures to access data or perform action queries, you can use the User Connection Designer to link specific stored procedures and create customized rdoQuery objects to access them. This can also be done in code. This option routes your queries to the ODBC API SQLPrepare and SQLExecute functions. Unlike DAO queries that are stored in the database, these rdoQuery objects are re-created each time your program executes.

Note The outdated Visual Basic version 4.0 rdoPreparedStatement object is still supported in Visual Basic version 6.0 for backward compatibility, but is not recommended for future development. You are encouraged to replace all references to rdoPreparedStatement objects with rdoQuery objects.


Creating rdoResultset Objects

While RDO has a number of options to make the process of retrieving result sets and returned parameters easier, all of them ultimately result in the creation of an rdoResultset object. For example, all of the following techniques can be used to create and manage rdoResultset objects:

  • Use the OpenResultset method against an rdoConnection object. This is used to submit a query that you do not expect to execute more than once in your application. The returned rows are placed in an rdoResultset object.

  • Use the OpenResultset method against an rdoQuery object. This technique is used to submit a query that you expect to execute more than once in your application or that requires one or more parameters. The returned rows are placed in an rdoResultset object and any parameters returned from the query are referenced via the rdoQuery object's rdoParameters collection.

  • Create a stand-alone rdoQuery object, set its properties, and associate it with a specific connection using the ActiveConnection property. An rdoResultset object can then be created against the enabled rdoQuery using the OpenResultset method. The ActiveConnection property can be changed to refer to a different connection and the query re-executed without having to create another rdoResultset object.

For More Information See "Creating RDO Parameter Queries" later in this chapter.


Using the rdoOpenResultset Method

Using the OpenResultset Method

No matter which type of query you execute, or which type of result set you create, you must use the OpenResultset method to build an rdoResultset to fetch and navigate through the data rows. The connection and specific cursor type used to create the rdoResultset depends on the properties of the object referenced with the OpenResultset method. For example, if you execute the OpenResultset against the rdoQuery object, RDO uses the connection specified in its ActiveConnection property and the cursor type specified in its CursorDriver property.

The OpenResultset method accepts the following arguments:

  • When you use the OpenResultset method with an rdoConnection object, you must specify the name argument — basically the name argument string specifies an SQL query. When you use OpenResultset against an rdoQuery object, the SQL statement is drawn from the rdoQuery object's SQL property so the name argument is not needed. When OpenResultset is used against the rdoTable object, the query is assumed to include all rows from the specified table. If your SQL query needs to include parameters, see "Creating RDO Parameter Queries" later in this chapter for more information about choosing the right SQL syntax for parameter queries.

  • The type argument specifies the type of cursor — unless you have disabled cursor creation on this connection by specifying rdUseNone for the CursorDriver property. If you do not indicate a cursor type, rdOpenForwardOnly is assumed. The type argument can also specify keyset, dynamic, static, or forward-only cursor types.

    Note Not all cursor libraries are capable of implementing all cursor types. For example, the ODBC client-side cursor library (rdUseODBC) is incapable of creating keyset or dynamic cursors. When a driver is incapable of creating a cursor, the driver generally uses a fall-back strategy that selects another cursor that it is capable of creating.

  • The locktype argument specifies the type of locking used to support concurrency. Generally, this option determines how and when pages and rows are locked by the remote server. For example, using the locktype argument, you can select several types of optimistic locking, one type of pessimistic locking, or no locking at all, as when you select rdConcurReadOnly (the default). For more information, see "OpenResultset Method" in the Language Reference".

  • The options argument specifies if the query should be executed asynchronously and the ODBC function used to execute the query. By default, the query is executed synchronously and the SQLPrepare and SQLExecute API functions are used.

Choosing a Cursor Type

Using arguments of the OpenResultset method or properties of the rdoQuery object, you can specify the type of cursor (if any) and other attributes of the rdoResultset object. However, in many cases, cursors are not the best way to retrieve data as they negatively affect performance and resource utilization.

For More Information See "OpenResultset Method," "Type Property," and "SQL Property" in the Language Reference".


Coding SQL Statements

The SQL property contains the structured query language statement that determines how rows are selected, grouped, and ordered when you execute a query. You can use a query to select rows to include an rdoResultset object. You can also define action queries to modify data without returning rows.

You cannot provide a table name at design time for the SQL property of the RemoteData control — as you can when using DAO. However, you can either use a simple query like SELECT * FROM <table>, or at run time populate the rdoTables collection and use one of the table names returned in the collection. The rdoTables collection is populated as soon as it is associated with an active connection and referenced.

Your query's SQL syntax must conform to the SQL dialect as defined by the data source query processor. The SQL dialect supported by the ODBC interface is defined by the X/Open standard. Generally, a driver scans an SQL statement looking for specific escape sequences that are used to identify nonstandard operands like timestamp, literals, and functions.

When you need to return rows from a query, you generally provide a SELECT statement in the SQL property. The SELECT statement specifies:

  • The name of each column to return or "*" to indicate all columns of the specified tables are to be returned. Ambiguous column names must be addressed to include the table name as needed. You can also specify aggregate expressions to perform arithmetic or other functions on the columns selected. Aggregate or computed columns must also be aliased to provide name references for them in the rdoColumns collection.

  • The name of each table to be searched for the information requested. If you specify more than one table, you should provide a WHERE clause to indicate which column(s) are used to cross-reference the information in the tables. Generally, these columns have the same name and meaning. For example the CustomerID column in the Customers table and the Orders table might be used to join the two tables on a common column.

  • Optionally, a WHERE clause to specify how to join the tables specified and how to limit or filter the number and types of rows returned. You can use user-supplied parameters in the WHERE clause to specify different sets of information from query to query. If you need to provide WHERE clause criteria at run time, you must create a parameter query.

  • Optionally, other clauses such as ORDER BY to set a particular order for the rows or GROUP BY to structure the rows in related sets.

Each SQL dialect supports different syntax and different ancillary clauses. See the documentation provided with your remote server for more details.

For More Information See "Creating RDO Parameter Queries."


Creating RDO Parameter Queries

If the SQL query you need to execute includes one or more parameters in the WHERE clause, you can use the rdoQuery object to run it and manage the parameters for each execution. This technique is especially useful when executing queries that are run repeatedly or against a number of connections — and especially when executing parameterized stored procedures.

Tip You can also build up your own queries, concatenating the parameters together to form a complete SQL statement. In some cases, this approach might be the only way that a parameter query can be created — especially in cases where the query is complex or uses the remote database syntax in an unusual way.

In any case, the SQL statement you submit must use the correct syntax. Many problems associated with parameter queries result from improperly coding the native SQL required by the remote server or the ODBC SQL syntax as described below. Queries submitted with incorrect syntax can result in a variety of problems, including syntax errors returned from the remote engine or RDO's inability to create the rdoParameters collection.

Managing Parameters with the rdoParameters Collection

When you want RDO to manage the parameters for you using the rdoParameters collection, you include a question mark for each parameter in your SQL statement. The "?" acts as a placeholder for input, output, and input/output query parameters; your code indicates which is which by setting the Direction property. RDO and the ODBC interface automatically manage these parameters and bind each to an rdoParameter object with a predefined data type. In some cases, your code might have to force a specific data type for certain parameters. This is especially true when your query contains a expression whose arguments are passed as parameters.

When all parameters have been marked and identified, RDO and the ODBC interface automatically create a driver-specific SQL statement and an rdoParameters collection to manage the individual parameter values and data types. For the most part, you do not have to worry about quoting strings used as arguments or other special formatting.

Note When the rdoQuery object is created, no check is made for proper syntax. It is only when the query is executed or the rdoParameters collection is accessed that the query is compiled and its parameters evaluated. If the statement is not coded correctly, you can trigger a trappable 40054 "Invalid parameter was passed" error, or some other ODBC error. In some cases, the rdoParameters collection is not created, so when it is referenced, you might get a trappable error indicating that the object does not exist.

Tip While you might have discovered ODBC drivers for DAO (Access) databases in your list of available ODBC drivers, they are not ODBC Level II compliant. You can use RDO to submit queries and return result sets from this driver, but it is incapable of creating rdoParameter objects or managing query parameters as supported by Level II compliant drivers.

Choosing the Right SQL Syntax for Parameter Queries

When coding the SQL property of an rdoQuery object or the name argument of the OpenResultset method, you can choose between one of three syntax styles to code your parameter query:

  • Concatenated Strings. Your code builds up the SQL statement and its parameters using the Visual Basic concatenation (&) operator. This statement can be passed to the name argument of the OpenResultset method or the rdoQuery object's SQL property. A concatenated string parameter query might look like this:
    sSQL = "Select Name, Age From Animals " _
& " Where Weight > " & WeightWanted.Text _
& " and Type = '" & TypeWanted.Text & "'"
  • Native SQL syntax. The SQL syntax used by the remote server. You can execute your own query or stored procedure and pass in parameters by concatenation, placeholders, or both. The parameters marked with placeholders are managed by RDO as rdoParameter objects. A SQL parameter query might look like this:
    sSQL = "Select AU_LName from Authors" _
& " Where AU_Fname = ?"

    – or –

    sSQL = "Execute MyStoredProc 'Arg1', 450, '" _
& Text1 & "'"

    – or –

    sSQL = "Execute MyStoredProc ?, ?, ?"
  • ODBC CALL syntax. Designed to call stored procedures that return a return status or output parameters. A placeholder can be defined for each input, output, or input/output parameter; the placeholders are automatically mapped to rdoParameter objects. You can also mix in concatenated operators as needed. An ODBC CALL parameter query might look like this:
    sSQL = "{call ParameterTest (?,?,?) }"

    – or –

    sSQL = "{? = call ParameterTest (?,?,?) }"

    – or –

    sSQL = "{? = call CountAnimals (?, ?, 14, 'Pig')}

Note The SQL Server ODBC driver requires that all nonbound parameters (the parameters you concatenate into the query in code) appear to the right of all placeholder parameters (those marked with a ?). If they don't, a trappable error occurs indicating "Wrong number of parameters."

Benefits and Limitations of the ODBC CALL Syntax

There are a number of benefits to using the ODBC CALL syntax. For instance, ODBC uses an Open Data Systems Remote Procedure Call (ODS RPC) to perform the query. The parameters are passed in their native format and don't have to be parsed or converted into other data types. It also means that ODBC does not have to "prepare" the query for processing, as it already exists in the form of a stored procedure on the remote server. This makes these calls more efficient and allows for better portability across databases.

Using rdExecDirect with Parameter Queries

The rdExecDirect option forces RDO to use the ODBC API SQLExecDirect function when executing the procedure. This bypasses the ODBC API SQLPrepare step, which is used to create a temporary procedure to execute the query. This option can be used in situations where the SQL syntax required is acceptable to the remote server but unacceptable to the ODBC interface. However, when executing a stored procedure parameter query, you should not use the rdExecDirect option because it prevents proper type binding of the parameters.

Note In some cases, the temporary stored procedures created by the ODBC interface might not be removed until the connection is closed. Using the rdExecDirect option can eliminate this problem.

Summarizing the Syntax Options

The following table summarizes the options available when using each of the three syntax styles:

Syntax Options

Feature | Native SQL syntax | ODBC Call syntax | Concatenated strings
Can pass native SQL that does not reference a stored procedure | Yes | No | Yes
Can execute stored procedures | Yes | Yes | Yes
Can use ? placeholders for parameters | Yes | Yes | No
Manage return value | No | Yes | No
Manage output arguments | No | Yes | No
SQL statement can include multiple Select statements? | Yes | No | Yes

Note RDO's ability to manage the parameters of your query in the RDO parameters collection is gated by the ODBC interface's ability to correctly parse the query and determine correct data types for each parameter. In some cases, it is impossible for the ODBC driver manager to properly identify each parameter of an SQL statement. In these cases, converting the statement into a stored procedure, even temporarily, might enable an otherwise unusable query.

Tip While the ODBC Call syntax can be used in situations where you pass no arguments, or have no returned arguments, you should generally use the it when you need to capture the stored procedure return status and output arguments.

Coding a Typical Parameter Query

A parameter query simply substitutes user-supplied or application-supplied parameters into an ordinary query. While this query is usually a SELECT statement, it could be an INSERT, UPDATE, or DELETE query as well. The following example illustrates how to code a simple SELECT query with a single parameter. The query looks up authors by name from the Pubs sample database.

First, set up an SQL query that marks each parameter using the ? parameter marker.

QSQL$ = "SELECT * FROM Authors WHERE Au_Lname = ?"

Next, create an rdoQuery object to manage the query and its parameters.

Set PSAuthors = cn.CreateQuery("",QSQL$)

Next, use the following code to insert the value entered by the user (Text1.Text) into the query.

PSAuthors.rdoParameters(0) = Text1.Text

Note that the rdoParameters object can be implied here, as it is the default collection of the rdoQuery object. The equivalent code would be:

PSAuthors(0) = Text1.Text

Next, create an rdoResultset to fetch the qualifying rows (those whose last name match the parameter value).

Set MyRs = CpwPSAuthors.OpenResultset()

If the user changes the parameter value in Text1.Text, you can re-apply the new parameter and re-execute the query by using the Requery method against the rdoResultset(MyRs) without having to rebuild the rdoQuery object.

PSAuthors(0) = Text1.Text
MyRs.Requery

When RDO executes the Requery method, it refreshes the parameter value(s) in the rdoParameters collection bound to the query parameters, flushes the current result set, sends the query to the data source for execution, and creates a new rdoResultset.

Keep in mind that when the query is first created, RDO and the ODBC layers create a temporary stored procedure on the remote server designed to accept the parameters. Each time the query is executed, this temporary query is simply passed the new argument(s) and executed.

Tip If you used the rdAsyncEnable option with the OpenResultset method, use it also with the Requery method.

Note When executing stored procedures that do not require parameters, do not include the parenthesis in the SQL statement. For example, to execute the "MySP" procedure, which takes no parameters, use the following syntax:

{Call MySP }

If the user changes the parameter value, you can re-apply the parameter value and re-execute the query by using the Requery method against the rdoResultset (MyRs).

Cpw(0) = Text1.Text
MyRs.Requery

Concatenating Parameters

You can also specify parameters in any SQL query by concatenating the parameters to the SQL statement string. For example, to submit a query using this technique, you can use the following code:

QSQL = "SELECT * FROM Authors WHERE Au_Lname = '" _
 & Text.Text & "'"
Set MyRs = Cn.OpenResultSet(QSQL)

In this case, the rdoParameters collection is not created and cannot be referenced. To change the query parameter, you must rebuild the SQL statement with the new parameter value each time the query is executed or before you use the Requery method. In addition, unless you use the rdExecDirect option, RDO creates a new temporary stored procedure to execute the query each time you use the OpenResultset method.

For More Information See "rdoQuery Object," "rdoParameter Object," "Requery Method," and "OpenResultset Method" in the Language Reference". For additional information on stored procedures, see "Using RDO to Execute Stored Procedures."


Using RDO to Execute Stored Procedures

Stored procedures are one of the most effective ways to manage and protect complex relational databases. They play a vital role in systems that support large databases, large numbers of users, or both. Using stored procedures, you can protect critical data from random access, those with malicious intent, and those with innocent intent but destructive tendencies. In many sites, stored procedures are the only way to access or update data — access to base tables is strictly prohibited.

One of the most powerful and important features of RDO is its ability to execute and manage the results returned by stored procedures, regardless of their complexity. The following topics discuss how to best use this feature.

Stored procedures usually require one or more parameters, and while some stored procedures return rows, not all do. RDO provides a number of ways to handle these variations; however, if you must capture the stored procedure return status or output parameters, you must use the ODBC CALL syntax as described in "Creating RDO Parameter Queries."

A stored procedure can return any number of result sets — and stored procedures can invoke other stored procedures. Your code needs to be prepared to manage all of these result sets, some containing rows, some just returning parameters, some returning only the number of rows affected, and some all three.


Coding a Typical Stored Procedure with RDO

This example walks you through the steps needed to execute a stored procedure that takes two parameters. Stored procedure parameters can be input, output, or both input and output. In most cases, the ODBC driver can automatically determine the type of parameter and correctly assign it to the Direction property. Our example executes the sp_password procedure, which can be used in SQL Server systems to change a user’s password. This procedure assumes the password to be changed belongs to the current user because only the system administrator can change other users' passwords.

This procedure query accepts two input parameters and passes back a return value. You could use the Execute method to run this query, but the return value would be lost: You would have no easy way of telling if the password change was successful. To capture the return value and create an rdoQuery that can be used repeatedly to change the user password, write code as shown below:

Dim CPw As rdoQuery, QSQL As String
QSQL$ = "{ ? = call sp_password (?, ?) }"

The next line of code creates the rdoQuery and names it SetPassword. The SQL property is set with the QSQL query defined above. This line only needs to be executed once. The new rdoQuery object is automatically appended to the rdoQueries collection, where it can be recalled later.

Set CPw = cn.CreateQuery("SetPassword",QSQL$)

The next step sets the Direction property to indicate that the parameter is used for input, output, or both. The default Direction is rdParamInput, but in most cases it is unnecessary to set the Direction property at all, because the ODBC driver can determine this value from the stored procedure's definition.

The ordinal number of the parameters is based on the order in which they appear in the SQL statement. In this case, the “0th” parameter is the return value (? = ), the “1st” is the first input parameter, and the “2nd” is the second input parameter. As you can see, the rdoParameters collection is zero-based. The code shown below is actually referencing the rdoParameters collection, which is the default collection for the rdoQuery object.

Cpw.rdoParameters(0).Direction = rdParamReturnValue

This next step sets the two input parameters that RDO inserts into the query when it is executed. Note that rdoParameters is the default collection and is implied in the following code.

Cpw(1) = "clyde"      ' Set the first input parameter.
Cpw(2) = "framis"      ' Set the second input parameter.

Once the parameter direction and values are set, you can use the Execute method to run the query if it does not return rows, or the OpenResultset method if the procedure contains one or more SELECT statements:

Cpw.Execute()

Once the procedure is executed, you can examine the rdoParameters collection for the returned value:

If Cpw.rdoParameters(0) <> 0 Then
   Msgbox "Could not change password"
End If


Capturing Stored Procedure Output Parameters

Using the same technique used with parameter queries in SELECT statements, you can also capture the output parameters from a procedure.

To capture output parameters

  1. Use ODBC escape syntax to establish placeholders for the output and input parameters and return values.

  2. For ODBC data sources that don't automatically determine the direction, your code must set the Direction property of each rdoParameter object to indicate how the parameter is used.

  3. For ODBC data sources that don't automatically (or correctly) determine the data type for individual parameters, your code must set the Type property of each rdoParameter object to indicate its data type.

  4. Execute the query.

When the query has completed processing, retrieve the output parameter values from the rdoParameters collection.

Coding Output Parameter Stored Procedures

This example executes a stored procedure that expects two input parameters and returns two output parameters along with a return value parameter. Note that the SQL query uses ODBC syntax in the rdoQuery, which is required if you intend to execute stored procedures that return output arguments.

Dim SQL As String, MyOutputVal1 As Variant
Dim MyOutputVal2 As Variant, MyRetValue As Variant _
rs As rdoResultset
Dim Ps As rdoQuery

' Use named arguments to open the connection.
Dim cn as New rdoConnection
With cn
   .Connect = "dsn=Sequel;uid=;pwd=;database=workdb"
   .EstablishConnection Prompt:=rdDriverNoPrompt
End With

' Use ODBC parameter argument syntax.
' Note each argument is identified
' with a ? character – one for the ReturnValue,
' one for each of the input arguments
' and one for each of the output arguments.

SQL = "{? = call TestOutputRS (?, ?, ?, ?) }"

' Create reusable rdoQuery.
Set Ps = cn.CreateQuery("PsTest", SQL)

' Set Parameter "direction" for each output
' and return value parameter.

Ps(0).Direction = rdParamReturnValue
Ps(3).Direction = rdParamOutput
Ps(4).Direction = rdParamOutput

' Set the input argument values.
Ps.rdoParameters(1) = "Test%"
Ps.rdoParameters(2) = 1

' Create the result set and populate the Ps values.
Set rs = Ps.OpenResultset(rdOpenStatic)

MyRetValue = Ps(0)    ' The return value argument.
MyOutputVal1 = Ps(3) ' The first output parameter.
MyOutputVal2 = Ps(4) ' The second output parameter.


Creating RDO Cursors

Not all queries you create need to be returned in the form of a cursor. As a matter of fact, cursors are a particularly expensive way to fetch data and rarely used in production-class applications. If you set the rdoDefaultCursorDriver property to rdUseNone, all result sets created by your application will be created as if you used the OpenResultset method with the rdOpenForwardOnly and rdConcurReadOnly options set, and with RowsetSize set to 1. This is often the most efficient way to pass data from the remote server to your application.

It is also possible to create low-impact result sets that are also updatable through use of the Edit/Update methods. However, in most situations this approach is impractical because the base tables are not directly updatable, so creating an updatable cursor is not possible. Whenever you create a result set with a stored procedure, the result set is not updatable — at least not using the Edit/Update methods. In these cases, you can use the WillUpdateRows method to execute an action query that performs the actual update operation(s).

Regardless of how you create a cursor, you can usually update the data using one of several techniques — even if the cursor is not updatable:

  • Executing a stored procedure that updates a selected row based on a code-provided key.

  • Executing an action query that changes specifically addressed rows. In this case your code creates a suitable WHERE clause used in the query.

  • Using the WillUpdateRows event to trap update operations and substitute appropriate stored procedure calls to perform the actual changes.

In summary, there are not very many situations where cursors are an optimal or viable way to access your data — especially on large production databases. However, there are situations where you need to have the ability to:

  • Scroll forwards and backwards (browse) through a limited result set.

  • Move to a specific row based on a saved value (a bookmark).

  • Move to the 'nth' row of a result set in absolute or relative terms.

  • Update limited result sets created against base tables using the RemoteData control.

There are even fewer situations that justify inclusion of a query that simply creates an unrestrained cursor against one or more base tables. For example, "SELECT * FROM Table" is an example of an unrestrained query. Not only is this not a permissible option in protected systems, but it can cause serious concurrency problems as you attempt to scale your application to more than a few users. Whenever you create a cursor, be sure to limit the scope to the fewest number of rows possible. In interactive systems (where there is a human operator), fetching more than a few hundred rows is often counterproductive and leads to increasingly complex concurrency problems.


Choosing an RDO Cursor Libary

Choosing an RDO Cursor Library

RDO supports several different cursor libraries — each with a specific purpose. While not all cursor libraries support all types of cursors, all libraries support forward-only result set management. For example, the ODBC client-side driver can only support rdOpenStatic and rdOpenForwardOnly cursor types, while the SQL Server server-side driver supports all four types.

Selecting the right cursor library is important when it comes to using certain RDO functionality. For example, you must use the client batch library to create dissociate rdoResultset objects or use the BatchUpdate method. To disable creation of cursors and use forward-only, read-only, or single-row result sets, use the rdUseNone option.

When creating a stand-alone rdoQuery object whose query is to be used as a method, you should set the CursorType before the query is executed because there is no option to do so when the query is executed.

The value of the CursorType property is used as the type argument of the OpenResultset method. To choose the type of cursor driver to use, set the rdoDefaultCursorDriver property of the rdoEngine or the CursorDriver property of the rdoEnvironment or rdoConnection object. The following table describes the options available with these properties.

Option | Description
rdUseODBC | RDO will use the ODBC cursor library. This gives better performance for small result sets, but degrades quickly for larger result sets.
rdUseServer | RDO uses server-side cursors if they're available.
rdUseClientBatch | RDO uses the client batch cursor library.
rdUseIfNeeded | (Default) The ODBC driver chooses the appropriate style of cursor and uses server-side cursors if available.
rdUseNone | RDO creates a cursorless result set.

Using the ODBC Cursor Library

The ODBC cursor library is the simplest of all of the libraries. It only supports static and forward-only cursors, so it is of limited appeal to many developers. It is also not particularly efficient for situations where you must create cursors of more than a few hundred rows. It is the most flexible in that it can access virtually all ODBC drivers — even those that are not fully ODBC Level II compliant.

The static cursor implemented by the ODBC cursor library is simply a data dump of the selected rows copied to the workstation.

Note When using the ODBC cursor library, you cannot execute a stored procedure that returns solely a Binary Large Object (BLOB) data column. You can, however, execute a query that returns another column and a BLOB column using the ODBC cursor driver. This limitation does not apply to the Microsoft SQL Server server-side cursor library.

Using the rdUseIfNeeded Cursor Library Option

This CursorDriver property option tells RDO to choose server-side cursors if available. Because it is the default driver option, this means that if you connect to Microsoft SQL Server, RDO chooses server-side cursors. If you connect to Oracle or other ODBC data sources, the ODBC cursor library is used.

Using the Server-Side Cursor Library

If your data source supports server-side cursors and you choose the rdUseServer CursorDriver option, the remote database engine builds cursor keysets on the server and extends the SQL dialect to support creation and management of data using cursors. Microsoft SQL Server versions 6.0 and higher support server-side cursors, but not all data sources do.

Server-side cursors reduce the amount of memory and disk space required on the client machines, but shift that load to the server. For SQL Server, cursor keysets are maintained in the TempDB database on the server, so it must be sized to meet the needs of additional temporary cursor keysets generated by multiple clients.

Server-Side Transact SQL Cursors

Another option you might wish to consider leverages SQL Server's ability to implement server-side cursors using its own Transact SQL cursor functions. You can create a server-side cursor, position to specific rows, and perform positioned updates using these functions.

Server-Side Cursors and Multiple Result Set Queries

If you submit a query that contains more than one SELECT statement to SQL Server using the server-side cursor library, the ODBC driver will reject the query because it is unable to handle this situation. If you must execute multiple result set queries or stored procedures that contain multiple SELECT statements, you have several alternatives:

  • Convert the query to submit individual SELECT statements.

  • Use the cursorless result set option enabled by setting the CursorDriver property to rdUseNone.

  • Create a forward-only, read-only rdoResultset with the RowsetSize property of the parent rdoQuery object set to 1.

  • Switch to the ODBC cursor library by setting the CursorDriver property to rdUseODBC.

Using the Client Batch Cursor Library

RDO 2.0 introduced the Client Batch cursor library. This library is capable of creating all four types of cursors in addition to the forward-only result set. In addition, this library is designed with Microsoft's proprietary Rushmore technology, so it is far more efficient than the ODBC cursor library. This option also enables you to create dissociate rdoResultset objects, perform offline changes to the data, and re-associate the rdoResultset with a live connection to post updates back to the database using optimistic batch updates. Even if you do not create a dissociate rdoResultset, you can still use the BatchUpdate method to submit blocks of database changes.

This library also creates all cursors on the client. That is, either raw row data row images or keysets are built in on the client system.

Using the rdUseNone Cursor Library Option

This CursorDriver property tells RDO to bypass creation of a cursor and create a single-row, forward-only, read-only result set.

Not all queries you create need to be returned in the form of a cursor. As a matter of fact, cursors are a particularly expensive way to fetch data and are rarely used in production-class applications. If you set the rdoDefaultCursorDriver property to rdUseNone, all result sets created by your application will be created as if you used the OpenResultset method with the rdOpenForwardOnly and rdConcurReadOnly options set, and with RowsetSize set to 1. This is the most efficient result set available for many applications, as it is the simplest to implement. However, it has the least number of features — some of which your application might require. The efficiency of this technique can permit your application to forgo the luxury of a full cursor.


Choosing an RDO Cursor Type

An rdoResultset object can be defined as a set of rows accessed with or without a cursor. The CursorDriver property of the rdoEnvironment or rdoConnection object determines which cursor library is used and where the cursor keyset will be created — on the client workstation or on the server. However, the type argument of the OpenConnection method defines how the cursor is built.

When you chose one of the type argument settings, the result set is returned as one of the following types:

  • Forward-only result set.

  • Static scrollable cursor.

  • Keyset cursor.

  • Dynamic scrollable cursor.

Each of these result set types is built based on the type available and the capability of the ODBC interface for your data source. However, if you set the CursorDriver property to rdUseNone, RDO does not create a cursor for your rdoResultset — it always (and only) creates a forward-only, read-only, single-row, cursorless result set.

Forward-Only Result Sets

In cases where you need to retrieve data quickly with the least amount of overhead, use a forward-only rdoResultset. This type of result set can be updatable, but it only exposes one row at a time. No cursor keyset is created, and data values are static — they are not updated as changes are made on the data source. However, because of its efficiency, it might be faster to rebuild a forward-only rdoResultset than to build and maintain a keyset-type rdoResultset.

Unless you use the rdConcurReadOnly option and set the RowsetSize to 1, RDO creates a cursor to manage your forward-only result set, which can impact performance. However, if you choose rdUseNone as the cursor driver, a forward-only, read-only cursorless result set is created by default.

Static Scrollable Cursors

A static-type rdoResultset is somewhat similar to a Jet snapshot-type Recordset object, except that static cursors can be updatable. This type of cursor creates a static copy of the data rows on the client or server, depending on the cursor driver chosen. Static cursor data appears to be unchanging. That is, the membership, order, and values in the result set used by a static cursor are generally fixed when the cursor is opened and fully populated. Rows updated, deleted, or inserted by other users (including other cursors in the same application) are not detected by the cursor until it is closed and reopened.

Keyset Cursors

A keyset-type rdoResultset is similar to a Jet dynaset-type Recordset object. A key (basically a bookmark) is built and saved for each row in the cursor and stored either on the client workstation or on the server machine. When you access each row, the stored key is used to fetch the current data values from the data source. In a keyset-driven cursor, membership is frozen once the keyset is fully populated; therefore, additions or updates that affect membership are not made a part of the rdoResultset until it is rebuilt.

On some ODBC data sources, modifications or additions made directly to keyset cursors using the AddNew and Edit methods are included in the result set, but additions or modifications made using Execute do not affect the cursor. Consult your server manual for details.

To build a keyset-driven cursor, you must provide sufficient resources on the client or server to hold the keys and a block (based on RowsetSize) of data rows.

Changing Membership When Using the AddNew Method

Although you can use the AddNew method to add a row to the database using an updatable keyset or static cursor, the new row is not always added to the rdoResultset — that is, some cursor libraries add rows to both the cursor and to the underlying table but others do not. The following table summarizes how keyset and static cursors are implemented by the various cursor libraries:

Cursor library | Rows added to cursor on AddNew
rdUseIfNeeded (server-side) | Yes
rdUseODBC (ODBC) | No
rdUseServer (server-side) | Yes
rdUseClientBatch (client batch) | Yes
rdUseNone (no cursor) | Not applicable

Dynamic Scrollable Cursors

A dynamic-type rdoResultset is identical to a keyset-driven cursor, except that membership is not frozen and bookmarks are not supported. Because RDO constantly checks that all qualified rows are included in the membership, this type of cursor carries the largest burden of overhead. However, a dynamic cursor may be faster to initiate than a keyset cursor, because the keyset cursor carries the overhead of building the initial keyset.

Generally, dynamic cursors should be avoided, as they are extremely expensive to implement and maintain — that is, this type of cursor uses a great deal of RAM and requires significant network bandwidth to maintain.

Using the RowCount Property

When a keyset-type or static-type rdoResultset is first created, the RowCount property is set to:

  • The number of rows in the populated result set if rows are returned.

  • 0 if no rows are returned by the query.

  • –1 if RowCount is not available.

When your code references the RowCount property, RDO fully populates the rdoResultset before returning control to your application. RowCount is not applicable to dynamic cursors where the number of rows can change, or to forward-only result sets that only expose one row.

Sequencing Operations

If there is an unpopulated rdoResultset pending on a data source that can only support a single operation on an rdoConnection object, you cannot create additional rdoQuery or rdoResultset objects or use the Refresh method on the rdoTable object until the rdoResultset is flushed, closed, or fully populated. For example, when using SQL Server 4.2 as a data source or a client-side cursor on SQL Server 6.x, you cannot create an additional rdoResultset object until you move to the last row of the current rdoResultset object. To populate the result set, use the MoreResults method to move through all pending result sets, or use the Cancel or Close method on the rdoResultset to flush all pending result sets. This restriction might not apply to your data source. If you need to submit a query and the hStmt is not available, you must open an additional connection.

Managing RDO Concurrency

You can control how the data source and the chosen cursor library manage row and page locking while you make changes to the data by changing the LockType option of the OpenResultset method. The following table describes the four types of locking.

LockType option | Description
rdConcurLock | Pessimistic concurrency. This technique uses the lowest level of locking sufficient to ensure that the row can be updated.
rdConcurRowVer | Optimistic concurrency using row versions. The ODBC cursor library and the data source compare row ID or TIMESTAMP values to determine if the row has changed.
rdConcurValues | Optimistic concurrency using row values. The ODBC cursor library and the data source compare data values.
rdConcurReadOnly | The cursor is read-only. No changes of any kind are permitted.
rdConcurBatch | The client batch cursor library is being used and you wish to defer all updates until you use the BatchUpdate method.

Caution The rdConcurLock option locks the entire rowset as set by the RowsetSize property. This lock is established immediately as the result set is opened — it does not wait until the Edit method has been executed. As the rowset is repositioned, the rows or pages touched by the rowset are locked and those remaining rows are released. Because of this mechanism, pessimistic locking should only be used in special cases.

Optimistic Locking

With optimistic locking, column data values or row versions are compared to test concurrency. In this case the original row as it was prior to the Edit or AddNew method is compared with the row as it exists in the database prior to being changed by the Update method. If the row data changed in this time frame, a trappable error results. At this point, your temporary row buffer is lost. To carry out the changes, you must refresh the current row using the Move 0 technique, then use the Edit or AddNew method, fill in the new values, and repeat the Update method again.


Running RDO Asynchronously

RDO 2.0 supports far more asynchronous operations than supported by RDO 1.0. For example, all of the following operations now permit use of the rdAsyncEnable option:

  • Establishing a connection. When using the OpenConnection or EstablishConnection methods, this option prevents blocking while waiting for connections that can take an indeterminate time to establish.

  • Creating result sets. When using the OpenResultset method, this option prevents blocking while queries are being executed.

  • Executing action queries. When using the Execute method, this option prevents blocking while queries are being executed.

  • Populating result sets. When using the MoveLast method, this option prevents blocking while queries are being completed and result sets are fully populated.

  • Refreshing result sets. If you used the rdAsyncEnable option when the result set was created, it is also used when the result set is refreshed using the Requery method.

To better support asynchronous operations, RDO now supports two properties that can be polled to test for asynchronous operation completion:

  • The StillExecuting property to test for completion of queries using the OpenResultset, Execute, or Requery methods.

  • The StillConnecting property to test for completion of a connection attempt using the OpenConnection or EstablishConnection methods.

In addition, RDO also exposes a number of events on the rdoConnection object that are fired as the operations are completed:

  • The Connect event fires when the connection attempt is complete. The event fires regardless of the success or failure of the operation.

  • The QueryComplete event fires when the query or reposition (MoveLast) operation is complete.

Handling Asynchronous Queries

When your application submits a query to a remote server, you often have no way of determining how much time will pass before the first row is returned. Because of the way SQL Server and some other servers process result sets, your application might not be passed the results until:

  • All rows have been processed — as in the case of a sorted result set.

  • The server-side row buffer is filled (approximately 4K).

  • The query completes and the server-side row buffer is flushed.

A typical query can take from milliseconds to hours or even days to complete — or it might never complete if the server or your workstation runs out of resources before the server completes the job. In some cases, the remote server begins to pass rows back fairly quickly, at which time RDO begins to populate your rdoResultset based on the type of cursor you choose. However, the remote server often stops working until your application begins to access those rows by moving forward in the result set. In many cases, rows or entire pages of data are locked on the server until the query is complete, which can only occur when you populate the last row of the rdoResultset object by using one of the Move methods, like MoveLast, or simply referencing the RowCount property.

Preventing Your Application from Blocking

Because of this uncertainty, it makes sense to prevent your application from blocking while the query is taking place and to ensure your application populates the result sets as quickly as possible. Unless you enable asynchronous operation by setting the rdAsyncEnable option with the OpenResultset method, your application will block until the first row of the result set is returned by the ODBC driver.

In addition to enabling the rdAsyncEnable option, you can also code an event handler to trap the QueryComplete event that fires when the query completes.

When you use the OpenResultset method with the rdAsyncEnable option, RDO returns control to your application immediately. At this point, the rdoResultset object is instantiated, but its rowset might not be ready to use — and won't be until the remote server begins to pass rows back. As an indication of this state, the StillExecuting property will return True until the first row is available. Once StillExecuting returns False, the rdoResultset data is available and the QueryComplete event is fired.

What to Do While Waiting for an Asynchronous Operation

When using the asynchronous operation option, your application regains control while the remote server is busy working on your query or simply populating your result set. Since this operation might take some time to complete, you need to decide what your applications should do in the meantime. The following suggestions can help you choose a viable strategy:

  • Establish an event handler to trap the Connect event. This event procedure can be used to finalize the connection operation or simply test for a valid connection. For example, you might wish to submit a separate set of logon or initialization procedures unique to your application in this handler.

  • Establish an event handler to trap the QueryComplete event. This event is fired when RDO detects that the first result set is ready for processing, or when the query has failed. This technique is preferable over polling the StillExecuting property.

  • If you cannot create an event handler to notify your application that the asynchronous operation has completed, you can periodically poll the StillExecuting or StillConnecting properties to test for completion of the operation. This loop should include the DoEvents statement to permit other portions of your application to gain access to the processor (unless this is not possible because of other design considerations).

  • Consider leveraging the workstation's processor to run an overlapping procedure that assumes completion of the previous operation. For example, you might do this in a situation where your application processes rows from an external file and passes these rows to a remote server as INSERT statements. During the time that a batch of INSERT statements is being processed by the remote server, your application could prepare the next batch to be sent. Once the first batch completed, the subsequent batch would be ready to process and the cycle could be repeated.

This drawing illustrates how to setup an asynchronous file import routine.

Figure 11.2 Running overlapping procedures while waiting for an asynchronous operation

This technique leverages both the thread used to execute your query on the remote server and the thread available on the workstation.

What You Can't Do While Waiting for an Asynchronous RDO Operation

As long as the asynchronous operation is still executing, you cannot reference the rdoResultset, rdoQuery, or rdoConnection object being processed — except for the properties and methods listed in the following table.

Valid Properties and Methods During Asynchronous Operations

Remote Data Object | Valid Properties and Methods
rdoConnection | StillConnecting property; Cancel, Close methods
rdoQuery | StillExecuting property; Cancel, Close methods
rdoResultset | StillExecuting property; Cancel, Close methods

If you decide that you have waited long enough for an asynchronous operation to complete, you can choose to tell the remote processor to abandon the operation by using either the Cancel or Close methods.

  • The Cancel method abandons the query operation.

  • The Close method abandons the operation and discards the RDO object it is applied to.

Using one of these methods is not always prudent in cases where you have submitted complex multistatement or broad-scope update operations because database integrity could be compromised.


Coding RDO Event Handlers

RDO 2.0 exposes a wide variety of events that can make handling asynchronous operations, centralized error handling, or common routine handling far easier to implement. These event procedures are all enabled when the initial RDO object is created. Basically, when you use WithEvents with the Dim statement, the object's event handlers are exposed to your code. For example, to expose the events on an rdoConnection object, you can code the following:

Private WithEvents MyCn As rdoConnection

In design mode, the Object dropdown in the code window will include an entry for MyCn. Once selected, each of the rdoConnection events are exposed.

While you cannot use the WithEvents operator with the Dim x As New syntax, you can use the following syntax to instantiate stand-alone RDO objects.

Private WithEvents MyCn As rdoConnection
...
Set MyCn As New rdoConnection

Note that the rdoQuery object does not expose any events — these are exposed on the parent rdoConnection instead. Each rdoConnection event handler that traps events for the rdoQuery objects is passed a pointer to the query that is causing the event. For example, the rdoConnection object's WillExecute event could be coded as follows to display the SQL about to be submitted as a query.

Private Sub MyCn_WillExecute( _
   ByVal Query As RDO.rdoQuery, Cancel As Boolean)
Msgbox "About to exectue:" & Query.SQL
End Sub

For More Information More detail on using the WithEvents keyword is available in "Adding Events to a Class" in "Programming with Objects" in the Programmer's Guide, and in the Language Reference".


Using RDO Events

Instead of establishing polling loops to periodically test for the completion of asynchronous operations, you can code your own event handlers to trap the query, connection, and other asynchronous operation events as they occur.

The following tables briefly describes each of the RDO asynchronous query events.

rdoEngine Object Asynchronous Events

Event | Fires …
InfoMessage | When SQL_SUCCESS_WITH_INFO is returned by an ODBC API function.

rdoEnvironment Object Asynchronous Events

Event | Fires …
BeginTrans | After the BeginTrans method has completed.
CommitTrans | After the CommitTrans method has completed.
RollbackTrans | After the RollbackTrans method has completed.

rdoConnection Object Asynchronous Events

Event | Fires …
BeforeConnect | Before the call to SQLDriverConnect so the developer can do his own prompting.
Connect | After a connection operation completes — successfully or not.
Disconnect | After a connection has been closed.
QueryComplete | After an query completes — successfully or not.
QueryTimeout | When the QueryTimeout time has elapsed and the query has not yet completed.
WillExecute | Before the query is run, allowing the developer to prohibit the query from running or make last-minute adjustments to the SQL.

rdoResultset Object Asynchronous Events

Event | Fires …
Associate | After a new connection is associated with the object.
Dissociate | After the connection is set to nothing.
ResultsChange | After current rowset is changed (multiple result sets).
RowCurrencyChange | After the state of the current row has changed (due to an edit, delete, or insert) or when the current row pointer has changed.
RowStatusChange | After the state of the current row has changed (due to an edit, delete or insert).
WillAssociate | Before a new connection is associated with the object — developer can override or cancel the operation.
WillDissociate | Before the connection is set to nothing — developer can override or cancel the operation.
WillUpdateRows | Before an update to the server occurs — developer can override or cancel the operation.

rdoColumn Object Asynchronous Events

Event | Fires …
DataChange | When the value of the column has changed.
WillChangeData | Before data is changed in the column; allows developer to cancel.


Working with RDO Result Sets

Once the rdoResultset is created, you can begin to access the data values it contains. When working with any result set there are a few points to keep in mind:

  • If the result set is created as a cursor, you should fully populate it as soon as possible because the rows and pages fetched for your cursor might be locked by the remote database engine. A result set is not fully populated until you have positioned to its last row.

  • In many cases a cursorless result set can be more efficient. Do not create rdoResultset objects that enable more features than absolutely necessary. For example, do not request an updatable result set if you do not intend to change data using the cursor. And do not request a fully scrollable cursor if you simply need to populate a list box.

  • Consider using cursorless result sets and action queries to perform updates. In many cases these techniques can improve performance. Although somewhat more complex to implement, they can be used when direct access to underlying data tables is not possible.

  • Use asynchronous operations and event procedures wherever possible to prevent application lockups, or at least mitigate their impact. It is also possible to enable asynchronous processing of Move methods like MoveLast to prevent lockups during result set population.

  • Creating several smaller result sets for complex result set processing is often faster and manages system resources better.

  • Generate your result sets from stored procedures whenever possible, because it increases efficiency on the server, on the network, and on the workstation and makes application development simpler.

  • Dissociate an existing rdoResultset object created as a static client batch cursor from its connection. You can continue to make changes to the data using the Edit or AddNew methods. When you are ready to post the changes back to the database, you can associate the rdoResultset with an open connection by setting the ActiveConnection object to an open rdoConnection object.

Setting RDO Cursor Limits

You can limit the number of rows returned by a query by creating an rdoQuery and setting the MaxRows property. Once the query processor returns MaxRows rows, it stops working on the query. This is an especially easy way to govern the number of rows returned from ad hoc queries.

Note The MaxRows property also affects data modification queries. For example, if MaxRows is set to 100, and you execute an action query such as an UPDATE statement, only the first 100 qualifying rows are updated.

Determining How Many Rows Are Returned

Generally, you can use the RowCount property to determine the number of rows that qualified to be members of your result set. When you access this property, RDO basically executes a MoveLast method to fully populate the result set before returning to your application. This operation is not performed asynchronously, so your application might block while the query is completed. In some cases, there is no RowCount support from ODBC, so the property returns –1.

Handling RDO QueryTimeout Events

To limit the amount of time the query processor works on a query, set the QueryTimeout property on the rdoConnection or rdoQuery. Once the QueryTimeout period expires, the QueryTimeout event fires. This event is fired each time the QueryTimeout time is reached — whether or not you are using the rdAsyncEnable option.

Your event handler has the choice of continuing or canceling the asynchronous query operation. If you or your user decide to continue working and wait for another QueryTimeout period, pass False to the Cancel parameter. Changes to the QueryTimeout property do not take effect until you start another query.

If you do not code a QueryTimeout event procedure, the query is canceled, the StillExecuting property is set to False, and control returns to the application.


Positioning the Current Row Pointer

At any one time, only one row in an rdoResultset is exposed for data retrieval or modification: the row addressed by the current row pointer. You move the current row pointer from row to row by using one of the methods or properties as shown in the table below. Each of these methods implies a starting location, and the reposition is made relative to that location. In some cases the current row pointer will be indeterminate or positioned beyond either end of the result set. In these cases, there is no current row, so no attempt should be made to reference the data columns or update the row. In other cases (as with forward-only result sets), you will not be permitted to move backwards in the result set or even to the end with the MoveLast method. Generally, this is because of your choice of cursor.

Methods and Properties that Reposition the Current Row Pointer

RDO Methods and Properties | Move to the rdoResultset object's … | Relative to…in the rdoResultset
MoveFirst | First row | Anywhere
MoveNext | Current row + 1 | Current row
MovePrevious | Current row – 1 | Current row
MoveLast | Last row | Anywhere
Move | 'n' th row forward or back | Current row
PercentPosition | % of populated rows | First row
AbsolutePosition | 'n' th row | First row
Bookmark | A presaved location | Anywhere
LastModified | A presaved location set by RDO | Anywhere

Tip Using the Move method with a 0 argument simply refetches the current row.

Note You cannot use a bookmark to reposition the current row pointer if the BOF or EOF properties are True. If you are working with a scrollable rdoResultset and you wish to reposition to a bookmark, use the MoveFirst method or another Move method to reposition back into the result set.

If the rdoResultset object supports bookmarks, these can be used to save the current location in a Variant variable. You can subsequently position back to a saved location in the rdoResultset by setting the Bookmark property with a bookmark. The LastModified property is also returned in the form of a bookmark that can be applied to the Bookmark property to reposition back to the last row changed or added.

Note When using forward-only rdoResultset objects, you can only use the MoveNext method. Other repositioning techniques are not supported. You can, however, use the Close method to close the result set or use the MoreResults method to terminate processing of the current rdoResultset object at any time.

Be sure to check the EOF and BOF properties to determine if the current row is positioned beyond the end or beginning of the result set. When using the Bookmark property to reposition the current row, it is possible to reposition to a row that has been deleted by another user. If this occurs, a trappable error results and you must reposition to another valid row.

For More Information See "BOF Property," "EOF Property," "AbsolutePosition Property," "PercentPosition Property," and "Move" in the Language Reference".

Enabling the Asynchronous RDO MoveLast Method

If at all possible, you should execute the MoveLast method as soon as you can to fully populate your result set. When MoveLast has positioned to the last row in the result set, the statement handle is freed and any residual page or row locks are released by the remote server.

By setting the rdAsyncEnable option when using MoveLast, RDO returns control to your application immediately to prevent unneeded lockups. In this case, the rdoResultset data is unavailable until the StillExecuting property returns False. At this point, the QueryComplete event is also fired.


Updating RDO Data

Not all rdoResultset objects are updatable. In some cases, you might be unable to update a result set using a cursor, but be able to update using an action query. In order for a cursor to be used to update base tables, a number of conditions must be met:

  • The table has a unique index that the cursor library can use as a unique row identifier. Not having a unique key is the most common fault when you discover that a table cannot be updated.

  • The logged-on user has read-write access to the underlying columns, tables, and database. In some cases, the only way to update a table might be by stored procedures.

  • The remote system's database administrator permits direct update access to tables (many don't).

  • The result set is based on a complex multitable join. While it is possible to update a join, not all joins are updatable.

  • The type of cursor supports updates.

  • Other users are not accessing the data. Could other users have the data pages locked?

  • The Updatable property for both the rdoResultset and rdoColumn objects returns True. Even when this property is True, the result set or specific columns still might not be updatable.

Updating Through Stored Procedures

Many multiuser systems do not attempt to create applications that depend on the direct updatability of base tables. While it seems very easy to create a cursor to update a specific row in a chosen table, your system administrator might prohibit this particularly dangerous activity. In addition, database business rules and relational integrity often depend on synchronizing other tables in the same logical operation. Your database might include triggers or other mechanisms to perform some or all of these operations automatically, so your application can be simpler and not have to deal with the details.

To update a base table and perform ancillary operations with RDO cursors, you can begin a transaction and manage all aspects of the update in your application, perhaps by executing a number of independent update operations. However, this fairly complex approach must be duplicated for each application that needs update access to the tables. Once the business rules change or the referential integrity schema is tuned, your application must often be recompiled and redistributed.

To implement a better solution, many sites use stored procedures to return rowsets and subsequently update specific rows. This approach leverages most remote database and network security schemes and has been successfully implemented in systems supporting 1,000 or more users. Another benefit of this approach is performance. Because your applications are usually written with limited-scope result sets and do not depend solely on cursors, the workstation, network, and remote server all work together more efficiently.

Updating with Stored Procedures Executed via the WillUpdateRows Event

RDO 2.0 also permits you to execute a stored procedure when using a cursor to update the database. This means that you can use your own stored procedures to add, change, and delete data. If you code a procedure for the WillUpdateRows event, you can submit a stored procedure to perform the update. Or you can simply code a number of update or other operations that are executed instead of, or in addition to, the cursor-based operations.

If the result set is not in batch mode, the WillUpdateRows event is fired for each call to the Update method just before the ODBC update is sent to the server. In batch mode, RDO fires the WillUpdateRows event when the BatchUpdate method is executed.

By setting the WillUpdateRows event ReturnCode argument, you can indicate the success or failure of your update operation. The following table summarizes the options you can use for the ReturnCode argument.

ReturnCode | Indicates
rdUpdateSuccessful | Your update operation was successful. RDO notifies no other handlers (if there are more than one handler for this event) and marks columns as updated.
rdUpdateWithCollisions | Your update operation was successful, but some rows produced collisions (batch mode only). RDO notifies no other handlers (if there is more than one handler for this event). Columns are not marked as updated.
rdUpdateFailed | Your update operation was not successful. RDO notifies no other handlers (if there is more than one handler for this event) and generates a trappable run-time error. Columns are not marked as updated.
rdUpdateNotHandled | (Default) Your procedure did not handle the update. RDO continues notifying other handlers (if any). If no other procedures handle the update, RDO updates the data and marks the columns as updated.

Updating Data Sources Using Cursors

If your database does not implement sophisticated referential integrity or complex business rules and can permit direct access to base tables, you can use an rdoResultset cursor to update data rows. Keep in mind that many of the problems associated with updating have to do with managing concurrency — row and page locking, and sharing data. Your error handlers should be prepared to deal with a wide range of contingencies, including row and page lock conflicts, loss of connection, update conflicts, and many others.

Changing data row values in an rdoResultset involves the following steps:

  1. Position to the row using one of the current row repositioning techniques discussed in "Positioning the Current Row Pointer."

  2. Use the Edit method to activate the row to be altered. All data is copied into a temporary row buffer.

  3. Provide a new value for each column that you need to modify. To address the temporary row buffer, simply address the rdoResultset object's columns as you would when retrieving data from the columns.

  4. When all columns are changed to the correct values, use the Update method to save the row buffer to the data source. The current row is deleted and the buffered row replaces it. At this point the LastModified property returns a bookmark to the modified row.

Abandoning an Edit

No changes are made until you use the Update method, when your edits are committed to the database. However, if you decide to abandon the edit and revert back to the unedited version of the current row, you have a number of choices:

  • Move the current row pointer to another row.

  • Use the Move method with a 0 argument to refresh the current row.

  • Use the CancelUpdate method.

  • Use the Rollback method to undo the transaction, assuming you preceded the operation with the BeginTrans method.

  • Set the Cancel argument to True in the WillUpdate event to bypass the actual ODBC update operation.

After the edit, the current row pointer is positioned to the row modified — which might be located at the end of the rdoResultset. To revisit the row that was just changed, use the bookmark provided in the LastModified property.

Adding and Deleting rdoResultset Rows

In addition to changing columns in a specific row, you can add a row to an rdoResultset which adds a row to a base table. This operations assumes that your application has read-write access and permissions to add rows to the underlying table(s). Because this is often not the case, be sure to investigate the WillUpdate event and the topics on using stored procedures elsewhere in this chapter.

Use these steps to add a specific row in an rdoResultset:

  1. Use the AddNew method to create a temporary row buffer to hold the new row to be added.

  2. Provide a new value for each column you need to modify. To address the temporary row buffer, simply address the rdoResultset object's columns as you would when retrieving data from the columns.

  3. Use the Update method to save the row to the data source.

After the row is added, the current row pointer is positioned on the row that was current before the new row was added — if the cursor supports addressing new rows. If this is the case, you can use the LastModified bookmark to move to the newly added row at the end of the rdoResultset.

Once you’ve added rows, you can also delete them from the result set, assuming you also have permission to delete data (you might not).

Use these steps to delete a specific row in an rdoResultset:

  1. Position to the row using one of the Move methods or the AbsolutePosition or PercentPosition property.

  2. Use the Delete method to delete the row from the cursor (if any) and the data source.

Once a row is deleted, the current row is no longer valid, so you must reposition to another valid row in the rdoResultset.

Making Changes with RDO Action Queries

A more efficient alternative to using the Edit, AddNew, Delete, and Update methods is to use the Execute method. By executing an SQL query that contains one or more UPDATE, INSERT, or DELETE statements, you can make changes to the database without using the RDO methods. Depending on the type of data source and its ability to support complex multistatement operations, these SQL statements can contain logic that performs so-called "make-table" or SELECT INTO queries that create new permanent or temporary tables or perform other complex operations. You must manage the errors and concurrency yourself. You can also submit transaction statements that bind the operations into one or more atomic sets using the SQL syntax supported by your data source. Because the Execute method also supports the rdAsyncEnable option, these operations can be performed asynchronously.

Note The Execute method is not designed to execute queries that return rows. If you execute a stored procedure that performs a mix of “action” operations and row-returning operations, you must use the OpenResultset method and parse the result sets that are generated.


Processing Multiple RDO Result Sets

Any SQL statement can include multiple SELECT statements or stored procedures that invoke one or more SELECT statements. Each SELECT statement generates a result set that must be processed by your code or discarded before the RDO resources are released and the next result set is made available.

Action queries also generate row-less result sets that must also be processed — this is another type of multiple result set query. In many cases, when you execute a stored procedure, it might return more than one result set. It is often difficult to determine if a stored procedure will return more than one result set because a stored procedure might call another procedure.

For example, if you submit a query that includes four SELECT queries to populate four local ListBox controls and a stored procedure that updates a table, your code must deal with at least five result sets. Because you might not know how many result sets can be generated by a stored procedure, your code must be prepared to process n sets of results.

There are two approaches to executing queries with multiple result sets:

  • Execute the OpenResultset method directly against a connection.

  • Use the OpenResultset method against an rdoQuery.

Both are processed in similar ways, but if you use the rdoQuery, you can examine the RowsAffected property to determine the number of rows affected by action queries. While it is possible to execute a multiple result set query using the Execute method, it is not possible to retrieve the rows affected from individual statements, and a trappable error results if any of the queries returns rows.

Using Server-Side Cursor Libraries with Multiple Result Sets

Not all cursor drivers support the ability to process queries that contain more than one set of results — the SQL Server server-side cursor driver is an example. However, if you request a cursorless result set by using the rdOpenForwardOnly, rdConcurReadOnly options and by setting the RowsetSize property to 1, you can execute queries with multiple result sets using server-side cursors. You can also set these options for all result sets by setting the CursorDriver property to rdUseNone.

Multiple Result Sets: An Example

This section takes you through a step-by-step procedure that demonstrates how to execute a query with multiple result sets by using the rdoQuery object.

Tip Whenever you use the concatenation operator "&" to build SQL queries, be sure to include white space (such as spaces or tabs) between the operators on separate lines.

  1. Create your SQL statement and place it in a string variable — for instance, MySQL. For SQL Server, multiple statements must be separated by semi-colons.
    Dim MySQL As String
MySQL = "Select Name from Authors Where ID = 5; " _
& " Select City from Publishers; " _
& " Update MyTable " _
& " Set Age = 18 Where Name = 'Fred'"
  2. Next, create a new rdoQuery and set a variable declared as rdoQuery and multiple result sets to this object — in this case, MyQy. The example assumes an rdoConnection object (Cn) already exists. There are a number of other ways to instantiate and initialize rdoQuery objects; this example illustrates only one of these ways.
    Dim MyQy As rdoQuery
Set MyQy = Cn.CreateQuery("MyQy1", "")
MyQy.SQL = MySQL
  3. Execute the query by using the OpenResultset method against the rdoQuery object. If you do not need the extra properties and the ability to pass parameters to the query, you can use the OpenResultset method directly against the rdoConnection object. The arguments you use here affect all result sets fetched from this query. For example, if you need to use a cursor on the second result set, you must specify a cursor type when the first result set is opened.
    Dim MyRs As rdoResultset
Set MyRs = MyQy.OpenResultset(rdOpenForwardOnly, _
rdConcurReadOnly)
  4. You are now ready to process the first result set. Note that the rdAsyncEnable options argument was not set. Because of this, control is not returned to the application until the first row of the first result set is ready for processing. If the current rdoResultset contains rows, the RowCount property is set to a value > 0, and the EOF and BOF properties are both False. If no rows were returned, the RowCount property returns either –1 to indicate that the number of rows is not available, or 0, depending on the driver and data source.

    The following example fills a ListBox control called NameList1 with the results of the query.

    While Not MyRs.EOF         ' Loop through all rows.
' Use the first column.
NameList1.AddItem = MyRs(0)
MyRs.MoveNext            ' Position to the next row
' in the result set.
Wend
  5. The first result set is now at end-of-file (MyRs.EOF = True). Use the MoreResults method to activate the next result set. Once you execute MoreResults, the first set of rows is no longer available — even if you used one of the cursor options to create it.
    ' Activate the next set of results.
If (MyRs.MoreResults) Then ...
  6. You are now ready to process the second result set. This example uses only the first few names and discards the remaining rows.
    ' Loop through some rows.
Do While Not MyRs.EOF and MyRs(0) < "B"
' Use the first column.
NameList1.AddItem = MyRs(0)
MyRs.MoveNext
Loop
' Activate the next set of results
' and discard remaining rows.
If (MyRs.MoreResults) Then ...
  7. You are now ready to process the last set of results. Because this is an UPDATE statement, there are no rows to be returned, but you can determine the number of rows affected by using the RowsAffected property. The MoreResults method is used for the last time to release all resources connected with this query.
    If MyQy.RowsAffected = 0 Then
MsgBox "No rows were updated"
End If
' Activate the next set of results.
If (MyRs.MoreResults) Then ...

When you use the MoreResults method against the last result set, it should return False and other resources required to process the query are released. At this point the rdoQuery object can be reused. If you use the Close method against an rdoQuery object, it is removed from the rdoQueries collection.


Using the GetClipString Method

In situations where you want to fill a grid control or other control that supports bulk loading through its Clip property, you can use the GetClipString method to return a delimited string for 'n' rows of a result set. The resultant string can then be applied to the target control's Clip property, which will populate the control with data from your result set.

The GetClipString method returns a delimited string for 'n' rows in a result set based on its NumRows argument. If more rows are requested than are available, only the available rows are returned. Use the RowCount property to determine how many rows are actually fetched. The number of rows that can be fetched is constrained by available memory and should be chosen to suit your application. Don't expect to use GetClipString to bring your entire table or result set into memory if it is a large table.

Generally, the GetClipString method works just like the GetRows method, except that the data is returned as a string instead of a two-dimensional variant array. GetClipString can be used to fill a grid control or any control that has a Clip property. It can also be used to format export data from a result set to a sequential file.

After a call to GetClipString completes, the current row is positioned at the next unread row. GetClipString is equivalent to using the Move (rows) method.

If you are trying to fetch all the rows using multiple GetClipString calls, use the EOF property to determine if there are rows available. GetClipString returns less than the number requested either at the end of the rdoResultset, or if it cannot fetch a row in the range requested. For example, if a fifth row cannot be retrieved in a group of ten rows that you're trying to fetch, GetClipString returns four rows and leaves currency on the row that caused the problem. It will not generate a run-time error.

The ColumnDelimiter optional parameter can be used to substitute a different column delimiter than the default tab (Chr$(9)) character, and the RowDelimiter optional parameter can be used to substitute a different row delimiter. This is useful when working with a control that accepts a clip format, but requires different characters for the column and row delimiters (some grids have been known to require both a carriage return and a line-feed character for a row delimiter).


Executing Optimistic Batch Updates

RDO 2.0 implements a new technique to submit multiple database operations as a batch. The ODBC and SQL Server server-side cursor libraries both support multiple statement batches against data sources that support this concept. However, the individual operations are not managed by the cursor driver, so errors and other contingencies must be handled manually. In contrast, the Client Batch cursor library is designed to automatically:

  • Track the status of each row in rdoResultset objects it creates. As each row is changed or deleted or new rows are added, the Status property is automatically set to reflect that the row is "dirty." By examining the row status, the Client Batch cursor library can track those rows that have been added, changed, deleted, or unchanged.

  • Manage independent rdoResultset objects. While an rdoResultset cannot be created independently from saved local data, once a static result set is created, you can disconnect from the remote server and perform any number of changes to the rdoResultset object. You can then re-associate the rdoResultset with another connection by setting the ActiveConnection property, and use the BatchUpdate method to post the changes.

  • Permit postponement of the update operation. Because the Client Batch cursor library is designed to operate on an entire set of rows as a batch, you can delay posting the changes to the database for as long as necessary.

  • Build the SQL statements needed to perform the database changes. Basically, the Client Batch cursor library appends a suitable WHERE clause to each SQL statement to correctly identify specific rows by key.

  • Manage the result of each operation and report the success or failure status back to your application.

Basically, when using a deferred update strategy as described here, you generally assume that the data involved is not subject to significant changes while the offline or deferred operations take place. If, on the other hand, the data is being constantly queried and changed, this strategy quickly breaks down. The Client Batch cursor library is designed to trap collisions that occur when unexpected update conflicts occur — as when another user changes a row that is a member of the result set being updated. When this happens, each row involved in a conflict is marked and all three copies of the data are made available to your application:

  • The data as it was when first read by your application from the database.

  • The data as it is now recorded in the database — as the other user changed it.

  • The data which your user attempted to save.

Using these three versions of the data, it is your application's job to reconcile the differences.


Activating the RDO Client Batch Cursor Library

If your application needs to use the features provided by the RDO Client Batch cursor library, you must choose it as the operational cursor library by setting the rdoEngine object's rdoDefaultCursorDriver property or the rdoEnvironment object's CursorDriver property to rdUseClientBatch. The Client Batch cursor library is capable of creating and managing any of the cursor types — depending on the capabilities of your ODBC driver.


Creating and Changing an RDO Client Batch

If you set the CursorDriver to rdUseClientBatch, you can use the Client Batch cursor library with or without deferred batch updates. To choose the desired type of update operation, set the OpenResultset method's LockType argument as follows:

  • rdConcurBatch to select deferred updates. In this case, the Update method simply posts changes to the current rdoResultset, but no changes are made to the underlying database tables until the BatchUpdate method is executed.

  • Any of the other concurrency options. In this case the Update method immediately posts changes to the database on a row-by-row basis.

Dissociating the rdoResultset from its Connection

Once the rdoResultset is opened, you can begin making changes immediately. Or once you have fully populated a rdConcurBatch-style result set, you can dissociate the rdoResultset object from the current connection by setting the ActiveConnection property to Nothing. At this point you can make whatever additions, deletions, or updates on the individual rows are needed.

Making Changes to the Result Set

To make these changes, simply use the Edit, Delete, and AddNew methods followed by the Update method to commit the changes. Once all changes have been made, use the BatchUpdate method to post the batch of changes to the database. The BatchUpdate method scans the rdoResultset, checking the Status property for each row — which is set to rdRowUnmodified for all unchanged rows. If the row has changed, as indicated by a Status not set to rdRowUnmodified, the Client Batch cursor library generates an SQL query to perform the changes. This query is concatenated together with all other SQL operations needed to synchronize the local result set with the data on the remote server. Once all operations are included, the batch is executed (passed to the remote server for execution).

Setting the UpdateOperation Property

The UpdateOperation property determines how RDO and the client batch cursor library perform update operations on your data. Basically, you have two choices: either attempt to perform the update operation in a single step, or intentionally break up the operation into two separate operations — a delete followed by an insert.

Note Depending on your database triggers or other referential integrity checks, you might find it impossible to perform a separate delete operation.

In many cases, update operations are handled internally by the database engine as a delete operation followed immediately by a corresponding insert — coupled as an atomic transaction. Unless your database engine is capable of "update in place" and your update operation conforms to the requirements of this type of update, the update will be executed as a delete followed by an insert.

By choosing one of the following constants for the UpdateOperation property, you can determine how RDO and the Client Batch cursor library manage the update.

  • rdUpdate (Default): Uses an UPDATE SQL statement for each modified row.

  • rdInsertDelete. Uses a pair of SQL DELETE and INSERT statements for each modified row.

Even if you choose the rdInsertDelete option, the Client Batch cursor library encases the individual operations in a single transaction to ensure that if one operation fails, the entire operation is rolled back.

Setting the UpdateCriteria Property

The UpdateCriteria property is used to control how the Client Batch cursor library determines if the row about to be updated has changed in the base tables. If some other user has changed the data while your code was editing the row, this property selects the technique that RDO uses to ensure that you don't inadvertently overlay other users' changes.

By setting the UpdateCriteria property, you can determine how RDO builds the WHERE clause used to synchronize the local result set with the remote database tables. The BatchUpdate method creates an SQL statement for each row whose Status property is not rdRowUnmodified. Each of these statements includes a WHERE clause that uniquely identifies each row affected. By adjusting the UpdateCriteria property, you can adapt your application to deal with the update mechanism of your remote server. For example, in some servers, triggers might be fired unnecessarily if columns not affected by the changes you make are included in the SQL update.

Just exactly how individual rows are identified is determined by the UpdateCriteria property as follows:

  • rdKey (Default): Uses just the key column(s) in the WHERE clause. That is, RDO uses those columns you have designated as the primary keys — the columns that uniquely identify specific rows in the table(s).

  • rdKeyAndUpdatable. Uses the key column(s) and all updated columns in the WHERE clause. In this case RDO also adds the columns that were changed to the WHERE clause to ensure that the rows have indeed changed.

  • rdKeyAndModified. Uses the key column(s) and all the columns in the WHERE clause. In this case RDO includes the key and all other columns to the WHERE clause to ensure that the rows have indeed changed.

  • rdKeyAndTimestamp. Uses just the TIMESTAMP column if available (will generate a run-time error if no TIMESTAMP column is in the result set). This option compares the existing TIMESTAMP column with the column in the database to determine if the value has changed.

Tip If you are using SQL Server 6.5, you can use the SQLTrace utility to observe how RDO builds the WHERE clause when making a batch update.


Using the Status Property to Manage Batch Update Errors

The Status property is used to track changes made to individual rows in a Client Batch cursor's result set. If you create a Client Batch cursor and use the Edit, Delete, or AddNew methods followed by the Update method, RDO sets the row's Status property to indicate what operation has taken place on the row.

Initially, rdoResultset rows fetched from the remote server are marked as rdRowUnmodified. However, once you make a change to the result set, RDO changes the Status property to reflect the operation. For example, if you use the AddNew and Update methods on the rdoResultset object, the Status property is set to rdRowNew. If you wish to have RDO and the Client Batch cursor library ignore a change you have made, you can set the Status property to rdRowUnmodified before using the BatchUpdate method.

When the BatchUpdate method returns, the Status property of each row indicates the degree of success or failure of each update. Your code should examine each row's Status property and take appropriate action based on each of the following possible status codes:

  • rdRowUnmodified. The row has not been modified or has been modified successfully. No action is required.

  • rdRowModified. The row has been modified, but not updated in the database. This status would indicate that the row needs to be updated, but either has not because the BatchUpdate method has not yet been executed or failed to complete the update operation.

  • rdRowNew. The row has been inserted with the AddNew method, but not updated in the database. This status would indicate that the row needs to be added to the database, but either has not because the BatchUpdate method has not yet been executed or failed to be added.

  • rdRowDeleted. The row has been deleted with the Delete method, but not updated in the database. This status would indicate that the row needs to be deleted from the database, but either has not because the BatchUpdate method has not yet been executed or failed to be deleted.

  • rdRowDBDeleted. The row has been deleted locally and in the database. No action is required.


Using ODBC API Functions

Each rdoEnvironment, rdoConnection, rdoQuery, and rdoResultset object includes support for an ODBC handle that you can use to manipulate the object through the ODBC API. The hEnv, hDbc, and hStmt properties of the ODBC API correspond to the rdoEnvironment, rdoConnection, and rdoResultset objects respectively.

The following table lists the ODBC handles that are created by RDO. You can use these handles in your own ODBC API code.

RDO Object Property | Handle created by the ODBC API
rdoEnvironment.hEnv | SQLAllocEnv
rdoConnection.hDbc | SQLAllocConnect, SQLDriverConnect
rdoResultset.hStmt | SQLAllocStmt
rdoQuery.hStmt | SQLAllocStmt
rdoPreparedStatement.hStmt | SQLAllocStmt

Caution While it is possible to use the ODBC API with the RDO ODBC handles, you are cautioned that incorrect use of the ODBC API can cause unpredictable behavior. For example, if you close connections or deallocate any of these handles using ODBC API code, the RemoteData control or RDO can behave unpredictably. The ODBC handles should not be saved for future use as they are subject to change without notice.


Using the RemoteData Control

The RemoteData control is similar to the Visual Basic Data control in most respects. Both the Data control and the RemoteData control are designed to connect data-aware bound controls to a data source. The primary difference is that the RemoteData control uses RDO to connect to the ODBC driver manager, while the Data control uses DAO to connect to the Jet database engine, which can connect to native Jet databases or IISAM data sources. The Data control can also connect indirectly to RDO by using the ODBCDirect interface. Even though both controls use the same ODBC driver manager and data source entries, RDO, ODBCDirect, and DAO cannot share data source connections, data objects, or other resources.

Note The Data control supports the ability to use the RDO DLLs via the ODBCDirect option. In this case, your code uses Data Access Objects (DAO) instead of RDO to manage the Data control and its result sets.

Without a RemoteData control, a Data control, or its equivalent, data-aware (bound) controls can’t automatically access data. You can perform many simple remote data access operations using the RemoteData control without writing any code at all. However, most applications rely on additional RDO code to perform most data access operations, as RDO provides more flexibility and additional options not exposed by the RemoteData Control alone. In most respects, the RemoteData Control behaves very much like the Data control.

Data-aware controls bound to a RemoteData control automatically display data from one or more columns for the current row or, in some cases, for a set of rows on either side of the current row. The RemoteData control performs all operations on the current row.

If the RemoteData control is instructed to move to a different row, all bound controls automatically pass any changes to the RemoteData control to be saved by the ODBC data source. The RemoteData control then moves to the requested row and passes data back from the current row to the bound controls, where it is displayed.

Handling Errors with the RemoteData Control

The RemoteData control automatically handles a number of contingencies, including empty result sets, adding new rows, editing and updating existing rows, and handling some types of errors. However, in more sophisticated applications, you will need to trap some error conditions that the RemoteData control can’t handle. For example, if the remote server has a problem accessing the data source, doesn’t have permission, or can’t execute the query as coded, a trappable error results. If the error occurs before your application procedures start, or because of some internal errors, the RemoteData control's Error event is triggered.

The RemoteData control uses and creates the same RDO objects discussed in this chapter. When the appropriate RemoteData control properties are set, the RemoteData control can create an rdoResultset that can be manipulated in code like any other rdoResultset. You can also create an rdoResultset in code and set the RemoteData control’s Resultset property to point to this object.

Note To use the RemoteData control, you need to add it to the Visual Basic Toolbox. To add the RemoteData control to your Toolbox, click the Project menu, then click Components; in the Components dialog box, click the Controls tab and select the “Microsoft RemoteData Control 2.0” check box.

RDO is only supported on 32-bit operating systems (Windows 95 and Windows NT). To use the Remote Data Objects, you must set a reference to the Microsoft Remote Data Object 2.0 object library in the Visual Basic References dialog.


Understanding RemoteData Control Operations

Once your application begins, if sufficient properties have been set at design time, Visual Basic uses the RemoteData control to establish a connection to the ODBC data source. This creates an rdoConnection object by using the control's DataSourceName, UserName, Password, Options, CursorDriver, LoginTimeout, Prompt, and Connect properties. Notice that all of these properties are unique to the RemoteData Control, but are all exposed by the Data control using other properties as shown below:

RemoteData Control Properties | Data Control Property
Connect | Connect.
CursorDriver | (ODBCDirect only).
DataSourceName | DatabaseName.
LoginTimeout | Embedded in the Connect property's connect string.
Options | Options.
Password | Embedded in the Connect property's connect string.
Prompt | (ODBCDirect only).
UserName | Embedded in the Connect property's connect string.

If insufficient information is provided in the RemoteData control properties, the ODBC driver manager shows a dialog box to gather missing parameters, unless you have set the Prompt property to disable this dialog. If the user or your code does not provide sufficient parameters, a trappable error results.

If the connection is established, the RemoteData control sets or resets the Environment, Connection, DataSourceName, Transactions, and Connect properties based on the values used to establish the connection.

Once the connection is established, the RemoteData control immediately executes a query against the data source using the SQL, CursorDriver, Options, LockType, ErrorThreshold, and ResultsetType properties. This creates an rdoResultset object and sets the Resultset, ResultsetType, and Updatable properties. By default, a read-only, static rdoResultset is created. The StillExecuting property is set to True while the rdoResultset is created. If you choose to cancel the query, and the rdAsyncEnable option is set, you can use the Cancel method against the rdoResultset to terminate query processing.

Once the first row of the rdoResultset is available, the StillExecuting property is set to False and the RemoteData control passes column data to each bound control requesting data. The rdoResultset object's EOF property returns False and the RowCount property is set to a nonzero value if any rows resulted from the query. If no data is returned by the rdoResultset, the RemoteData control’s behavior is determined by the EOFAction property. For example, if EOFAction is set to rdAddNew, the RemoteData control's Validation event fires on the current row, followed by an automatic AddNew, followed by a Reposition event on the new row.

Note Make sure that each bound control’s DataField property corresponds to the Name property of the rdoColumn object returned by the rdoResultset or an empty string (""). If it doesn't, a trappable error occurs.

When you are using an Image or PictureBox control bound to an image-containing field in a RemoteData Control (RDC), and the RDC uses batch cursors (CursorDriver = rdUseClientBatch), be aware that the Image or PictureBox control doesn't display the image. To correctly display the image, either set the RDC's Options property to 128 (rdFetchLongColumns), or use a different cursor.

Positioning the Current Row Pointer

Users can manipulate the RemoteData control with the mouse. They can move the current row pointer from row to row, or to the beginning or end of the rdoResultset by clicking the control. The RemoteData control doesn’t permit the user to move off either end of the rdoResultset using the mouse. You can’t set focus to the RemoteData control.

Keep the following guidelines in mind when writing code that will reposition to the current row pointer:

  • Before each reposition, the bound controls that have changed data are queried for new data for the current row. The Validate event is invoked, and if not canceled by the action argument, the data source is updated if the data changed.

  • After the RemoteData control positions to a new row in the data source, column data is passed to the bound controls and the Reposition event is invoked.

  • Once either end of the rdoResultset is reached, the RemoteData control’s behavior is determined by the EOFAction and BOFAction properties.

  • You can use the MoreResults method against the rdoResultset to complete processing of the current result set and determine if additional result sets are available. If MoreResults returns True, the process of handling the rdoResultset is restarted — just as if a new query had been executed. The previous result set is no longer available.


Creating an rdoResultset Object Programmatically

In some cases you might need to create an rdoResultset independently of the RemoteData control, but still need to access the columns of the result set using bound controls. In this situation, simply create an rdoResultset and assign it to the RemoteData control's Resultset property.

Note The Resultset property is only available at run time.

You can also create the RemoteData control's rdoResultset programmatically by setting the RemoteData control's properties in code and using the Refresh method. Any existing RemoteData control's rdoResultset is discarded.

The RDO objects created by the RemoteData control can be manipulated independently of the control — with or without bound controls. The rdoConnection and rdoResultset objects each have properties and methods of their own that you can use with your procedures. In addition, the RemoteData control exposes the Environment, Connection, RowsetSize, and several other properties used to create the rdoResultset — which is exposed through the Resultset property.

For example, the MoveNext method of an rdoResultset object moves the current row to the next row in the rdoResultset. To invoke this method with an rdoResultset created by a RemoteData control, you could use this code:

MSRDC1.Resultset.MoveNext

If you need to execute a parameter query using the RemoteData control, create an rdoResultset based on an rdoQuery, and then set the RemoteData control's Resultset property to the newly created rdoResultset. You can also use this technique to access another rdoEnvironment.


RemoteData Control Asynchronous Operation

If you set the RemoteData control's Options property to rdAsyncEnable before the RemoteData control creates the rdoResultset, control returns to your application before the rdoResultset contains rows. Just as with other asynchronous RDO programming, check the StillExecuting property of the rdoResultset object to determine when the first data row is available. To cancel the query, use the rdoResultset.Cancel method. Once the query is complete, the RemoteData control's QueryCompleted event is invoked, which indicates that new data is now available from the rdoResultset.

If you do not specify asynchronous operation with rdAsyncEnable, no other Visual Basic operations or events can occur until the first data row of the rdoResultset is fetched. However, other Windows-based applications can continue executing while the rdoResultset is being created.

When you use a RemoteData control to create an rdoResultset object, the remote database engine automatically populates the rdoResultset object as a background task. As a result, any existing bookmarks are saved, the user doesn’t need to manipulate the RemoteData control, and you don’t need to invoke the MoveLast method in code. In addition, page locks used to create the rdoResultset are released more quickly, making it possible for other rdoResultset objects to access the same data.

You can also create an rdoResultset object using the OpenResultset method and set the Resultset property to the new rdoResultset. If this is done, all other appropriate RemoteData control properties are reset to reflect the new rdoResultset and rdoConnection.


Intercepting RemoteData Control Errors

The RemoteData control handles errors that occur during different stages of the development cycle. Each of these stages requires a different type of error management on your part or in your code:

Development stages | How the RemoteData control handles errors
For example, when setting RemoteData Control properties to access a specific database. | Visual Basic exposes error dialogs indicating the problem.
Before the form containing the RemoteData Control is loaded (before Form_Load is completed). | The RemoteData Control fires the Error event.
After the form containing the RemoteData Control is loaded (after Form_Load is completed). | The RemoteData Control or RDO triggers a trappable error.
A user clicks a RemoteData control button. | The RemoteData Control fires the Error event.
The RemoteData control attempts to open an rdoConnection and creates rdoResultset objects after the Form_Load event. | The RemoteData Control fires the Error event.
A custom control performs an operation, such as the MoveNext method, the AddNew method, or the Delete method. | The RemoteData Control fires the Error event.

When the Error event fires, you have the option of handling the error yourself, or simply letting Visual Basic expose an error dialog to the user.


Topic Not Found

No matching topic was found. Some possible reasons and remedies are as follows:

  • The file (.chm) that contains the destination topic is not in the expected location.

    Remedy: Rerun the MSDN Library Setup.

  • The file (.chm) that contains the current topic or destination topic is out-of-date and the link is no longer valid.

    Remedy: Install the latest version of the MSDN Library.

  • The MSDN Library CD is not in the CD-ROM drive.

    Remedy: Insert the MSDN Library CD into the CD-ROM drive.


Customizing Areas of the Hierarchical FlexGrid

The Hierarchical FlexGrid contains different areas that you can customize. You can customize these areas using the Hierarchical FlexGrid's Property Pages dialog box or programmatically, using the Code Editor window. Customization of the areas can enhance readability and increase the usefulness of your Hierarchical FlexGrid.

The areas of the Hierarchical FlexGrid include:

  • Standard

    The standard area of your Hierarchical FlexGrid is the cells that contain data-bound information.

  • Band

    The band area contains display information for each Recordset within your Hierarchical FlexGrid. See Formatting a Band for more information.

  • Fixed

    The fixed area consists of rows and columns that are fixed, or static.

  • Header

    The column header area identifies information about the Recordset that is bound to your Hierarchical FlexGrid. When used, column headers repeat for each band in your Hierarchical FlexGrid. See Formatting a Band for more information.

  • Indent

    When displaying bands vertically, the indent area of your Hierarchical FlexGrid is the area that indents a band of data by a specified number of columns. The indented column before each band contains empty, non-working cells. See Formatting a Band for more information.

  • Unpopulated

    The unpopulated area of your Hierarchical FlexGrid contains the cells to the right and below the working areas of your Hierarchical FlexGrid. This area is empty and contains non-working cells.

Customizing Standard Appearance

You can customize the appearance of the standard area of your Hierarchical FlexGrid, making information more clear and accessible. The standard area contains the cells that are bound to data.

To customize the appearance of the standard area of a Hierarchical FlexGrid

  1. Right-click your Hierarchical FlexGrid, and select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the General tab, set the following standard properties:
    Property | Description
    Rows | Number of rows in the Hierarchical FlexGrid.
    Columns | Number of columns in the Hierarchical FlexGrid.
    AllowBigSelection | Enables the user to select an entire column or row within the Hierarchical FlexGrid by clicking on a column or row header.
    MousePointer Determines the type of mouse pointer.

    Note To use a custom mouse pointer, you must assign a custom icon to the MouseIcon property from the Picture tab.
    FillStyle | Determines whether text and cell formatting should apply to a single cell or all selected cells.
    SelectionMode | Determines how your Hierarchical FlexGrid will allow selection by the user.
    AllowUserResizing | Determines whether the user is allowed to resize rows and columns using the mouse.
    ScrollBars | Determines whether the Hierarchical FlexGrid has a scrollbar, and if so, whether this is horizontal or vertical, or both.
    HighLight | Determines when cells should receive emphasis.
    FocusRect | Defines the type of focus rectangle that your Hierarchical FlexGrid draws around the current cell.

  3. On the Style tab, set the following standard properties:
    Property | Description
    MergeCells | Whether cells with identical contents are grouped into a single cell spanning multiple cells or rows.
    RowHeightMin | A minimum row height for the entire control, in twips.
    PictureType | Whether a picture displays in color or monochrome.
    WordWrap | Whether your Hierarchical FlexGrid allows text within a cell to wrap or have multiple lines.

  4. On the Font tab, set the following standard properties:
    Property | Description
    Font | The name of the font style.
    Size | The size of the font, in points.
    Effects | The text effect: underline or strikeout, or the text style: bold or italic. You can select up to four Effects check boxes.
    Sample Text | A preview of the font selection.

  5. Click OK to apply the standard properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Customizing Fixed Appearance

You can create fixed rows or columns to display in your Hierarchical FlexGrid. To increase readability, you should use a fixed area to display one heading set instead of using repeating column headers for each band. This area remains static, identifying the rows, below, or columns, to its right.

To customize the appearance of the fixed area of a Hierarchical FlexGrid

  1. Right-click your Hierarchical FlexGrid, and select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the General tab, specify the number of fixed rows and columns in the Fixed Rows and Fixed Cols text boxes, and then click Apply.

  3. On the Style tab, select the style for the fixed text from the TextStyleFixed box, and then click Apply.

  4. On the Font tab, set the following standard properties:
    Property | Description
    Font | The name of the font style.
    Size | The size of the font, in points.
    Effects | The text effect: underline or strikeout, or the text style: bold or italic. You can select up to four Effects check boxes.
    Sample Text | A preview of the font selection.

  5. From the Color tab, assign each fixed property a color. To do this, first select a Color Set. Next, click the property you wish to change (for example, BackColorFixed), click a color in the Color Palette, and then click Apply. Repeat this for each fixed area that you wish to change.

    Note When using Windows Default colors, the Hierarchical FlexGrid appears in the colors specified in the your system's Display Control Panel. In addition to changing Hierarchical FlexGrid color properties to a standard or Windows default color, you can create your own color definitions by clicking Edit Custom Color and using the resulting Color dialog box. For information on each property, see the Hierarchical FlexGrid's property topics.

  6. Click OK to apply the fixed properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Customizing Header Appearance

You can change the formatting and display properties of the band header in your Hierarchical FlexGrid.

To customize the header appearance of a Hierarchical FlexGrid

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Bands tab, select a heading style from the TextStyleHeader box, and then select ColumnHeaders.

    Note To prevent your Hierarchical FlexGrid from displaying duplicate headings, you must set the Fixed Row text box, from the General tab, to 0. In addition, if you want to display only one set of headers for a band at the top of the Hierarchical FlexGrid, fixed rows must be used instead of column headers.

  3. In the Column Caption and Column Name list boxes, specify the columns to display. By default, this list includes all the fields in the Recordset with their field name. To remove a Field object from the display or to change its name, use the Column Caption list. To deselect a field, click the corresponding check mark. To change its name, click the name to select it, then click it again to access edit mode to rename it.

  4. Click OK to apply the band properties to your Hierarchical FlexGrid and close the Property Pages dialog box.

Customizing Unpopulated Areas

You can change the formatting and display properties of the unpopulated area of your Hierarchical FlexGrid. This unpopulated area is empty and contains no data.

To customize the appearance of the unpopulated area of the Hierarchical FlexGrid

  1. Right-click your Hierarchical FlexGrid, and then select Properties from the shortcut menu. The Hierarchical FlexGrid's Property Pages dialog box appears.

  2. On the Style tab, change the GridLinesUnpopulated property.

  3. On the Color tab, assign each fixed property a color. To do this, first select a Color Set. Next, click the property to change (for example, BackColorUnpopulated), click a color in the Color Palette, and then click Apply. Repeat this for each unpopulated area that you wish to change.

    Note When using Windows Default colors, the Hierarchical FlexGrid appears in the colors specified in the your Display Control Panel. In addition to changing Hierarchical FlexGrid colors properties to a standard or Windows default color, you can create your own color definitions by clicking Edit Custom Color and using the resulting Color dialog box.

  4. Click OK to apply the fixed properties to your Hierarchical FlexGrid and close the Property Pages dialog box.


Using the File-System Controls (Directory List Box, Drive List Box, and File List Box)

Many applications must present information about disk drives, directories, and files. To allow users of your applications to explore the file system, Visual Basic provides two alternatives. You can use the standard dialog boxes provided by the common dialog control, or you can build custom dialogs using your own combinations of three specialized controls: the drive list box, the directory list box, and the file list box.

You can use the file-system controls to allow users to investigate and choose among available disk files in your applications. Consider using the common dialog control if you just need a standard File Open or Save dialog box.

For More Information See "Using the Common Dialog Control" earlier in this chapter for more information.

Sample Application: Winseek.vbp

Many of the code examples are taken from the Winseek sample application (Winseek.vbp) which is listed in the Samples directory.


Visual Basic and the Microsoft Message Queue

Visual Basic makes it easy to build solutions that utilize the Microsoft Message Queue.

The Microsoft Message Queue (MSMQ) offers a fault-tolerant and easy-to-use way to move information around an enterprise network. MSMQ is available as part of the Windows Option Pack (see http://backoffice.microsoft.com/) and includes the "Microsoft Message Queue Object Library." Once installed, this library can be referenced in Visual Basic via the "References" dialog (available from the Project menu) to provide users access to MSMQ’s object library.

MSMQ allows programmers to create application message queues and pass user-defined messages between them. These messages can also be made part of Microsoft Transaction Server (MTS) transactions, making their delivery contingent on the success of a transaction. This offers many possibilities to easily create transactional systems.

Sample Application: Message Queue

A suite of samples (Message Queue) demonstrating simple use of MSMQ by Visual Basic are included with Visual Basic 6.0. These sample use Visual Basic Property Bags persisted as text to pass information between applications. For information about the location of Visual Basic sample applications, see Visual Basic Samples


Working with Lists in Webclasses

One common scenario is to populate the contents of a list or select element at run time, using text replacement. List elements and select elements produce elements on your HTML page that are similar to the list box and combo box controls you use on Visual Basic forms.

Prepopulating Select Elements

Select elements in HTML have the following syntax:

<select name="List1" id="List1" value="List1">
   <option value="List">First list item
   <option value="List">Second list item
   <option value="List">Third list item
</select>

This select element is prepopulated with the list items indicated between the <select> and </select> tags.

Populating a List Element in Code

Select elements have a restriction that only <option> tags may appear between the opening and closing <select> tags. Therefore, to populate such an element at run time using text replacements, you would do the following:

  • Modify your HTML to remove the select element and put text replacement tags in its place.

  • Write code in the ProcessTags event procedure that would insert the select element and its option tags when the tag prefix is processed.

Your resulting HTML code might look like this:

<WC@SelectElement></WC@SelectElement>

Your code in the ProcessTags procedure would take the form:

If TagName = "wc@SelectElement" Then
      'Code here using Response.Write to add a select element and its
      'contents to the page.
EndIf

For More Information See "Performing Text Replacements in a Webclass" for more information on tag prefixes, the ProcessTags procedure, and replacing elements in your templates.


Topics on Enterprise Programming

The Visual Basic Enterprise edition includes development tools for building three-tiered client/server applications.

Visual Basic Enterprise Edition Features

Professional and Enterprise Editions Links to new and enhanced enterprise programming features.

Developing for the Enterprise

Visual Studio Enterprise Edition Visual Studio's guide to distributed application development for business-critical data management. The book includes information on designing solutions based on a flexible architecture, developing large-scale data access strategies, and creating integrated solutions with Visual Studio tools.


Topics on Internet Programming

You can use Visual Basic to create Internet applications that merge HTML pages with Visual Basic code. Use these links to get the information you need to create your own applications for the Internet or an intranet.

What's New in Internet Features?

Professional and Enterprise Editions Provides links to what's new or modified in client- and server-side Web programming.

Introduction to Internet Applications

Professional and Enterprise Editions Introduces the developmental approach for creating client-based and server-based applications for the Internet.

Developing DHTML Applications

Professional and Enterprise Editions Explains how to create, test, and deploy client-based internet applications.

Developing IIS Applications

Professional and Enterprise Editions Explains how to create, test, and deploy server-based Internet applications.

Downloading ActiveX Components

Professional and Enterprise Editions Explains how to package and deploy ActiveX controls, components, and user documents on the Web, through Internet Component Download.