VB6 UML Design and Development (Wrox Press)
VB6 UML Design and Development (Wrox Press)
Language
Many people have a rather vague idea about what UML, or the Unified Modeling Language, actually is
and how and why it's used. This is especially problematic in Visual Basic because it is so easy to dive
straight into the IDE and produce a lot of code very quickly. This is one of Visual Basic's best and worst
points.
Visual Basic is great in that you can knock up a project with relatively little effort, especially if you use any
of the myriad of wizards that Microsoft have so helpfully provided. There is a flip side though. Because we
can code VB relatively easily, there's a tendency not to think about project design beforehand, but rather to
do the design 'as we go along'... which is just a gentle way of admitting that no design work is going into
the project at all. VB programmers can get away with this on small experimental applications, but when
they move into more ambitious programs - and most particularly, into multi-tier application development -
then it's a serious professional error to rely on such an ad hoc design approach.
Designing even a two-tier client/server application is scary enough though. This is when we really should
begin to think about using UML. UML, as you will see throughout the book, provides an object-oriented
method by which you, and others in your development team, can create a design for an effective, efficient
application.
However, while UML does provide a mechanism that helps us to design object-oriented projects, it offers
little in the way of help when it comes to coding the project.
So there you have it: UML is a set of standard models to design object-oriented projects - but it is not a
description of how to actually implement those models. We will be exploring the full meaning of this
definition of UML throughout the book - but it's not a bad place to start.
Before we go any further, you might be considering this question: is UML the only set of standard models
to help with OO programming? A brief history of UML will answer this inquiry, and also offer you an idea
of the impressive credentials that UML has behind it as a practical tool.
In 1994, the three principal authors of Booch, (Grady Booch), OMT (James Rumbaugh) and OOSE (Ivor
Jacobson) started the process of merging the three methods to form a Unified Modeling Language.
The Unified Modeling Language has since been developed by a consortium of companies led by Rational
Software Corporation. In November 1997, UML 1.1 was certified as an industry standard by the Object
Management Group (OMG).
8
Introducing the Unified Modelling Language
Members of the OMG include almost all of the major software companies, including Microsoft, IBM,
Computer Associates, Oracle, Sun, and Compaq. Such a strong industry alliance ensures that the UML is
well accepted and implemented in the computer industry. Currently, many modeling tools such as Rational's
Rational Rose support the UML.
A model is a description of the problem we are set to solve. It simplifies the reality by capturing a subset of
entities and relationships in the problem domain.
A problem domain describes not only a particular problem but also the conditions under
which the problem occurs. It's therefore a description of a problem and the relevant context
of that problem.
A model shows us what the problem is and how we are going to tackle it. We may use diagrams, text, or
any other agreed form of communication to present the model.
A modeling language, therefore, is a language for describing models. Modeling languages generally use
diagrams to represent various entities and their relationships within the model.
UML accomplishes these tasks by having a series of different models. Each model represents a different
view of the project. Some models are built from others, so there is a logical sequence in which the models
are built.
9
VB6 UML
I mentioned earlier that without employing a decent design method such as UML, most VB programmers
will only really be able to develop programs in an ad hoc manner. This will, more often than not, lead to
badly thought through programs and, ultimately, a lot of unsuccessful VB applications. We therefore need
to use UML because it's a key to successful project design and development for us.
Many developers quickly feel overwhelmed when they try to build Enterprise level Visual Basic projects.
These projects are difficult and complex. They not only require knowledge of how to program several
different types of Visual Basic components, but also how to make these components work with server
products, such as Microsoft Transaction Server (MTS), SQL Server (or other databases) and Internet
Information Server (IIS).
As this book will show you, UML modeling is the key to being able to successfully build an advanced VB
project - such as an Enterprise project. You will see how UML models help us to understand our user's
requirements, then transform these requirements into a realistic project design, and finally help turn that
design into the most efficient Visual Basic code possible... with the least amount of effort. These models
will also cut down on overall costs and development time.
While UML is an essential tool for creating Visual Basic projects, it is only a part of a larger puzzle. In
addition to understanding UML, a Visual Basic developer must:
Understand the features of Visual Basic, and how these features integrate with server products
Know how to apply these VB features to the design of the project
Know how to turn their design into Visual Basic code
When we first try to put all of these pieces together in a Visual Basic project, we can easily get lost in the
maze of components, programming techniques, and constraints of the system. From our very first steps into
the world of UML, I will show you how UML models can create order out of this chaos. If you want to
learn how to build Visual Basic Enterprise solutions in a rational, sane way, with a high probability of
success, then this book is for you.
There is a remarkably strong connection between UML and project management - please consider these
three points:
A successful object-oriented Visual Basic project is built on a foundation of good project management
The foundation of good project management is thoroughly designing your project
The foundation for good object-oriented design is UML
We shall discuss the concept of project management in detail in the next chapter. However, I wish to
emphasize at this point that ultimately a successful VB project depends on using UML.
10
Introducing the Unified Modelling Language
This is a visual representation of a simple fact: an understanding of UML provides the foundations for
developing a well-designed and well-managed Visual Basic project.
Requirements Analysis
Analysis
Design
Implementation - Coding and Testing
The analysis and design phases, especially, involve the use of UML diagrams.
In case you're wondering, we'll be learning more about the full details of UML diagrams
and exactly what those details mean from Chapter 3 onwards. For now, enjoy the whirlwind
tour so you can see how all the parts of UML fit together to form a cogent design method for
VB programmers.
Things in UML describe conceptual and physical elements in the application domain
Relationships connect things together
These two elements are brought together in UML diagrams to help us visualize things and their
relationships in a well-structured format.
11
VB6 UML
UML Diagrams
There are quite a few UML diagrams that you can use when designing your application, and you can pick
and choose those which will be of most use to you. However, there is a basic core set of diagrams that you
will almost certainly use. This core set of diagrams includes:
We'll now run through these types of diagram at whirlwind pace - I'll explain how you can methodically go
about creating your own diagrams later on in the book, when we come to build a real-world project. You'll
notice, as we run through them, that some diagram types have sub-types themselves (such as collaboration
and sequence diagrams). This may be your first clue as to the richness and diversity of UML as an
analytical design tool.
Notice that as we progress through this sequence of UML diagrams, we will also be
progressing towards an ever more focused and clearly defined idea of the project we
are designing and planning to develop. This is one of the fundamental points of the
UML approach.
Use case models can be built from interviews with the user, and are the first step in converting the user's
needs and requirements into a useful model.
Use cases are more like a model than a diagram because they describe the system, or parts
of the system, with words rather than with pictures.
Use cases are detailed enough to include all of the information on the project, but simple enough for even
the most technically challenged user to understand. Use cases can also be associated with business rules,
which explain special rules, related to the use case.
Let's take an example. In an order entry application, the use cases could include descriptions of various sub-
parts of the system. These sub-parts, that together could make up the whole system, could be such things
such as Taking an Order, Creating a New Customer, etc. For the use case Create New Customer, there
could be a verbal description of the process of creating a new customer that looked as follows:
12
Introducing the Unified Modelling Language
Overview
The main purpose of this use case is to create a new Customer
Primary Actor
Sales Representative
Secondary Actor
None
Starting Point
The use case starts when the actor makes a request to create a new Customer
End Point
The actor's request to create a Customer is either completed or cancelled
Flow of Events
The actor is prompted to enter information that defines the Customer, such as Name,
Address, etc. The actor will then enter the information on the Customer.
The actor can choose to save the information or cancel the operation. If the actor decides
to save the information the new Customer is created in the system, and the list of
Customers is updated.
Measurable Result
A Customer is added to the system
Business Rules
Customer
Customer Fields
Restrict Customer Create
Without getting too heavily involved right now in the details of this use case, what we're seeing here is a
verbal description of what happens when someone using the program we want to design needs to create a
new customer. Actors are identified (sales representatives). Start and end points located (in this case, a
sales representative asking to create a new customer, and successfully having created a new customer), and
a flow of events are drawn up to explain how the system can get from the start point to the end point.
Further to this, some alternative sequences are mentioned (perhaps the user decides they don't want to add a
customer half way through the process), measurable results are defined, and some business rules are
created.
13
VB6 UML
One of these business rules is called Restrict Customer Create. This business rule has been added to the
design of this project to ensure that nobody will be able to create a customer who already exists on the
system. This Restrict Customer Create business rule might be written as follows:
Overview
This rule is for when a Customer is added to the system
Business Rule
Each Customer must have a unique CustomerID
This definition simply describes the business rule itself and what type of rule it is, and then states the
conditions of the rule being defined. Business rules are highly contextual to the actual business
environment in which the program is being written, but this is a good way to present them clearly and
professionally.
Depending on the size of the system we're working one, we may actually need to create quite a few of these
use case statements and business rules before we have captured the key aspects of the system we're
designing. It's crucial, however, that we draw up these statements from the people who will be using the
system, and the people who want to see the system in place. It's the first step in our design process.
That's all we need to know about use cases for now - but you'll see a much more in-depth
discussion and tutorial of them in Chapter 3.
Interaction Diagrams
Interaction diagrams are the next step of the UML design process. Interaction diagrams concentrate on
showing how objects or things in the system interact with each other to give a dynamic view of the system.
There are two basic types of interaction diagram:
sequence diagrams
collaboration diagrams
Essentially these both model the same information, except that sequence diagrams emphasize time ordering
whereas collaboration diagrams show spatial or structural organization. We'll take a quick look at both of
these now, and pick up on sequence diagrams later on in Chapter 4.
14
Introducing the Unified Modelling Language
For the most part, you choose to create either collaboration diagrams or sequence
diagrams. While we will discuss both types in this whirlwind tour of UML, we will primarily
be using sequence diagrams throughout this book.
Sequence Diagrams
This type of diagram can be used to convert the written use case models that we saw in the previous section
into a clearer visual model. This visual model will show how the objects associated with a particular use
case communicate with each other and with users over time. Sequence diagrams are very general. For
example, they may show that some Object 1 passes a message to some other Object 2, and that Object 2
then performs some operation within itself and finally returns the message back:
The internal workings of Object 1 that led to the creation of the message, and the internal workings of
Object 2 that led to the return message, are not shown in sequence diagrams. (Details of the inner workings
of the objects are represented in another type of diagram called an activity diagram - which we'll see in the
next phase of the UML design process.)
Sequence diagrams map out every possible sequence of events that can be performed within each use case,
including correct and incorrect paths. The correct paths in the sequence diagrams can be used to design the
GUI of the project as they show what the user will need to do to interact with the application. Incorrect
sequences will later be used to map out errors and how to handle these errors.
Sequence diagrams also show what public methods and properties our components must have. You can
compare the sequence diagrams for one or more components and attempt to find patterns that exist that can
be used to simplify the coding of the components.
Collaboration Diagrams
Collaboration diagrams are also built from the use cases - but that this time the emphasis is on the spatial
distribution of the objects involved. This is not to say that there are no temporal elements in collaboration
diagrams, since the sequence of events is mapped using numbers:
15
VB6 UML
Personally, I often find this type of diagram quite confusing in comparison with the equivalent sequence
diagrams. This particular collaboration diagram presents the same situation as the sequence diagram we just
looked at - Object 1 and Object 2 with exactly the same relationships as before. However, it should be said
that there are times when collaboration diagrams can make good sense - especially if we find that we want
to emphasize a set of objects themselves rather than any sequence of events between them.
We're still not finished yet though - the final stage in the overall UML design process is to move on to our
activity diagrams.
Class Diagrams
A class diagram is a simple static picture of a class; as such, it will include all of the public and private
methods and properties of that class. Class diagrams are, of course, built from use cases, sequence and
activity diagrams that we've been developing throughout the UML design process.
This sample class diagram is simply a schematic layout of the relevant information that we would need to
go away and create the objects we've developed in our UML design process straight into the VB
programming environment.
Naturally, for a larger project we would probably need to derive many such class diagrams
in order to complete the design of all the objects involved in our system.
16
Introducing the Unified Modelling Language
Using class diagrams and software products, such as HOW from Riverton Software, we can automatically
generate code for our applications. In this book I will not use any UML software products however: my
aims here are to teach you what's really happening from the ground up. Once you understand the underlying
concepts, the decision is yours whether you choose to use a package such as HOW, or write all your own
code directly.
Activity Diagrams
Activity diagrams take the information available from the collaboration and sequence diagrams that we've
just looked at, and present that information in a more detailed fashion. The purpose of activity diagrams is
now to show the inner workings for a particular object.
As you may have noticed, we are gradually moving towards more and more detail
about our project design as we proceed through the different UML diagrams.
Activity diagrams can map out a method or property showing what that method or property has to do in a
step-by-step manner. Activity diagrams will look very similar to mapping out a method or property using
pseudo-code. This detailed map can then be used to explore the best method of coding a method or
property, check for missing or unnecessary sections, and as a guide to writing the code. Here is a sample
activity diagram:
This activity diagram simply specifies what Activity is to be initiated at a certain Decision point,
depending on the outcome of that Decision. This is a step-by-step definition of certain situations that
pertain to the project we are designing and about to develop in VB, and is a considerable way forward in
our journey towards defining a project and preparing it for development and implementation.
17
VB6 UML
Other Diagrams
The diagrams we've looked at so far (use cases-sequence-activity-class diagrams) form the essential parts of
UML that we need to build a Visual Basic project, and so we will be focusing on them in greater detail
throughout the text. There are, however, other models that I won't be discussing in great detail, and I
mention them now for sake of completeness for the interested reader. These other diagrams include:
Now that we've defined the core diagrams that are involved with designing a project with UML, we can
draw our first conclusions about how these diagrams can be put to together.
So far, however, I've suggested that there's a completely straight progression from one type of diagram to
the next - that we can move from use cases to sequence diagrams to class diagrams to activity diagrams to
VB. While these different UML diagrams most certainly do move us towards an ever more focussed
definition of our project design, in reality we can't always travel in a straight sequence. Project design is
often a more iterative process than this.
A rather more realistic sequence for the UML process is presented in the following diagram:
I think it's a good idea to take this on board as soon as possible: common sense tells us that sometimes our
later thoughts on some sequence diagrams, for instance, may affect our earlier thoughts on how we built our
activity diagrams - and so on.
18
Introducing the Unified Modelling Language
We should also recognize that this picture needs to take into account the constraints and rules of the other
tools we are using to build this project - such as the programming language and technology that will be used
to implement the project itself. While the private methods and properties of our classes, as well as the series
of steps our code will follow (as shown in our activity diagrams) are all internal to our system, how our
system will work from the inside is determined by the choices that we make when building our system.
These external factors must also therefore be taken into account:
Using information on how each of these choices affects our system, we will be able to define the different
parts of our activity diagrams and complete the methods and properties of our classes.
The user's requirements and the business requirements will create the use case documents. These use cases
will mold the project design from the outside. The programming language, operating system, server
components and technologies will mold the project from the inside. These two forces meet at the activity
diagram, where both external and internal forces shape the methods and properties of each class.
19
VB6 UML
VB and UML
The UML models and diagrams are powerful tools that we can use to build complex projects.
Unfortunately, translating theory into reality sometimes is not so easy. UML was designed for languages
that are completely object-oriented, and for users who are well versed in the theoretical aspects of software
engineering.
Visual Basic approaches OOP from a slightly different approach from languages such as C++. Visual Basic
users tend to be less interested in theory and more interested in application. Our presentation of UML will
be geared to both the Visual Basic language and application development. To accomplish this, we will keep
the explanations of UML simple and show how to apply each model to building a VB project. We will
occasionally bend the models a little bit, but in the end, we will have a method that is specifically geared
towards Visual Basic. Before we can move on there are two important questions that need to be answered:
Does the Visual Basic programming language possess enough object-oriented features to be used with
an object-oriented modeling language such as UML?
Do Visual Basic Projects require good management practices such as modeling the project with UML?
Whether Visual Basic can be considered an object-oriented language is a rather controversial topic. I will
give you several arguments, which I hope will convince you that Visual Basic is object-oriented enough to
make OOP projects, and that these projects can best be built using UML.
Technically speaking, an object-oriented language must include inheritance. As Visual Basic does not
include inheritance, by this definition it is not object-oriented. Is this the end of the discussion? For some
people it is, but I don't feel the discussion should end there.
Inheritance is a powerful tool that allows us to build many similar objects from one base class. For
example, I can create a Dog, Cat and Rat object from a common Animal class. The Animal class will have
properties such as Height, Weight, Color and methods such as Walk, Sleep, and Run. This relationship is
called an is a relationship. A Dog is an Animal. A Cat is an Animal. Unfortunately, in VB we cannot make
one base class (such as Animal) and use the identical class to create a Dog, Rat and Cat object.
However, we can get around Visual Basic's lack of inheritance by using object hierarchies. The lack of
inheritance in the Visual Basic language does not prevent us from making objects that have an is a
relationship, it is just more difficult to do so. Inheritance can have some problems, such as when an existing
method or property in a base class has to be changed. Inheritance is often misused and is not the solution to
every problem. While it would be nice to have inheritance in Visual Basic, it does not prevent us from
making object-oriented components.
20
Introducing the Unified Modelling Language
To demonstrate this let us focus on what these components are supposed to do, rather than the methods we
use to build our object-oriented components. An object-oriented language must be able to build components
that have the following properties:
Visual Basic components designed with UML models, and working under an Enterprise system that
includes MTS, can be built with every one of those properties. I'm not going to say we will do this in the
same manner as we would in a C++ program - I'll admit that sometimes we do have to get really creative to
get VB components to do these things. Yet a properly designed VB component can do everything a
component built from a true object-oriented language can do.
Because VB components can fulfill the requirements of an object-oriented component, I will treat Visual
Basic as if it were an object-oriented language. You can form your own opinion. Hopefully, I have not
offended you by saying that Visual Basic is object-oriented.
If you would like to learn more about objects and OOP, then I recommend you read
"Beginning Visual Basic 6 Objects", (Wrox Press) for an introduction to object-oriented
programming, and "Professional Visual Basic 6 Business Objects", also published by Wrox
Press, for a more advanced discussion on using objects with VB6 in a business environment.
Project management is the most essential part of a successful Visual Basic project.
Regardless of the language used, most software projects are failures. A quick look at the statistics shows
that one-third of all corporate software projects are cancelled before completion, and five out of six
completed projects are unable to deliver the desired features. It doesn't have to be this way. This entire
book focuses on showing you a coherent, clear way to manage an object-oriented Visual Basic project using
UML. If you follow the methods outlined in this book, you can almost guarantee the success of your
projects.
21
VB6 UML
My Approach
My approach in this book is different from what you'll find in most other UML books. I want this book to
be a tool that will teach you UML and also help you to able to program an Enterprise level Visual Basic
project. Therefore, the goals of this book are as follows:
To explain each of the essential UML diagrams and show you how to build them yourself
To teach you how to use these UML diagrams to design an Enterprise level Visual Basic application
To show you how to convert a UML design into Visual Basic code
To attain my second and third goals, my book will use UML diagrams to design a three-tier Visual Basic 6
project.
This project can be used, with minor modification, in many of the Enterprise Projects you may build. From
Chapter 3 onwards, we shall be working our way through the complexities of building this type of real-
world application. Together, we shall deal with the same questions, problems, and difficulties that you are
likely to encounter when making your own applications. At every step of the way, I will demonstrate how
UML will help us find the answers, solve our problems, and remove the complexities and difficulties of this
project.
Almost every chapter will introduce and explain another piece of the Enterprise puzzle, and show just how
UML can help you put that piece into its appropriate place. In the next chapter, I will lay the foundation
you need to understand the book's project by giving you grounding in good project management and good
Visual Basic programming techniques.
As you may have a limited amount of experience with the wide range of BackOffice products and Visual
Basic programming techniques needed to build this project, I will also show you everything you need to
know about programming a three-tier Visual Basic project - from RDS (Remote Data Services) to ADO
(ActiveX Data Objects) and MTS (Microsoft Transaction Server).
A Real-World Project
The Visual Basic project that we're going to build will be an Order Entry project. We will go through each
stage, as it would happen on a real project. As UML models are needed to build our Order Entry
application, these models will be defined in detail and then used.
The final model we build can be used as the building block for any three-tier Visual
Basic application.
This book follows the normal progression of a project. Each step of the way our UML models will be
guiding us and helping us build our project. We'll begin to examine this project (and how to manage it)
straight away in the next chapter.
22
Introducing the Unified Modelling Language
Summary
This chapter has been designed really just to familiarize you with some of the what and why of UML.
Hopefully, it should now seem less like a vague concept and more like a sensible practical methodology
that you are looking forward to seeing implemented in the design of a three-tier Visual Basic project.
As we progress through this book we will consider each aspect of UML in more detail as we see it in action
as our project develops.
Now that we've taken an initial glimpse at UML and some of what it can offer us, we will move on to taking
a closer look at the project that we're going to be developing throughout the book, and some basic elements
of project management.
23
Project Design and Management
Visual Basic's ability to create a wide range of powerful, efficient applications has made it one of the most
commonly used programming languages. Unfortunately, its relative ease of use has often resulted in
programmers neglecting one of the most important parts of project management: the formal design phase.
While skipping a formal design phase might save some time at the beginning of the project, it will prove
costly later on.
How costly? In the best-case scenario, a great deal of time will be needed toward the end of the project,
during the maintenance and upgrade phases. In the worst-case scenario, a lack of planning will result in a
complete failure of the project. Companies are still creating applications without a formal design phase,
believing that they are cutting back on the overall time of the project. In reality, this will always result in a
short-term gain and a long-term loss.
UML is ultimately about creating successful projects. Before we approach the details and application of
UML (from Chapter 3 onwards), it seems appropriate to consider the other techniques and practices that are
allied with UML, and that we will see through the pages of this book.
The key to any successful project is design and management, so we'll be looking at the following subjects
in this chapter, all of which are allied in principle to the UML approach:
Designing a Project
Creating Standards
Frameworks and our Northwind project
Patterns - making future projects easier
The Project Development Lifecycle
The Enterprise Application Model
Techniques for Project Management
Part 1: Project Design
Part 2: Project Implementation
VB6 UML
Designing a Project
When we're designing any project, we must have a general plan to guide that design process. Imagine you
were an architect designing a ranch-style house. As an architect you have a wide range of freedom with
regard to how you design the ranch house. Yet, there are certain design limitations and various rules or
standards that must be followed. You cannot, for example, put the bedrooms on the second floor, because a
ranch house has no second floor; nor are you likely to have the front door leading directly into the master
bedroom. You will also have patterns which are common to many houses. The design of the ranch house
will be built upon established standards, an existing framework, and some established patterns.
A framework is a skeleton for the design. It provides merely the bare bones, but it does give the architect a
guide to work off. This framework will be built out of different components, such as the entrance hallway,
the master bedroom, the living room, etc. Each of these components will be built from a well-defined
pattern. An example of a pattern for an entrance hallway for a house could be:
The front door leads into the entrance hallway. The hallway will have a coat closet and will
lead into the living room.
The architect creates a design using components made from patterns, and places these components in their
appropriate places within an established framework. The framework, components and patterns come from
years of experience, testing and experimentation to find the best possible elements to design a house:
Just as an architect has a framework and patterns, software developers also have frameworks that tell them
where to place their components, and patterns that describe how these components should be designed and
built. We are now going to consider exactly what standards, frameworks and patterns are in relation to our
Visual Basic project.
26
Project Design and Management
Standards
Before a single line of code is written, we must create a set of standards. Standards fall into the following
categories:
Documentation Standards
Prior to creating UML documents, we must have a set of standards explaining how these diagrams will be
drawn, where they will be stored, and what procedures will be used to access and edit them. It is pointless
to have someone working on detailed UML design documents if these documents simply get filed away and
forgotten. Making sure that there is a repository to hold all of the documents, prior to creating any of them,
ensures that nothing will get lost or forgotten. Repositories also prevent two team members from doing the
same or conflicting work. The repository will allow any member of the development team to have access to
the documents, and this makes it easier for them to implement the design.
Coding Standards
Coding standards will help you transform your UML diagrams into a set of uniformly coded components
that become the working application. Coding standards allow a component to be passed from one team
member to another for additional coding work, quality control, or assembly without the second developer
having to spend hours trying to understand the code. Each programmer will have his or her own artistic
style, which will influence their style of coding, but their style should not violate any of the team standards.
While some developers may view this as restricting, developers have to understand that they are working on
a team. As a team member you have to develop techniques of coding that allow the entire team to work
effectively and efficiently.
These will include the naming conventions used in your code, commenting of code, use of enumerated types
and constants, global constants, and any other methods of coding that will be done by all of the developers.
Frameworks
Frameworks should be chosen before the final code is written for a project and before the creation of UML
diagrams that are dependent upon the internal workings of the system.
Frameworks provide skeletal designs that can be built upon to create an organized
system where many components work together.
27
VB6 UML
A project design must work in accordance with these frameworks. As a large portion of the design is based
on these frameworks, the designer should thoroughly test them before actually committing to their use and
designing the project with them. Frameworks fall into one of the following two categories:
General Frameworks
These can be applied to whole projects and determine where components must be located and what tasks
each component must perform. In addition to these general frameworks, there are also smaller component
frameworks that come from some of the components that we use to build our project.
Component Frameworks
These components will be a fundamental part of our system and have rules associated with their use.
These rules must affect the way we design one or more other components of our
project to classify them as a framework.
As this is my own definition, I will discuss it in more detail. Both the ADO and MTS can be considered
frameworks by this definition, so we will use them as examples:
ADO can be used as a component of a project to give other components in the project the ability to
communicate with a data store
MTS can be used to manage server component transactions and to manage the creation and destruction
of these server components
ADO and MTS both have numerous rules that must be followed if they are to work properly. These rules
will place constraints on the design of the components. They will shape the way nearly every component of
the system is designed.
Like any component in an object-oriented project, a component framework will perform a whole set of
operations for us without us worrying about how these are done. This is encapsulation. I'm using the phrase
component framework because these components are more than just a simple piece that we plug into our
system. The frame of a house, which is also a component of the house, will determine the shape and
structure of the entire house. The components that our project is built on, the ones that will define and
shape the internal structure of our project, such as ADO or MTS, are the components that I will refer to as
frameworks. From this point onward, I will simply refer to these as frameworks of our system instead of
calling them component frameworks.
Choosing Frameworks
The choice of a framework should be made very carefully, as it can shape the inner workings of our project.
Programming technology is new and evolving every day. Just because the literature says a framework can
solve a certain set of problems, that doesn't necessarily mean it really can. Unless we actually test the
framework ourself and prove that it works under the set of conditions that we require it to, we cannot be
sure that it really works.
28
Project Design and Management
During the design phase, the majority of any quality control resources should involve the testing of various
frameworks and patterns. Having blind faith that something will work is not only foolish, but could cost
millions of dollars and months of time. I am saying this from personal experience.
One company spent over a year developing a middleware solution that completely depended
on a non-Microsoft DLL working under MTS. The first time the developers actually checked
to see if this DLL really worked under MTS was when the project was completed. The DLL
functioned fine as long as five or less users were accessing the component. When more users
tried to use the component, everything froze and came to a grinding halt. As this was
supposed to be an Intranet solution used by hundreds of users at the same time, this
certainly was not a workable solution. Four months of additional work resulted in a
rewriting of the project that finally allowed multiple users.
This company was fortunate as they were able to salvage most of the original work, but this
mistake could easily have resulted in starting from the very beginning. If this company had
spent two days writing a small test application to load test the DLL during the design phase,
this problem would have been detected and several months of work would have been saved.
I could tell you many other stories where entire projects were built based on a framework or technology that
simply would not work for a project, and was only discovered during the final phase of the project.
Remember that the majority of the frameworks are new and have only been tested in a limited number of
environments.
Patterns
Patterns are becoming the hottest new idea in OOP. There is good reason for this. A pattern can provide us
with a basic blueprint that we can use to design our project.
Patterns can allow us to build our components in an assembly line manner. Patterns help a project stay
within time deadlines, keep costs down and significantly reduce the amount of coding that needs to be done.
Without a pattern telling us how to build our components, we will find ourselves asking questions such as,
"Will we build our customer object for the client as a single class or do we build it as a class hierarchy
from one, two or perhaps three classes?" Having a well-defined pattern, which shows exactly how we
should build a component, provides us with the answers to questions like these.
29
VB6 UML
General Patterns
These apply to a general system and have nothing to do with choices of operating systems, programming
languages, or technologies. An example of this would be a general pattern for making an Order Entry
application. Every Order Entry application will have certain features regardless of what you use to build it.
This type of pattern is useful because it can guide you in a general design of your project. We won't be
concerning ourselves with general patterns in this book, because I want us to drill right down to some in-
depth UML analysis and build from zero up.
Specific Patterns
These can be used to build a particular component of a system, and they are dependent upon the system that
we're using. For example, if I were building a three-tier project, I might need to build dozens of client
components. It would be really helpful if I could find a pattern to build this type of component. I could then
reuse this pattern over and over again, to design and build all of my client components.
UML diagrams will not only provide us with the patterns that exist within our project's components, but
also show us how to code these objects. If we are fortunate, there will already be a coding solution for the
patterns we find in our UML diagrams. If there isn't already a pattern, we will need to create activity
diagrams in order to map out the coding solution.
Unfortunately, you see very little discussion of patterns in the Visual Basic literature, so there are very few
patterns for Visual Basic components. Visual Basic has only recently moved toward being object-oriented,
and I believe that many developers are still trying to write OO programs using non-OOP techniques. As the
Visual Basic community moves more toward using OOP techniques, I think they will also be jumping onto
the pattern bandwagon.
There are many resources that can guide us in finding these patterns, but for the most part we will end up
designing the patterns for the individual components ourselves. This is especially true because many of the
frameworks are so new that there are often very few, if any, patterns created for the framework's
components. In addition, since these frameworks are new, they usually have not been thoroughly tested
either.
This means that before we use any framework we will first have to create our own patterns and thoroughly
test them. While this may sound difficult, it really is not.
Throughout this book I will show you that these patterns can be found quite easily if we design our
components with UML models. The patterns we will use to design our project will be based on object-
oriented principles, good coding techniques, and sound project management.
30
Project Design and Management
If we are using the same internal system for all of our projects, then once we've designed and built the
internal components for the first time, we can reuse them for all of our projects based on this internal
system. By the same reasoning, once we've designed and coded our Enterprise patterns, in order to build
future Enterprise projects, we only need to be concerned with the user requirements and building the
external part of our project; the patterns will provide everything else.
If we take all of these patterns and put them together, we can build what can be considered our own Visual
Basic 6 Enterprise framework. This framework will have a set of rules that must be followed, and will
shape how we build the rest of our project, i.e. the external part built from the user requirements (and use
cases).
Creating entire frameworks to build a particular type of project is perhaps one of the most
efficient ways of building projects. Many companies and third-party software vendors are
working late hours trying to make frameworks like this for Visual Basic 6 Enterprise
projects.
We can think of this as building prefabricated houses. Every prefabricated house is built from a similar
structure, a basic framework that will underlie every house. This doesn't mean that the houses will all look
identical. Once we move beyond the basic framework, each house will have certain things added to make it
unique: landscaping, color arrangement, bathroom fixtures, carpeting, etc. While the framework of each
house is identical, we can place different things within this framework to make completely different houses.
When it comes to our Visual Basic projects, the idea is to make "prefabricated" projects. We make our basic
framework and then, based on the users' requirements and the needs of the particular project, we will add
the features to make the application unique.
Building the framework is usually the most difficult part of a project. From design to implementation, it can
take six months to a year. However, once these frameworks have been established, we don't have to worry
about things like making connections to databases. As long as we follow the rules of our Visual Basic
Enterprise framework, the framework will handle all of these things. We only need to be concerned with
adding features that are required by our project.
IES Incorporated
IES Inc. is a development company. They have been contacted by the Northwind Company to build an
Order Entry application for them. In addition to Order Entry, this project will also provide reports for
management.
31
VB6 UML
Northwind's Requirements
After some discussion, the main requirements for the Northwind project were found to be as follows:
Internet/Intranet
This requirement can be achieved by having a client that does not have a continuous connection to the
database. This means that state has to be stored on the client, and the server components have to be
stateless.
Data Access
The choices for data access with Visual Basic include DAO, ODBC Direct, RDO, and ADO. Let's consider
the candidate technologies in turn, weighing up the pros and cons to make a decision on which technologies
we'll use for the Northwind project.
32
Project Design and Management
DAO (Data Access Objects) has been around for several years now, but works poorly with remote data
sources. Although DAO allows us to connect and disconnect from a data store as many times as we like,
it does not support disconnected recordsets.
Disconnected recordsets are well suited to Internet requirements (we discuss these
technologies in much more detail in Chapter 8).
If we were using DAO, and we wanted to update our data store regularly, we would really want to
establish continuous connection to our data store. But a continuous connection to the data store will not
work for us – given the details stipulated in the previous Internet/intranet requirements. DAO is therefore
not suitable for this particular project.
ODBC Direct requires a continuous connection in the same way that DAO would have done; so ODBC
Direct will not work for our project.
RDO (Remote Data Objects) is a thin layer on top of ODBC, and inherits from ODBC the ability to
create client cursors - which allows for disconnected recordsets. While RDO will work for our project,
its use is limited to relational databases using ODBC. As we are not sure what type of database
Northwind is going to upgrade to, the choice of RDO will limit the choices of the new database to
relational databases with ODBC drivers.
ADO (ActiveX Data Objects) is the primary method of database connection that Microsoft is currently
supporting, and it's the technology that Microsoft is focusing a great deal of development upon. ADO has
a wide range of features that allow you to get extensive information from disconnected recordsets,
including the ability to reconcile inconsistencies in the data. ADO has a rich set of features specifically
designed for building a project that maintains state on the client and has no state on the server. ADO also
offers an object model that's fairly simple to use and can work with both relational and non-relational
databases. Using ADO will allow the user to upgrade to a very wide range of databases. Overall, ADO
would be a better choice than RDO for this project. ADO is therefore our chosen method for database
connection in this project.
Another technology we have chosen is Remote Data Services (RDS). RDS does offer some potential
advantages over other methods of connecting to remote objects, such as DCOM. DCOM relies on the
operating system to handle security, and this requires a great deal of work setting up the components.
DCOM also has some security issues when running through a proxy server that assigns IP addresses to the
internal computers. RDS places the burden of security on the component, which eliminates the difficulties
of setting the component up for use on both the client and server. This is a great advantage, and makes RDS
a better choice than DCOM for this project.
When it comes to building the GUI, Visual Basic offers a clear advantage over C++ and Java. The choice of
Visual Basic, rather than another GUI building programming language, would probably be based on the
following factors:
33
VB6 UML
Visual Basic is a very popular programming language. Let's say the developers are familiar with Visual
Basic. With this being the case, Visual Basic can easily fulfill every requirement listed above. Visual Basic
has a rich set of features that allows one to build powerful applications.
While Visual Basic is a great choice for the GUI, what about the rest of the application? Both C++ and Java
can build powerful components, but they both require a longer time to develop. Java does not offer any
major performance gains over a Visual Basic project. As this system will be Windows based on the client
and server, there is no need for Java's ability to work on multiple platforms. Java offers no major advantage
over Visual Basic, and will take longer to build - so we will eliminate it as an option for this project,
especially if the developers are not familiar with the language.
C++ might produce faster components, but is speed an important factor for our project? There will be at
most a few dozen Order Entry Clerks, so the internal system will never be too heavily stressed. As for the
future Internet applications, the few milliseconds gained by using C++ will make little difference in the
response time of the Northwind web site. In a site with tens of thousands of users per day, we may want to
get into a discussion of the benefits gained by programming in C++. In a site that is likely to get a few
hundred to perhaps a few thousand hits per day, a Visual Basic component, running under MTS, can handle
the stress.
Three-tier Framework
The three-tier framework we discuss in this chapter can be considered to be a general blueprint mapping of
where our components must be located and what tasks each component should perform.
The three-tier framework will define where your components go and what tasks they must perform. A three-
tier project has the data in one location, the database-related functionality in another location, and the user
interface and non-database related functionality in a third place:
Each of these three pieces has a well-defined location and set of tasks they must perform. The general
framework is more concerned with defining what different parts of the system will do, and where they will
do it, rather than how they will do it.
A three-tiered architecture is the primary framework we will be working with. This framework will actually
have several other frameworks within it, such as the ADO framework, and the Microsoft Transaction Server
(MTS) framework.
34
Project Design and Management
Often there are many ways we can build a component using a framework. Some solutions will work very
well for our system, and others will not work at all. For example, where is the best place to put the middle-
tier component? Do we put it on its own server, or do we put it on the same server as the database? Either
choice can result in a drastically different overall performance of our entire system.
Here is a fundamental truth for every Visual Basic programmer involved with this
kind of development.
There is no one solution that will work for every three-tier project.
Each project will have its own arrangement of components using frameworks in a
way that results in the best performance for the system. And every business problem
has its own unique rules that will affect how best to build a three-tier solution for it.
Working through a test project is the best way to find the most suitable techniques
for using a framework in your project.
A test project will help us find the most efficient way to use a framework for a given project. Based on our
tests and the information we gather, we can create an accurate technology model (a model of all the
technology, such as the ADO or MTS that will be used within the system). This technology model,
combined with the UML models, can help us create the most efficient solution for the project's design.
It should be an example that you, the reader, could use in real projects and could solve some of your
current programming needs.
It should be reusable in many different types of projects.
It should be built from a pattern, so that you can build similar client components from this pattern with
very little modification.
It should provide data controls (such as a grid control or a textbox) since Visual Basic is primarily a GUI
language and it's important to link the GUI to the client objects.
It should provide information through standard properties.
It should be built from code that is fairly simple.
Using UML patterns that we'll design through the first half of this book, we'll be able to carry on through
and use our UML diagrams to build a Visual Basic code template for the client components.
We'll be coding these components in the second half of the book, and our Visual Basic code template will
be used to build most of the client components. You'll see, as we progress from design to development, that
these code templates allow us to build many similar components from one template quickly and efficiently.
Entire portions of our project can be created from a few code templates.
35
VB6 UML
As you should be able to see, the process is iterative and incremental. It's iterative in that we revisit
several stages again and again; and it's incremental since with each iteration we advance the project
slightly. You'll see how this approach can lead to obvious benefits and help reduce errors and bugs shortly.
User Model: This model shows the user requirements, and how these requirements will affect the design
of the project.
Business Model: This model shows the business requirements of building the project. Included in this
model is the cost of the project, the time it will take to build the project, the platforms the project must
support, the cost savings, the profit to be earned through this project, and so on.
The Development Model: This model consists of two components. One component is concerned with
the development of the process model, and details issues such as planning, deliverables, and scheduling.
The second component covers the roles of the team members and concentrates on the management of
teams.
Physical Model: A model showing all of the hardware components of the system and how they fit in
with the other models.
Logical Model: A model of the components of the system and the rules that will govern these
components.
Technology Model: A model providing all of the information required to make the choices on the
technology used for the system. A technology model may include an analysis of DAO, ADO and RDO
to determine what is the best method for connecting to the database.
Although we won't be explicitly following the Enterprise Application Model, I've introduced it here because
it is related to the way I've structured this book. So let me explain how each sub-model of the Enterprise
Application Model is realized in this book.
36
Project Design and Management
A list of the different types of technology that can be used for this project
The advantages and disadvantages of each of the technologies
A detailed description of the technology
Rules associated with using this technology
As I mentioned previously, we'll be going through many of these phases throughout the book.
However, I won't refer directly to this model again: we'll be concentrating on the UML
Process itself, as that's the focus of this book. But if you meet the Enterprise Application
Model again in your travels, you'll at least know how it links into what we're doing in this
book.
Project Management
Following the methods of good project management has become critical for successful projects. This is
especially true for Visual Basic, now that it is evolving into an object-oriented language. Projects that get
bogged down in the later stages, that require long periods of debugging, and that miss deadlines are usually
the result of poor project management, and are not failures of Visual Basic.
37
VB6 UML
Proper project management is essential. However, this book is more concerned with UML,
and as a result, you may find that we either skip or barely touch on some of the concepts
outlined below. Nevertheless, you should apply them with the utmost vigilance in your VB
projects.
Project Design
Project Implementation
The rest of this chapter will drill down to a detailed discussion on Project Design and Project
Implementation from the perspective of good project management. Here is a precise map of the topics we'll
cover in the rest of this chapter:
38
Project Design and Management
These are all undertakings that will continue as the project develops, particularly as this is most commonly
an iterative process. However, these tasks can be distinguished from actual project implementation
considerations, since they should be started prior to any code being written.
If documentation is not undertaken, the design of the project will be more fluid, allowing the users to add or
remove items from the project throughout the project's lifetime. Although this may initially seem beneficial,
it can result in feature creep, where the users, project managers, and every other person involved in the
project keep adding features. This may persist up to the point where the project is nearly complete, and at
that point the addition of more features can result in the dramatic rewriting major sections of the
application. Obviously, this would not be good - so we must document early to avoid feature creep.
Thus, if the application is doing something it is not supposed to do, or not doing something it should, the
user should be able to catch this mistake. Showing users documentation and prototypes, as discussed below,
should allow any changes to the requirements to be found before the application is actually built. Without a
design document, features that are missing, improperly functioning or unnecessary are not likely to be
discovered until the first releases.
39
VB6 UML
Fixing any errors found during a review of the design document (and user interface prototypes) requires
simply rewriting the design document and making the necessary changes to the project's models. In the
worse case scenario, this will result in only throwing away a few days or weeks of design work as opposed
to much, much more application development time.
To correct mistakes found once the application is partially built requires the code to be re-written or thrown
away and replaced by the correct code. This usually takes a great deal of time and consequently money.
Planning and design allow you to find problems early on when they are easy to fix. Poor or insufficient
planning results in mistakes being caught or not caught later (anywhere from the beginning of coding to
after the final release) when they are difficult and expensive to fix.
If major corrections are made to a project when the application is partially built, the project should be
completely re-tested to ensure that the new changes are both working correctly and have been properly
implemented. Unfortunately, the changes usually result in a missed deadline and the project may be
released without thorough re-testing. The end result is an application that will almost certainly have serious
bugs.
The earlier an error is caught the easier it is to fix. The further along a project is
the harder and more expensive in time and money it will be to make a change.
The GUI prototype can be used to review the interface requirements. When the users work with the
prototype, they can ensure that the interface is easy to use and can determine whether the application is
really doing everything they require. A review of a GUI prototype combined with a review of the design
documentation allows the user to inspect the application design via two different techniques, one visual and
one written. Using two methods helps to eliminate the possibility of missing a mistake and results in a
design that is exactly what the users need.
If this review process has not taken place, the interface may need to be completely redesigned once the
application is complete. If the project is written correctly, which for most projects means components, this
may only result in a minor revision of the GUI and small changes within the code. However, if the project
is not compartmentalized, then it is likely that this could result in major re-coding.
Documentation of Risks
Risks are anything that can negatively affect the project. They include problems with design, personnel,
technology, etc. Any technology or programming technique that has not previously been used by the
development team should be considered a risk.
Just because the documentation says something should work, that does not mean that it will!
40
Project Design and Management
These technologies and techniques can be tested with small test applications during the design phase to
ensure that everything will work prior to the code being written. Just as design documentation and
prototypes help to make sure that the project will work externally, small test applications can be used to
check that the project will work internally prior to actually building the project. We shall therefore
consider the following project management techniques for dealing with risks:
Categorizing Risks
Risk Assessment
Personnel Issues
Categorizing Risks
Any changes in specifications, especially when made late in the project's development, can be a major risk
factor to every aspect of the project. Changes made after the coding begins can be assessed by how greatly
they will affect the project, how major the changes will be, and how they will affect the overall ability to
complete the project on time. Changes can be categorized as follows:
As a guideline, changes falling into the first category should almost always be made. Those in the second
category should only be made after careful consideration and research. Those falling into the third depend
upon the nature of the change, and can be implemented after some consideration. The last category should
almost never be implemented.
A risk document can contain sections for each risk. Each section lists what is being done to reduce or
eliminate the risk, and an estimate of the financial and time effects of the risk. Each solution can be
documented, including the effects of each solution on project costs and project schedule. There should also
be a section to note the date when the risk is eliminated.
Risk Assessment
A risk document allows both the development team and the users to review each and every identified risk.
Allowing all of the developers to place possible risks into the risks document makes certain that those who
are writing the code are most likely to see the problems and take appropriate measures.
Users and the developers requesting a change to the project should sign off any change that will have a
major effect on the project. Doing this will usually result in any unnecessary changes being dropped as no
one wants to sign off on something non-critical that will force a major delay of the project. This will also
force users to carefully consider all requirements. If the risk of adding new features is not evaluated, the
changes may take a long time to implement or may prevent the project from ever being completed.
When risks are not written down, they tend to be ignored, overlooked, or forgotten. In addition, if there is
no system for managing risks then there is no standard way for handling them - and solutions become
haphazard. Without risk assessment and prior testing of a new technology, the discovery that the technology
will not work for a project usually occurs during final testing of the completed product. This can result in
the entire project being redesigned and rewritten, with a resultant loss of a great deal of work and money.
41
VB6 UML
Personnel Issues
Personnel shortage issues should also be addressed in the risk document. Evaluating the risks from
personnel shortages and compensating for them, by adjusting schedules and workloads so that no one is
overburdened, will not only help keep the project running smoothly, but also reduce the risk of losing
developers. If personnel shortages are not addressed, unrealistic demands may be made on the developers.
This in turn leads to poor morale and often the churning out of code without careful design.
Maintaining a Library
A library allows everything to be clearly documented. As the project evolves through the design phase, each
change is documented and can later be retrieved for review. During the implementation phase, each version
of the project will also be saved in the library. The key benefits of maintaining a library are therefore as
follows:
A library records all aspects of a project. If one of the original developers leaves the company, their
knowledge of the project does not leave with them. It is still in the library. This information can be used
during revisions or on future projects to determine the best techniques and methods for creating this type of
project. Without a library, all information on the project is sitting within the heads of the people working on
the project or in scattered documents.
42
Project Design and Management
This means that each stage ends with the application working, fully tested, and
capable of performing a certain part of the complete project.
The key to building incrementally is building the project from separate, well-defined components. These
components are like bricks that can be used to build our project. If one component fails or does not perform
properly, only that component needs to be replaced; the rest of the project is unaffected.
Each stage will go through a full cycle of building the components required for the stage using detailed
design documents, testing the components, and testing the entire project as each component is added. The
different stages do not have to follow one after another. Different stages can be occurring at the same time
by different developers.
Building applications in increments does take extra work up front because the project will have to be
carefully planned and designed. However, building an application incrementally offers many advantages:
When a project does not produce any functioning components until the final phases of the application, it is
difficult to determine how much is being accomplished and difficult to monitor what stage the project has
reached. Often the only way to estimate the progress of the project is to ask the opinion of developers.
Team managers will rely on statements such as, "We are 80% of the way done with this particular module".
Unfortunately this number is not based on any measurable test of completeness, and can therefore result in
missed deadlines. Incremental development, on the other hand, allows project managers to at least monitor
how many components have been completed.
43
VB6 UML
Smaller incremental projects will also benefit the developers. Short-term accomplishments are easier to
envision and to meet. They help to keep developers focused on the completion of a set of features within a
small amount of time, instead of something way off in the future. Human beings work best when they have
very clear, identifiable goals and a reasonable time to accomplish tasks. Churning out code for long periods
of time, without actually producing anything workable, becomes very tedious. More importantly,
completing a task one or two years down the road is very difficult to work toward.
The initial stages of a project could complete the most critical parts of the application. When these parts are
finished, the application can be given to the user. As later stages are completed, the user can be given
upgrades, which contain more functionality. This means that a user could have a partially working
application that performs all of the essential functionality of the project within a short time as opposed to
having to wait a long time for the finished product.
When a project is not developed as a set of incremental components, the application can only be tested as a
whole. If there are problems at this final stage, they will take much longer to track down and fix than if they
were identified within a specific component.
Even when developing a project with components, however, there can be serious problems if you do not
build the project in increments. If a component functions perfectly on its own, it may still produce errors
when placed within the project. Until each component is tested separately and within the project, we cannot
say it is complete.
Development Flexibility
If the application is going to be used in many different environments, building a project with components
means that these components can be reused in these different environments. Without skillful project
development, it may be necessary to build completely different applications for each environment. This
requires a great deal of extra time and money, and results in building several applications with identical
functionality.
44
Project Design and Management
Testing and debugging go hand in hand. Both should be performed in a careful, planned manner that not
only gives the highest probability of finding problems, but also of finding where the problems are
occurring. Carefully designed charts and procedures should be created for testing and debugging.
If any changes are made to the project, everything should be re-tested. Carefully document, inside and
outside of the code, all of the bugs that were found, when they were found, who fixed them, and how and
when they were fixed. A haphazard method of debugging, i.e. just stepping through the code and making
random changes to suspect areas, is like driving a bulldozer through your code.
We will now discuss the following project management issues that are relevant when the team are testing
and debugging code:
Error Management
Error Location
Error Management
Documentation ensures that bugs are actually fixed. Placing documentation within the code allows you to
see where changes are made in case the changes result in further problems. One way of doing this is to
place module and function level headers in the project code which detail the description, the parameters
passed, the parameters returned (or changed), and a local revision history of the changes made to each
function.
Without this documentation, developers are left with no way of tracking which repairs were
made, or any way to change the repairs should they create further problems.
A testing phase that is not coherent and clear can result in errors being missed by the developer and found
later by the user. I acknowledge that even with the best testing and design, a few bugs may still get through:
but a poor testing phase will result in a complete infestation rather than one or two small bugs.
Documentation will also allow the project team to review every error once the project is complete - which
can help prevent them from being made again in future projects.
Error Location
Testing a component by itself outside of the entire project can identify bugs located within that component.
In this way, we can limit the possible bug sources to the component itself (private methods and properties),
which makes them easier to track down.
Once the component is found to be bug free, any bugs found when the component is placed within the entire
project are probably related to how the component communicates to the rest of the project (public
properties and methods). This allows both the public and private methods and properties to be fully tested.
Testing an application only when it is complete or after several components have been put together, makes
it very difficult to find which component is causing the problem. The loss in time is enormous, but more
importantly, the project manager may have to ask his developers to spend many frustrating days tracking
the problem down.
45
VB6 UML
Failure to test the entire project after each change or addition can result in bugs hiding in unsuspecting
places. Always remember that making any changes to an application may affect parts of the project we
would not expect to be affected by the change.
Visual Basic objects (which include your own classes, textboxes, ADO, etc.) can be considered as
individual components. These objects can be put together to build larger objects, such as forms and ActiveX
Controls, which will then be assembled into the final project. Using object-oriented programming in Visual
Basic is therefore the best way to create projects using components. In other words, building projects
incrementally using components, and the whole OOP strategy, go hand-in-hand.
Visual Basic programmers must finally break away from the pre-Visual Basic 4, 32-bit
applications which were, in their best implementations, modular but still monolithic
procedural applications. While Visual Basic's syntax may still be Basic style, its projects
and coding can be object-oriented.
The following considerations will arise from any careful approach to component design:
Making one change may result in many parts of the application being affected in an unpredictable manner.
Poorly designed objects cannot be pulled out of the project and tested by themselves. The result is that
making a code change could require the project team to test everything in the project - just to see if
something else has been affected. And if something else does break, another change is necessary… and the
whole process soon becomes unmanageable.
Testing becomes a complex tangle of trying to debug an entire project at once and tracing back and forth
through thousands of lines of code. Not a great situation for any project to be in.
46
Project Design and Management
When we compile a Visual Basic ActiveX project, it assigns several unique numbers called GUIDs
(Globally Unique Identifiers) to the component; these are stored in the system registry. If the component is
further developed with changes to its public interface, then subsequent compilations of these projects will
result in new GUIDs being assigned. Any computer that registered the original version will have to have
that registry information updated, often manually, using the regsvr32 program.
When we build a component and distribute it, we are making a promise that we will never, ever change its
interface at any time. We may add methods and properties (which fits perfectly with building our project in
increments), but changes to the existing interface must never happen. Breaking this promise means we have
to pay the price, i.e. a whole lot of cleaning up.
The only way to prevent this from happening is for the project manager to insist upon the project team
carefully planning their objects to ensure that they are building them correctly from the first release.
It is true that we should be able to leave the old registry entries in the registry without
causing any harm. In my experience, though, I have found that this can cause your
component not to run properly; so I would always clean up dead entries.
Summary
Using the techniques outlined in this chapter, project managers will be able to guide their teams towards
preventing cost and time overruns, poor morale, poor code, and projects that do not do what they are
supposed to do. These basic techniques provide security, giving the users and developers a clear, safe way
of managing a project on time and within budget.
Now that we have an understanding of basic project management practices, we are all set to start our actual
design work in UML. The first step is to determine the project requirements, so that's what we'll look at in
the next chapter, as we conduct some interviews and build some use cases.
47
Requirements Development
Before we can design a project, we must first find out exactly what our project is supposed to do when it's
finished. Of course, it's also just as important to know what the project is not supposed to do when we're
through. So here's a clear rule: for projects we're working on that have users, we should center our external
design upon the requirements of those users.
It's possible to determine the requirements of the project by interviewing the users and finding out how they
are going to use the system, what their needs are, and how they expect the system to work. There will be
different types of users, of course, and we should meet with at least one representative - preferably more -
from every user type.
In the language of UML, a type of user is called an actor, and a written model of the way that the actor uses
different parts of the system is called a use case.
This chapter concentrates on the UML model that is most likely to be the starting point of any project: the
use case. Here's how we'll explore use cases in this chapter:
We start with a clear explanation of the possible relationships that can exist between objects. This
concept of relationships between objects is fundamental to a full understanding of use cases.
We then examine how to identify the potential users of a project, and the best way to determine the
requirements of these users. We learn how to convert this information into a use case. Along the way, I'll
present some examples of creating use cases that are relevant to our project.
Once we've established our use cases we can go on to consider how they are related to one another and
to the users of the system. This information is represented in a use case diagram.
Finally, I'll demonstrate how we can identify some of the project’s objects from our use cases.
This chapter will therefore provide a clear step-by-step guide to creating use cases, use case diagrams, and
identifying the user-determined objects of the system.
VB6 UML
Before we start looking at actors and use cases, however, we need to understand those relationships
between objects that I mentioned a moment ago.
Relationships
Objects are things. A relationship is a connection among things. In the real world, we have relationships
that connect things together. For instance, an employee works for a company. The relationship between the
employee and the company is works for. A car has an engine, a steering wheel, and four wheels. The
relationship between the car and its engine is a …has a… relationship.
Dependency
A dependency relationship indicates that one object knows another. Changes to the latter object (the
known object) affect the former object (the knowing object). In Visual Basic, adding a reference to a
component to a project establishes a dependency. That is, the project depends on the component.
Dependency is usually a one-way relationship. The fact that a project depends on a component does not
necessarily mean that the component depends on the project.
In UML, a dependency is represented by a dashed directed line from the dependent object to the depended
object. The diagram below shows a dependency relationship between a Visual Basic project and the
ActiveX Data Object (ADO) library:
In the above dependency, any changes made to the ActiveX Data Object also change the project. If the
project is originally built using ADO 1.5 and it is now using ADO 2.0, you might need to change a certain
portion of code so that it will take advantage of new features offered by ADO 2.0. When we distribute the
project, the ADO 2.0 library must be distributed.
Association
An association indicates that an object's state depends on another object. To understand the state of the
dependent object, we must understand its relationship with the object it depends upon. For instance, let's
say it's a fact that someone called David works for Wrox Press; but, if we didn't actually know anything
about what David did for a living, we would need to know whom David works for before we could
understand what David does when he's working. David's state depends on another object: the company he
works for.
50
Requirements Development
An association usually connects two objects, and in that case we say that it is a binary association. A less
common type of association is an n-ary association, where more than two objects are connected.
In UML, an association is represented by a solid line connecting two or more objects, as illustrated here:
Orders from
Customer Company
You can also add an arrow indicating the direction of the association, if you think it makes
the diagram clearer. You'll notice that the accompanying text "Order from" in the diagram
above makes the direction of the relationship fairly clear however.
It is often desirable to know how many instances of a class are allowed in an association. For instance, an
order can have one or more order lines. The allowed or required number of instances is called the
multiplicity of the association. In UML, a multiplicity of exactly one is shown as 1, and a multiplicity of
one or more is shown as 1..*. This representation is illustrated below:
Has
Order OrderDetails
1 1..*
This figure says that for each Order, there must be one or more OrderDetails lines. If your company has
decided that you can create an order without any order line, this multiplicity becomes zero or more,
represented as 0..*. You may also specify other multiplicity condition such as zero (0), zero or one (0..1), or
m to n (m..n).
A normal association between two classes represents a peer-to-peer relationship. That is, both classes are of
equal importance. However, classes are sometimes related to each other in a way that one class is a part of
another class. Therefore, they form a whole/part relationship. For instance, an order line is a part of an
order.
Two special cases of associations, namely aggregation and composite aggregation, are used to model such
relationships. Let's take a look at aggregation and composite aggregation now.
Aggregation
An aggregation is represented in UML as an association with a diamond attached to the "whole" class. An
Order/Customer relationship is illustrated here:
Has a
Order Customer
0..1 1
51
VB6 UML
This diagram specifies that an Order must have a Customer, hence the multiplicity of 1 on the Customer
end. The zero or one multiplicity (0..1) of the Order class, on the other hand, specifies that a Customer
can exist without any order attached to it. Although a Customer is a part of an order, its life does not
depend on the Order. If an Order is deleted, the Customer record may still remain in your system.
In other instances, the life span of a Part object in a Whole/Part relationship depends on the Whole object.
For example, when a car is destroyed, its steering wheel will also be destroyed. Of course, you may argue
that we may be able to reuse the steering wheel; I personally would not keep my steering wheel though.
This type of relationship is called a composite aggregation.
Composite Aggregation
A perfect example of a composite aggregation is the Order/Line association we used earlier. When an
Order is deleted, all OrderDetails lines are gone with it. A composite aggregation is represented in UML
as an aggregation with a solid diamond:
Has
Order Order Details
1 1..*
We can show the relationships among the Order class, the OrderDetails class, and the Customer class in
one diagram:
Customer
1
Has a
0..1
Order
1
Has
OrderDetails
1..*
This diagram simply merges the two previous diagrams we've been developing into a more concise visual
statement about the relationship between our Order class and the Customer and OrderDetails class.
52
Requirements Development
Generalization
A generalization is a relationship between a general class and a specific type of it. For instance, a
Customer class may represent all our customers while a GoodCustomer class may represent customers
who have good credit records. In this case, the GoodCustomer class is said to be a specialization of the
generic customer class.
A generalization represents an is-a-kind-of relationship. That is, a good customer is a kind of customer.
In UML, a generalization is represented as a solid line with a triangle pointing to the generic class, as
shown below:
Customer
GoodCustomer CODCustomer
VIPCustomer
In this diagram, the GoodCustomer class represents customers with good credit record. The
CODCustomer class represents customers who must pay when the goods are delivered (Cash On
Delivery). They are both special kinds of the generic customer class. The VIPCustomer class further
represents all good customers who spend $10,000 or more every month.
One of the most notable shortcomings of Visual Basic 6 is the lack of inheritance
implementation. There is no way to model generalization in Visual Basic. For this reason,
we will not discuss generalization further in this book.
We will now move on to consider actors and use cases in more detail.
53
VB6 UML
Actors
Actors do not necessarily have to be people. They can also be other external systems calling your system.
The system includes all of the components within our project. These components communicate with each
other by passing messages back and forth. In the case of an n-tier framework, the system is composed of
everything that makes up the client, business and data tiers.
Actors are not part of the system. Actors are external to your system. They are
something that must interact with the system.
An actor can be a person, such as an order entry clerk. For instance, if our application were using an online
credit card checking system that was external to your project, this would be an example of another system
that could be considered an actor.
An actor can:
Identifying Actors
An initial interview with someone in management can give us an overview of the system and the people
involved with the system. From this interview it should be possible to generate a list of possible actors.
After talking with these people, we might find that certain people actually play several different roles and
are actually several different actors. On the other hand, we might find that several users may only be one
actor. Defining an actor is not always easy.
Remember that actors do not represent every single person using the system. Actors
represent a particular type of person that interacts with the system.
For example, in the order entry application, our actors would include user types such as the order entry
clerk, the manager, the person who is in charge of products, etc.
We must also be careful not to get too specific with our actors. For example, we could have two people
taking orders, one by phone and another who is taking orders through the mail. In this case, we would not
make two roles for this if the two people were interacting with the system in the same way. As far as the
system is concerned, if they both are entering orders the same way, then they are the same actor.
54
Requirements Development
We can ask these questions, and other related ones, during our interviews with users of the system we're
designing and developing.
Manages
The directed line in the above figure represents a unidirectional association between the Sales Manager
actor and the Sales Representative actor. That is, a Sales Manager manages sales representatives. Most
UML tools allow you to enter specifications of actors so that you can document everything related to the
actors.
Now we can represent actors, classes and relationships, let's move straight on to some interviews so we can
begin to put into action what we've learned so far.
Interviewing Users
Once we have a list of potential actors for our system, we will want to interview at least one person from
each group.
When interviewing a user, here are some of the key questions you want answered:
What roles does the user play and what does the user do in each of these roles?
Does the user play more than one role in the system?
Does the user create, edit, delete, or save any information in the system?
What type of information does the user require to perform their task?
What are possible things that the user can do that are incorrect tasks?
These questions are not necessarily questions that you are going to ask the user directly. Instead, these are
questions that should be answered by the end of the interview – but that you may want to broach indirectly.
The best way I have found to do this is to work around the questions, leading the conversation towards the
topics themselves. Why? Because your interviewees may clam up if you ask them directly… or even worse,
they may give you idealized answers based more on what they think should happen rather than what
actually does happen!
55
VB6 UML
Our list of potential actors include the Sales Representative, the Sales Manager, the Vice President of
Sales and the Inside Sales Coordinator. Over the next few pages we will be interviewing these users and
recording their requirements in use cases.
What follows is an interview with Valerie, Northwind’s order entry clerk, who has been
entering orders in the application that Northwind wants to replace. She will be describing
what she does with the current system. Externally, the new system will perform similar tasks,
though it may do it in a very different way. Internally, the new application will be completely
different, which is why the old application is being replaced instead of being rewritten.
Valerie: Basically I'm an Order Entry Clerk - so I enter the new orders. We've got a computer system,
which is supposed to make things easier. It's like a database that I can get information from and add things
to. Everything I need should be in the database. A customer calls in with an order and I take their details.
Then I take their order – which tells us how many of our products they want.
Interviewer: So could you tell me what customer details you need and why?
Valerie: Well, first I need their company name and address so that I can tell whether they're a new or an
old customer. We have a list of customers to check from. Each customer has an ID number but I'm not
allowed to change that. We have different customers with the same name so that's why we need their
address. Also, if we have a new customer call in, I need to set up a new account for them. At this point I
also check that the details on our list are correct - maybe the customer's moved.
Valerie: Well, then I just take the details of their order, what they want from our catalog and how much.
The products are stored in the database too. I confirm these details and the computer works out how much
it's all going to cost and then I make sure the customer is happy with the price. Of course, I can't give them
a deal or anything, but sometimes they haven't realized how much their order is going to come to. So
anyway once the order is taken, I save it to the database and wait for the next one.
Interviewer: Right, so could you tell me how the customer receives his goods?
Valerie: Oh yes, sorry, I have to do that too. From another list I select a suitable shipper depending on the
company's address, and add the cost of shipping to the cost of their goods and then check if the price is OK.
Interviewer: So adding new customers is one of your responsibilities. Could you tell me how you go about
setting up an account for a new customer?
56
Requirements Development
Valerie: Yes, that is one of my responsibilities. I'm supposed to be adding new customers or changing a
customer's details. To check if a customer is new, I have to search through our list. If the customer is new I
take the customer's company name and address, and add it to our list on the database. The computer then
generates the customer ID. Also, a customer may call up and say that their address has changed, and so one
of my responsibilities will be changing the customer's address. I have to get the customer information from
the database and modify it. All this can take some time because we have a lot of of customers. What I
would really like is an easier way to check customers – like if I just typed in the company name and it knew
if the customer existed or not. Or even if it gave me a list of customers with that name straight away rather
than me having to find it myself.
Interviewer: Are there any possible errors that can occur while you are doing this?
Valerie: Yeah, if someone else is changing a customer record, I won't be able to modify it until they are
done. One thing that is really annoying is that each field can only be a certain length, but the program will
allow you to type in as many letters as you want. If you type too much, the program will just chop off the
extra letters.
Valerie: Umm, here. I've written it down. The company name can be 40 letters; the contact name, 30
letters; the contact title, 30; the address, 60; city, 15; region, 15; postal code, 10; country, 15; and the phone
and fax, 24 digits. I have to remember these.
Interviewer: Do you have any other responsibilities besides taking orders? Can you edit orders or change
orders or do things like that?
Valerie: No, I can't change an order. Once I've entered an order, if there's an error in the order, the Sales
Manager has to take care of it and he is responsible for modifying the orders.
Valerie: No, I don't have anything to do with them, the inside sales person handles that.
Valerie: Yeah, sometimes when we are shorthanded or really busy, the Sales Manager will step in and take
orders.
Interviewer: Can you think of anything else that you like or dislike about your current system?
Valerie: Well we have a lot of products, and different products clearly belong to different categories.
Sometimes it takes me a long time to find the right product because they are all in one long list. I could find
each product much more easily if they were split up into the different categories.
57
VB6 UML
Once we have gone back and spoken to Valerie about any aspects of the interview that were confusing, we
can create a summary of the interview so that the information is much clearer and broken down into tasks
that we believe represent her requirements for the new system.
The summary for each task will form the basis of a use case.
58
Requirements Development
The actor makes a request to create a new customer or the actor can type in the name and the system will
automatically allow her to add a new customer (if there are no existing customers with this name)
The actor enters the information on the customer
The actor verifies that the customer information is correct
The actor saves the new customer information
Other Information
A customer consists of the following fields and field sizes:
Company Name: 40 characters
Contact Name: 30 characters
Contact Title: 30 characters
Address: 60 characters
City: 15 characters
Region: 15 characters
Postal Code: 10 characters
Country: 15 characters
Phone: 24 characters
Fax: 24 characters
The system assigns the customer and order IDs
Customer and order IDs can not be edited
When the interview is complete, the user can look over the use case and verify that all of the information is
complete and correct. Use cases and the use case diagrams are not only the starting point for all of our other
UML diagrams, they are also a very easy to understand, user-friendly way of mapping out the system's
requirements. Use case diagrams, which are diagrams showing the relationships between use cases and
actors, provide something that any user should be able to understand and be able to use to verify if the
information is correct.
59
VB6 UML
Every use case must have a starting point, an ending point (which includes canceling
the task), and a measurable result.
From the interview summary we've conducted, we can see that there are several use cases here. The use
cases we can create from this interview are:
Create an Order
Modify a Customer
Create a New Customer
Although Valerie talked about modifying orders, the manager performs this task, so I haven't included it
here. These three tasks all have a beginning, an end and a measurable outcome, so they all classify as use
cases.
Note that Create a New Customer and Modify a Customer are tasks that are actually performed as part of
Create an Order. When one use case is part of another, we say that one use case uses the other use case. In
our analysis, Create an Order <<uses>> Create a New Customer and Modify a Customer. In this system, a
customer cannot exist unless they have placed an order, so these two subtasks will always be part of Create
an Order.
60
Requirements Development
It's important that you actually do these examples, as these diagrams can appear deceptively simple until
you start trying to create them yourself!
The UML does not specify the format and the style of documentation. In general, companies establish their
own documentation standard and methods to extract relevant information from interview notes. Let's review
the interview with our Sales Representative, Valerie, in detail now, to see how we can actually generate a
use case document from our notes.
Remember now, there are three use cases associated with our Sales Representative:
Create an Order
Modify a Customer
Create a New Customer
The order in which we create these use cases is discretionary. For illustrative purposes, we will begin with
the Create a New Customer use case, and I'll go through the creation of that use case with you step-by-step,
so you'll know exactly what's going on.
Once you're more familiar with how to draw up use cases like this, we'll run through the Modify a Customer
and Create an Order use cases more briskly. So let's begin with a detailed study of how to create our first
use case.
So let's run through the creation of the Create New Customer use case. We should first of all ensure that our
use case has a clear and explanatory Name. For this task, Create New Customer is very clear and seems the
most appropriate name:
If we now follow the instructions in the "How to Build a use case" section, above, we can see that the next
thing to do is create an Overview or summary of the purpose of the use case. This is clearly to generate a
new customer to list in the database. Therefore we have:
Overview: The main purpose of the use case is to create a new customer.
61
VB6 UML
Next, we need to consider the Primary and Secondary Actors associated with this system. This is not
necessarily as simple as it might at first seem. For this use case our primary actor is the Sales
Representative, and there are no secondary actors. Let's carry on building these sections then:
You may be wondering why the Sales Manager isn’t listed as a Secondary Actor, as
Valerie did say that the Sales Manager sometimes steps in and enters orders.
The point here is that entering an order does not make the Sales Manager qualify as
a Secondary Actor, because when the Sales Manager steps in and enters an order,
they are now performing the task of a Sales Representative. The Sales Manager is
therefore acting as a Primary Actor. This is probably one of the most confusing
parts of use cases.
Actors represent roles that different people step into when interacting with the system. Anyone can step into
that role regardless of what their actual title is. Whether it is the person whose job title is Sales Manager, or
the person whose job title is Sales Representative, both of them are performing the Sales Representative
task when they enter orders into the system. There is no secondary actor here. New customers are only
added to the system during the entry of a new order, which is only done by the actor called Sales
Representative.
Now we turn to the use case criteria of Starting Point, Ending Point, and Measurable Result. The use case
clearly starts when the actor requests to make a new customer and ends when the customer has been added
or when the request is cancelled. Therefore, if successful, the Measurable Result must be that a customer
has been added.
Starting point: This use case starts when the actor makes a request to create a new
customer
Ending point: The actor's request to create a customer is either completed or canceled
Next, we come to the meat of the use case: the Flow of Events. There are two use case flows. The first
describes what happens if everything goes to plan. The second, the alternative flow of events, describes
what happens when difficulties unforeseen by the actor arise or if mistakes are made. Let us first consider
the flow of events. The actor has already requested to create a new customer. Then:
The actor is prompted to enter information that defines the customer, such as name, address, contact, etc.
The actor can choose to save the information or to cancel the operation.
If the actor decides to save the information the new customer is created in the system, and the list of
customers that was presented earlier is updated.
62
Requirements Development
The actor attempts to add a customer that already exists. The system will notify the user and cancel the
operation.
The actor enters an improper value for one of the fields. The system will not allow the update until a
proper value for the field is entered.
The last two sections are the Use Case Extensions and Outstanding Issues. It is not sensible to break down
this particular use case into smaller use cases, and there are no unresolved points - so this part is simply:
We are now in a position to put all of this information together and to build out Create New Customer use
case as shown here:
Overview
Primary Actor
Sales Representative
Secondary Actor
None
Starting point
This use case starts when the actor requests to create a new customer.
Ending Point
Measurable Result
63
VB6 UML
Flow of Events
The actor is prompted to enter information that defines the customer, such as name,
address, contact, etc. The actor can choose to save the information or to cancel the
operation. If the actor decides to save the information the new customer is created in the
system, and the list of customers that was presented earlier is updated.
The actor attempts to add a customer that already exists. The system will notify the user
and cancel the operation. The actor enters an improper value for one of the fields. The
system will not allow the update until a proper value for the field is entered.
None
Outstanding Issues
None
In this example, we rewrote the interview summary in the flow of events. We could, however, have just
copied our summary exactly as it was and placed it in the flow of events. Either way is correct. Use a
format that works best for you, your group, and your users.
Overview
64
Requirements Development
Primary Actor
Sales Representative
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
Flow of Events
This use case begins when the actor requests to review an existing customer and the
system presents the information, as well as a list of products that the customer has
purchased. The actor makes a request to edit the customer. The actor can edit all the
information except the list of products. This list of products is updated when a customer
places a customer order for a product. The actor can either save the changes and return to
the list of customers or can return to the list of customers without any changes being
saved. If the actor chooses to save the changes, the edited customer information is saved
and the list of customers is updated.
An improper value for one of the fields is entered. The system will not allow the update
until it is corrected. Another actor is using the customer record and the customer record
is locked.
None
65
VB6 UML
Outstanding Issues
None
Overview
The main purpose of this use case is to create a new product order for a customer
Primary actor
Sales representative
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
An order is created.
66
Requirements Development
Flow of Events
The actor received the phone call from a customer. The actor is asked by the customer to
place an order. The actor opens up the order entry form. The actor retrieves the name and
address from the customer. The actor enters the name in the system. The actor confirms
that the customer’s personal information is correct. If the information is correct, the actor
requests the order information. If the customer information is incorrect the actor updates
the customer information. The actor selects a product category. The actor selects the
products. The actor types a quantity for each product. The actor selects a shipper. The
actor confirms with the customer that all the information is correct. The actor completes
the order details. The actor saves to the database.
None
None
Outstanding Issues
None
Interviewer: Could you start by telling me about your responsibilities and how you perform your duties?
Sales Manager: Yes, well actually I have quite a lot of responsibility. I have two main duties: deleting
customers and editing orders. With deleting customers I have to check the customer's credit record, and if
they are a bad customer I just delete them. Editing an order is pretty self-explanatory.
Interviewer: OK, let's take deleting a customer and expand on that. Are there specific criteria that
determine whether a customer should be deleted - or is it left entirely to your judgement?
67
VB6 UML
Sales Manager: Well, I have to check the credit history, billing history and their current balance.
Information from all three will determine whether I delete them. Obviously I can't delete a customer if he
has debts or is waiting for an order to be processed. Sometimes it isn't a clear-cut case. Then I hand it on to
the Vice President of Sales
Interviewer: Is there any other reason why you might delete a customer?
Interviewer: So let's move to editing an order. Could you give me more details on that?
Sales Manager: Well it's very straightforward. If a customer rings in to change his order, or the Order
Entry Clerk makes a mistake when entering the order, I need to make the changes. In fact I waste most of
my time sorting out other people's mistakes. OK, so I get the order from the database and edit it by
changing the products or quantity, and then save it again. Sometimes the customer changes his mind
completely and cancels his order; then I have to delete the whole thing.
Sales Manager: That's what I said, and sometimes the Vice President of Sales does this too. Obviously I
can't delete an order if shipping is in progress. The same is true for editing the order. Sometimes customers
ring in and want to be removed from the system entirely. Then I have to delete the customer. And another
thing, at the end of the day I have to print out the reports from the reports menu.
In a real application, it is likely that the customer would be marked as inactive instead of
actually being deleted.
68
Requirements Development
Other Information
The actor prints daily reports from a reports menu
Immediately after we interviewed the Sales Manager, we had a chat with the Vice President of Sales to
confirm his role in the tasks mentioned by the Sales Manager. The Vice President of Sales clarified the
situation. Among his duties are deleting a customer and deleting an order, but he does not share any other
duties with the Sales Manager.
Overview
Primary Actor
Sales Manager
Secondary Actor
69
VB6 UML
Starting Point
Ending Point
Measurable result
A customer is deleted.
Flow of Events
This use case is started when the actor requests to review an existing customer. The actor
then requests to delete the customer information.
If there are outstanding unfilled customer orders or debts for the customer the actor will
be advised of this by the application and the delete will not be allowed. If there are no
outstanding orders or debts the actor is prompted to accept or cancel the operation. If the
actor accepts the operation, the customer is deleted from the system and the customer list
is updated.
The actor tries to cancel a customer who has an outstanding order or debt. The customer
delete will be canceled.
None
Outstanding Issues
None
70
Requirements Development
Overview
Primary Actor
Sales Manager
Secondary Actor
Starting Point
Ending Point
Measurable Result
An order is deleted.
Flow of Events
This use case is started when the actor requests an order listing. The actor then requests
to delete an order. If shipping is in progress the actor will be advised of this by the
application and delete will not be allowed. If shipping has not started the actor is
prompted to accept or cancel the operation. If the actor accepts the operation the order is
deleted from the system.
The actor tries to delete an order that is being shipped. The order delete will be canceled.
71
VB6 UML
None
Outstanding Issues
None
Overview
Primary Actor
Sales Manager
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
An order is edited
72
Requirements Development
Flow of Events
This use case is started when the actor requests to display the order listing. The actor
then requests to modify an order or cancel the operation. If shipping is in progress, the
actor will be advised of this by the application and the edit will not be allowed. If
shipping has not started the actor can edit the products and quantities.
The actor tries to modify an order that is being shipped. The order edit will be canceled.
None
Outstanding Issues
None
If you had any difficulties understanding how any of these use cases were put together, you
need to go back to the step-by-step guide. Follow through the Create a New Customer
example that I presented, bearing in mind the use case that you are interested in. All use
cases are created in the same way.
73
VB6 UML
To save a lot of space, I've just included these summaries for the Inside Sales Coordinator.
As you can see, there are six use cases associated with the Inside Sales Coordinator - I leave
it up to you to develop these and see if you're comfortable now with building your own use
cases.
That's our round of interviewing done, and our use cases drawn up (if you haven't drawn up the Inside Sales
Coordinator use cases, don't worry: there's plenty for us to be getting on with). We can now convert these
use cases into use case diagrams, which are a useful visual presentation of the information we've gathered
so far. These diagrams also allow us to draw together information across several use cases. Let's move on.
Modify a Customer
In UML, every use case (such as the one presented here diagrammatically by the oval) must have at least
one actor. (So if we find ourselves with a use case that isn't associated with any known actor, we'd better go
back and see whether we've actually gone wrong in our analysis somewhere!)
74
Requirements Development
This level of abstraction, where we can represent use cases diagrammatically with these ovals, allows us to
present a larger view of the system as we understand it so far: the actors in our system, and their interaction
with the use cases we've been developing, can all be represented in a clear diagram. This is a useful step
forward in our definition of the system we're defining.
In the following section, therefore, we'll build a use case diagram as we investigate our system
requirements. We'll achieve this by employing the use cases we've just made.
Sales
Representative
Preferably, we should also enter a description of the actor. Most modeling applications provide additional
documentation tools for such purpose. For instance, when you double-click on an actor in the use case
diagram in Rational Rose, you can enter a description for it:
75
VB6 UML
This description will then be displayed when you select the Sales Representative actor:
You can then carry on to add all known actors to the diagram. For now, we will move on to add a use case
to the diagram.
If you don't have a copy of Rational Rose, it is not a problem. Rational Rose, and
other such tools, just make life easier for you… but there's nothing stopping you
from drawing these diagrams in any graphics package you have available, or even
drawing them on paper!
For the purposes of this book, I am teaching you how to think with UML and how to
use UML analysis to design and develop successful VB applications. Use the tool
that suits you – it's the analysis that really counts, not the means by which you
create these diagrams.
76
Requirements Development
You start to add a use case by placing an oval to the diagram, and then giving it a name. Here is how things
look in Rational Rose at this point:
You may also enter a description of the use case, just like we did with the actor. Since we've already
created a use case specification for the Create New Customer use case, we can simply use that same
description. If you feel that the whole specification is too long, you can enter a short paragraph or two and
place a reference to the full specification document. In Rational Rose, this description may also be shown in
an information box:
77
VB6 UML
Now Actors and use cases are not isolated entities in the system. They are associated with each other. We
can therefore draw a line connecting an actor and a use case, like this:
The above diagram clearly illustrates the relationship between an actor and a use case: a Sales
Representative can create a new customer.
We can now add the Modify a Customer use case to our diagram. First create a new oval representing the
use case, and assign it a name. Then we can enter a brief description for it. Finally, we draw a line
connecting the Sales Representative actor to this new use case.
Modify a Customer
Now we can follow the same steps we just ran through for the Create a New Customer and the Modify a
Customer use cases, to add the Create an Order use case to our diagram. As usual, we should at least give it
a name and a brief description. Our use case diagram now details three use cases and one actor, and it looks
like this:
78
Requirements Development
Modify a Customer
Sales
Representative
Create an Order
79
VB6 UML
UML models this type of relationship with the <<uses>> stereotype. We can now enhance the previous
diagram to show the <<uses>> stereotype among use cases:
<<uses>>
Modify a Customer
Sales <<uses>>
Representative
Create an Order
The fact that one use case uses the functionality provided by another is represented with a directed line
from the client use case to the base use case, as in the above figure. The relationship between these two use
cases is marked with the stereotype name <<uses>>.
In our diagram, the Create a New Customer use case is the client use case, and the Create
an Order use case is the base use case.
So now we've modeled the situation where an order is placed for a new customer. But now let me ask you
this: are all orders the same? For instance, what happens if a VIP customer requests an order to be delivered
immediately using the fastest shipping method? The Northwind Company would probably want to make
sure that a VIP customer like this gets their products as soon as possible. My point here is that although
we've modeled the ordering situation, we haven't reflected in our diagram that different ordering situations
may occur - such the VIP fast order situation.
One possible solution would be to use a flag to indicate that the order being created had to be done extra-
fast for our VIP customer. This is a good solution if the only difference between a standard order and a VIP
order is speed. But using a flag like this would become a bit harder to model if there were further
significant differences between two different types of order.
For example, let's suppose for a moment that the Northwind Company had some more rules about VIP
orders:
The Sales Representative must check if that customer has purchased a minimum of $10,000 worth of
products per month in the past 12 months.
If the customer does not meet the criteria, they will be charged a special handling fee.
The Sales Representative must also check whether there is sufficient inventory on hand to cover the order
and the availability of the shipping company.
80
Requirements Development
If these VIP rules really did exist at Northwind, we would obviously be looking at a different ordering
situation from the standard one we've drawn up in our use case diagram above. We would need a way to
adapt our initial use case diagram to reflect the different VIP order situation.
The VIP rules I've just shown you are not a part of the Northwind project we're developing
through this book (if you take a look at the interviews and use cases, you'll see they were not
mentioned). However, I will now run you through exactly what we would do if we were
indeed faced with these different situations based around one of our use cases.
Once we had created this new use case, we could then say that our new use case (for VIP orders) extends
the functionality of the original Create an Order use case.
We could then model this kind of use case relationship with the <<extends>> stereotype in UML. If a use
case is a variation of another use case, it is said to extend the original use case. In this example, the
Creating an Expedient Order extends the normal Create an Order use case, as shown below:
<<uses>>
<<extends>>
81
VB6 UML
As with the <<uses>> stereotype, the directed line and the <<extends>> stereotype indicates the
relationship between the Create an Order and Create an Expedient Order use cases.
This example of an expedient VIP order was given only to illustrate the <<extends>>
stereotype. If we examine the use cases of the Sales representative, we can see that this
example is not applicable to our system.
Let's return now to where we left off before we started considering all those VIP rules. So far, we've built
the use case diagram for the Sales Representative; we can now go on to consider the other actors in our
system.
Delete a Customer
Modify an Order
Delete an Order
We can also now begin to identify the relationship between these two actors:
Right now, we know that Sales Managers manage Sales Representatives. At this stage, we're not actually
sure whether this relationship is relevant to our system, but we should capture the fact anyway. And if the
relationship later turns out to be unimportant, we can always remove it from the diagram.
Let's add a direct relationship between the two actors, and update our use case diagram to reflect the three
use cases that the Sales Manager is associated with:
82
Requirements Development
<<uses>>
Modify a Customer
Sales <<uses>>
Representative
Create an Order
Manages
Delete a Customer
Delete an Order
Sales Manager
Edit an Order
Our use case diagram now has two actors and six use cases. The diagram's getting a little
crowded. You may want to put your graphics talent into good use to arrange the elements in
the diagram to make it look good to your customer. Just remember that the most important
task right now is to capture the functionality of the system, so don’t waste too much time on
beautifying it. After all, by default, good programmers are poor artists.
83
VB6 UML
<<uses>>
Create an Order
Manages
Delete a Customer
Delete an Order
Sales Manager
Edit an Order
Create a Product
Create a Category
Category
Edit a Product
Edit a Category Inside Sales
Coordinator
84
Requirements Development
With the addition of these use cases, the first version of our use case diagram is completed. As you can see,
we have captured a set of functions the system must perform. Here are some of the advantages of using
these diagrams so far:
Having to draw up diagrams has forced us to be quite explicit about what we've learned from our
interviews and use case analysis
We've drawn together a lot of related information and managed to represent some complex relationships
therein (if you look at the interviews themselves, it isn't that easy to follow what's going on in the
system)
Our client can understand what we've been analyzing without having to learn a foreign language in order
to communicate with us
We're gradually defining a Visual Basic system that we're going to be able to successfully develop
We've come a long way already: these diagrams have helped us to define the system we're building, and to
present some clear analysis of what we know about the system thus far.
It's important to realize that these use cases simply show what the user would do
with the system under development. We have not really explored the following
question: "Is this what the user really wants to do?"
Once our use cases are complete, we should review them with the user, and be ready
to discuss any recommended changes to the way the task is performed.
Here's a simple but beautiful truth: it's much easier to update use cases than it is to change a completed
project. While building and reviewing our use cases may itself appear time consuming, our entire project
will be built from these use cases. Getting them right will mean that our project will be right. Leaving
errors in our use cases will mean our project will have errors and will have to be redone - probably
somewhere toward the end of development, when it's expensive to make these changes.
There should also be a detailed review of our use cases from a developer's point of view; for instance, what
is the best system to implement these tasks? For this book, we will assume that this has already been done,
and a DNA Visual Basic application has been chosen as the best way to implement the system. But we'll
come back to discuss technological specifications again during our design process.
85
VB6 UML
However, if we think about this for a moment, the use cases can only deal with those components that the
actors (users) are interacting with. Nothing else can really come out of our interviews but this type of
information. Since the user usually does not interact with any objects that are working behind the scenes,
such as the server objects, these invisible objects will not generally be a part of our use cases.
Use cases primarily identify client-side objects: these are objects that the users
interact with, such as the Customer object or the Order object.
So how will we ever design a system from a set of use cases if our users don't know a lot about what's
really going on behind the scenes? The truth of the matter is that the framework that our project uses will
also provide a number of those invisible, behind-the-scenes objects that the user may never know about. In
this book, for example, we'll be using a three-tier framework, and several component frameworks, such as
the ADO, RDS and MTS.
Our UML design will therefore take account of use cases and also system framework
considerations. Part of our skill will be to bring these two concerns together, as we
shall see through this book.
Don't worry if you're new to the idea of frameworks and technologies such as ADO and MTS: we'll be
exploring these topics in a lot more detail in later chapters. You might also want to check back to our
discussion of frameworks, ADO and MTS from Chapter 2.
Let's now bring our analysis back to what we've drawn up so far - which is our set of use cases. These use
cases will allow us to progress to the next stage in the UML process: identifying the visible objects that our
user interviewees were able to talk about.
Objects are things: usually nouns, but sometimes they can be expressed as verbs.
Objects contain some type of information (Visual Basic properties); that is, they have state.
Objects have some type of behavior (Visual Basic methods); i.e. they do something.
Any component that contains something that has already been identified as an object is also an object.
For example: a car component contains tire and engine objects, so a car is also an object.
86
Requirements Development
You may be wondering about the first rule: how can a verb be a thing? Looking at this sentence: "The
customer orders…" the verb orders can be considered a thing, as the sum of all things that the customer
orders will become an order. An order contains information such as the products purchased, their prices,
etc. An order has behavior; it can be updated, created, deleted, saved, and reviewed. An order has
properties, such as the total amount, customer name, etc. Orders also contain products, which are another
type of object. Looking at the verb orders, we can see that we should actually rephrase our description to
say: "The customer will have an order…." Order is now a noun. Be careful when you look at the
descriptions of your systems. Sometimes things expressed as verbs can be rewritten as a noun and meet the
requirements of being an object.
In this case, there is no black and white rule that we can apply to decide whether an actor
should be modeled as an object in the system.
If we think about this a little further, we can imagine that at some point, Northwind might use the system to
record all Sales Representatives in the system. For instance, when a Sales Representative logs on, the
system might need to verify that Sales Representative's identity before accepting their requests. If that were
the case, we would need to model a Sales Representative as an object in the system.
However, in the use cases we've drawn up, Sales Representatives are only ever actors, and are not
themselves recognized within the system. We will therefore not model Sales Representatives as an object in
our system, because we have not identified such a need.
Noun: Customer
Is a customer an object? Yes, they are. But is this object relevant to the system? Absolutely! The system
will maintain a list of customers and all customers can order products.
Is the name of a customer an object? Yes, it is. Is this object relevant to the system? It surely is. Do we
need to model it as an object? It depends. If a name is just a character string that represents the customer's
business name, defining it as an object does not seem to be necessary, since we could simply use a built-in
string type.
87
VB6 UML
Is the address of a customer an object? Again the answer is yes, and it is certainly relevant to the system.
Do we need to model it as an object? We are not sure about this yet. If we only need a string of characters,
we do not want to create an object for it.
However, what if we need to extract parts of the address later on? For instance, if the Northwind Company
decided to launch a marketing campaign targeting customers in a particular area, it might need to extract the
postcode from the address. Attempting to extract such information from a flat string could be tricky. If, on
the other hand, we had modeled the address as an object that had a street number, a string name, a city
name, a state name, and a postcode, we could easily obtain that postcode from a customer record.
We can use our imagination and experience to make a decision here. The result is rather academic. Let us
just say that since this is not clear at the moment, we will not model the customer address as an object. We
can always come back to change it.
To save us from repeating the same argument again, let's also assume that we will not model the customer
contact as an object.
In fact, regardless of whether or not we model the various customer details as objects, there is one thing
that we can decide right now: these things are all properties of customers. (In Visual Basic, we will make
them properties of the Customer class when we design classes later on.)
Noun: System
Finally, after several cups of coffee and heated debate, we reach the last noun. Is the system an object? Yes
it is, but it represents the whole system, not a part of it:
Since the system is not going to be self-aware, most likely, this reference to the system is not in itself
relevant to the system itself. Therefore, we will not model it as an object in the application domain.
As we progress through our design and development, we'll come to see that it isn't going to
be the system that accepts or rejects the requests from the user. It is most likely that an
object representing a system function will handle the request in question.
We've now completed our analysis for this particular use case - Create a New Customer. We've identified
one object: Customer. Document it!
Equally importantly, we must not forget the questions we've asked and assumptions we have made during
this analysis procedure. Questions such as whether the customer address should be an object itself remain
unresolved, and must be addressed sooner or later. We should document these questions thoroughly.
88
Requirements Development
We won't analyze the rest of use cases in the same detail right now because I've just stepped
through the fundamental process, and the logic for the other use cases would be the same.
You would be bored to death if I repeat this eight times. But let me stress this point: the
efforts we make to create a good design of the system will save us from countless frustrating
days when we have to change our code to meet the ever-changing requirements. It's worth
getting it right first time around.
By my reckoning, if we've looked through all the remaining use cases for the Sales Manager and the Inside
Sales Coordinator, and performed the noun-object-relevance criteria that we used above, we would end up
with the following list of (highlighted) nouns that also qualify as objects in our system:
The actor retrieves the product names and quantities from the customer.
The actor completes the order details.
The actor saves the order to the database.
The actor selects a shipper.
The actor prints daily reports from a reports menu.
The actor selects a product category.
Customers
Products
Orders
Order Details
Shippers
Reports
Categories
It may take some time to identify the objects or components of a system. However, the important thing is
that we have managed to identify all the visible objects in the problem domain. We have laid a solid
foundation on which we will be able to move forward.
While it is indeed important that our objects have properties that represent the structure of the data in the
database, and that the methods do not violate the database's integrity, our objects will have a lot more to do
than merely maintain a database. We will have client objects that are providing information to the users,
objects that have nothing to do with the database (you could create a component to manage fonts for printed
reports), and finally we will be dividing our objects across tiers (more on that later). In addition, we will
need methods to handle communication between two different objects - which is not something we can
easily see from the database structure.
89
VB6 UML
Starting with a database structure and working backward cannot create all of our
objects efficiently or accurately. It is also quite possible, of course, that there may
not yet be a database at this early point in the development of the system!
Our components should initially be designed from use cases that center on how the users will use the
system. This design can later be expanded to include the effects of frameworks, for instance. My point,
here, is that when our objects are based on the needs of the user, it becomes a much simpler task to build
applications based on these objects that will satisfy all of the user's needs.
Summary
In this chapter, we've a taken an in-depth look at use cases and use case diagrams. They are the first key
UML models that we encounter when we create a project. These models are used to determine the system
requirements from the user's point of view. We should be able to identify many of the project's objects from
these use case models.
We have taken the first steps into the project that we will build as we progress through this book. We have
interviewed members of the Northwind Company sales team, and condensed a profuse amount of
information into a clear and concise set of use case models. We then drew up a use case diagram, and
finally identified the visible objects that users could relate to in this project.
In the next chapter, we will convert our written use cases into sequence and collaboration diagrams, which
will give us a more refined visual model of the events that occur between actors and components in the
system, and between the different components themselves within the system.
90
Interaction Diagrams
Now that we've learned how to build use cases and use case diagrams, it's time to move on to the next stage
of our project design: interaction diagrams. By developing our interaction diagrams, we will continue
moving towards a more detailed definition of our system design through the UML process.
Sequence diagrams
Collaboration diagrams
Sequence and collaboration diagrams present the same information in different forms, and may be used
interchangeably. Each diagram offers its own advantages, and the type of diagram that the you choose will
depend on your own particular preference, and the particular details of your project.
The use cases and associated diagrams that we developed in the previous chapter have allowed us to
identify the system's components and how these objects communicate with the actors and each other. Using
this information, we can now represent the interactions between these components, and between objects and
actors in interaction diagrams.
Although the two types of interaction diagram essentially display the same information,
sequence diagrams, as the name suggests, tend to concentrate on the temporal element,
while collaboration diagrams emphasize the spatial element of interactions.
Once our interaction diagrams are complete, they will help us find any patterns that exist within our system,
help build our user interfaces, and provide the information needed to carry the system design on further.
But before we get ahead of ourselves, let's find out more about sequence and collaboration diagrams.
VB6 UML
Interaction diagrams allow us to look at each of our components from the outside. In these diagrams, our
components are black boxes that perform certain functions. Other components or actors send requests in the
form of a message to a component, and the component responds by performing some function.
For example, the message could be a request to update a record, and the response could be
updating the record in the database and then sending a return message to say that the
update was successful.
Interaction diagrams are an excellent way to start us thinking about the public methods of our objects.
These public methods represent the responsibilities of our objects; that is, what they can do for other
objects in the system - or actors outside of the system. Of course, we've already seen that our components
are initially designed from interviews and use cases: they tend to center on how the users will use the
system. But we can expand upon this user-centric design later on, once we're established along our design
track. At that stage, we will also include the design effects of frameworks and system constraints, for
instance.
When our objects are based on the user's needs, it becomes a simple task to build applications out of these
objects. The design of the external part of the project is driven by the user requirements, and will therefore
have a high probability of meeting these requirements.
Of course, there are many other requirements, such as the system working efficiently, or getting the right
information showing in the right place… but these considerations should all be based on internal
requirements.
Internal requirements will extend our design once we've understood the system in
terms of the external user's requirements.
Interaction diagrams are a tool for us to describe our system with far greater definition than we have so far
seen. Our interaction diagrams will very much rely upon our earlier use case analysis, but they will take
that analysis much further. So let's move straight on and consider: if we can use sequence or collaboration
diagrams, which one is best?
Sequence and interaction diagrams are usually constructed from different UML models - and therefore
focus on different aspects of design. As we are about to find out, the question we really need to ask is “Do
we want to base our project design on the user's point of view?”
94
Interaction Diagrams
Sequence diagrams are built from use cases. Since use cases represent the client side of the application,
sequence diagrams will be heavily focused on the client side of the application. Once all use cases have
been converted into sequence diagrams, it's possible to expand these sequence diagrams to include server
side components. This makes building sequence diagrams a two-step process:
Collaboration diagrams, on the other hand, are usually built from an entirely different modeling technique
that uses CRC cards, although they can be built from use cases.
CRC stands for Class, Responsibility and Collaboration. The CRC card technique of
designing a project focuses on the components in the system instead of the user
requirements. While we will use the CRC diagrams as a tool to help us build activity
diagrams, it is really a completely different approach to designing your project from
using use cases.
I will include a brief discussion of collaboration diagrams, as it is possible to produce them from use cases -
and they are a commonly used part of the standard.
I will not go into a discussion of making CRC cards and converting them into collaboration diagrams,
however, as I feel the use case approach is more appropriate for a Visual Basic project. The heart of a
Visual Basic project is still the user interface, and this interface should be based on the user's requirements,
not just the components the user employs.
Sequence Diagrams
Sequence diagrams represent interactions. They show how messages are passed over time between
components and from actors to components of the system. These messages will define the public methods
of the components.
Represent all the possible events that can occur for a use case within a single sequence diagram.
Build one sequence diagram for each possible event in a use case.
We could, for example, build a sequence diagram that just shows communication between the actor and the
customer object when creating a customer. We could also build another diagram that shows the
communication between all of the objects, including the actor, the customer component, the server
component and the database component.
Sequence diagrams are models of the system: they can be used to visualize the communication within the
system, thus to help the developer arrive at the best design of the system.
95
VB6 UML
Starting Points
As I've already mentioned, the starting point for our sequence diagram should be a use case. As you will
recall from the previous chapter, we can identify from a use case the objects and components that are
required for a particular interaction.
We can usually start by assuming that our interaction will need an actor and some objects. We can therefore
draw up this elementary situation as shown here:
96
Interaction Diagrams
Object 2 creates Object 3, which then exists until it's destroyed by Object 2 again. This is shown first
with the word create over the arrow leading out of Object 2's timeline, and then by the word delete over
the arrow with a cross at the end of Object 3's timeline.
We can see here that the interaction commences when the actor sends a message to Object 1. Object 1 then
returns a message to the actor, which then talks to Object 2. As a result Object 3 is created - and so it
continues… the interaction shown here finally ends when Object 1 sends a message to the actor.
Reflexive Messages
In the sequence diagram we've just looked at, you'll see that Object 3 sends a message to itself. This is
known as a reflexive message, and is represented by an arrow that loops back on itself. Reflexive messages
can represent situations where an object needs to communicate within itself – something we'll find arising
later on.
Focus of Control
You may also have noticed the long thin boxes that are present along the time lines in our last sequence
diagram. These represent the focus of control. This is the period of time that an object/actor is responsible
for an action. This may be direct control or indirect control resulting from a sub-procedure.
Although these focus of control boxes may be useful, it's worth noticing that the sequence of
events occurring during an interaction is adequately represented without them.
97
VB6 UML
For the purposes of this book, we don't really need this extra information in our sequence diagrams. I direct
the interested reader towards 'Instant UML' also by Wrox Press, for a broader treatment of the UML
notation at large.
Right now, that's all we need to know about sequence diagrams to start creating our own diagrams for our
Northwind project. Don't worry - I'll take you through the diagrams step-by-step.
In the Create New Customer use case we have only one object: Customer. So let's add Customer to the
sequence diagram. The way in which a sequence diagram is created varies among different modeling tools.
I will assume you have found a tool that suits you.
First, add a new object to the diagram. It should look something like this:
: Customer
Note the colon before the class name Customer. The colon indicates that this is an instance, or object, of
the class.
98
Interaction Diagrams
Objects interact with each other; classes do not. If you are unclear about this
distinction, you might want to read an object-oriented tutorial to get the hang of
this. For the moment, it's enough to know that we instantiate objects from classes,
which are essentially the blueprints for our objects. If you want to learn more about
this, I refer you to "Beginning VB Objects" also published by Wrox Press.
Only objects participate in sequence diagrams. The timeline descends from the object box and, as I
mentioned earlier, represents the time the object exists, and will also indicate the order of interactions
among objects in the diagram.
: Customer
: Sales
Representative
In the same way as for the Customer object, an instance of the Sales Representative actor, indicated by
the colon preceding the actor's name, is placed in our diagram. It also has a time line attached to it.
Starting Point: This use case starts when the actor makes a request to create a new customer.
Now there is a problem here: what object does the actor make the request to? It seems quite obvious that the
Customer object is the target. But how does an actor interact with an object? Through an interface, of
course.
An interface may be a form with input fields mapped to the Customer object, or an ActiveX Control hosted
in a web browser. Either way, we know that there is an interface between the actor and the Customer
object. We should add it to the diagram and place it in between the actor and the object, like this:
99
VB6 UML
: Interface : Customer
: Sales
Representative
: Interface : Customer
: Sales
Representative
Request to Create a Customer
Now we can see that a Sales Representative can send a “Request to Create a Customer” message to the
Interface object. Remember, the tall, thin rectangles are called the focus of controls. They show the period
of time during which an object is performing an action. The top of the rectangle indicates the start of the
action, and the bottom is its completion.
Flow of Events: The actor is prompted to enter information that defines the customer, such as Name,
Address, Contact, etc. The actor can choose to save the information or to cancel the operation. If the actor
decides to save the information the new customer is created in the system, and the list of customers that was
presented earlier is updated.
The actor is prompted to enter information that defines the Customer, such as Name, Address, Contact,
etc.
100
Interaction Diagrams
Clear enough? Not exactly. How is the actor prompted? It may be that the Interface displays relevant fields
so that the actor can enter data into those fields. Ok, let's assume that this is the case; but how does the
Interface know that it should display those fields? It may simply display such fields when it receives the
request. Or it may pass the request to the Customer object to ask for permission and further instructions.
Let us assume, again, that the latter approach is the one we want. So we'll add another message to the
sequence diagram:
: Interface : Customer
: Sales
Representative
Request to Create a Customer
Request to Create a Customer
By now you're probably wondering why we don't have a more detailed description of events in the use case.
The answer? The use case specification is adequate for its purpose because it provides enough information
to specify the user's needs and to allow them to review it. Our Sales Representative Valerie really doesn't
care how the customer entry form figures out what it should do when she clicks on the Add Customer
button, for example. As long as Valerie gets to enter the new customer information, she is happy.
So we know that our use case specification is too limited for direct translation into a
sequence diagram. If it were anything otherwise there would be little point in having both
models. So what do we do? In general, we should talk to our customer or review the
requirement document in order to find out exactly what should be done. For instance, do we
need to put some kind of verification or authorization process in this case? We would also
update our use case specification to include the further information. This is another example
of iterative analysis and design process at work.
What should the Customer object do when it receives the request to create a new customer? Again, the use
case doesn't say anything about that. This is understandable, since the question is really about the internal
message handling - and the use case only identifies the external behavior of the system. We'll leave this
issue for later stage: when we understand a bit more about the system. As we will discover later, there are
other UML diagrams that consider these interactions in more detail.
101
VB6 UML
For now, just add a message to the sequence diagram indicating that the Customer object accepts the
request, like this:
: Interface : Customer
: Sales
Representative
Request to Create a Customer
Request to Create a Customer
Request Accepted
The Interface object will simply display the fields so that the Sales Representative can enter customer
data. The use case specification states that:
We need to add yet another message to our sequence diagram – one that represents the action when the
Sales Representative enters customer data:
: Interface : Customer
: Sales
Representative
Request to Create a Customer
Request to Create a Customer
Request Accepted
102
Interaction Diagrams
The Interface object does not need to communicate to the Customer object while the Sales
Representative is entering the data. When Valerie, the Sales Representative, has finished however, she
would tell the Interface object to save the data as stated in the use case:
We must therefore add the message Save Customer Data to our sequence diagram:
103
VB6 UML
This means that we need to add a new message to the diagram indicating this possible cancel event. Here
we need to indicate that the Sales Representative either saves the information or cancels the request. We
will need to use branches to represent this either-or situation. The mutually exclusive conditions are given
in brackets before the message:
You can see that the timeline of the Interface has been split into two at this point:
As the interaction progresses only one of these timelines will be followed, depending on the decision of the
Sales Representative to either save or cancel.
If the actor decides to save the information, the new Customer is created in the system
104
Interaction Diagrams
Obviously, the Interface object will need to send a message to the Customer object indicating that it
should save the customer data:
How does the Customer object save the new Customer? Usually this is done by writing the Customer data
to a database. We do not care how this can be achieved, but we do need to show this process in the diagram.
We should add a new object, DB Business Logic, to our sequence diagram:
105
VB6 UML
The name of the new object is not important at this stage, as long as it shows what the object is. We can
then let the Customer object send a message to the DB Business Logic object to save the customer
record, like this:
The use case specifies that something in particular happens after the customer record is saved in the
database:
Again, the use case doesn't say how the Customer list is updated, so we'll just assume that this is done
when the DB Business Logic sends back a confirmation that the Customer record is saved:
106
Interaction Diagrams
The Customer object may then send a confirmation back to the Interface object:
The Interface object will update the Customer list by sending an Update Customer List message to
itself. We'll model the behavior of an object sending a message to itself with a directed line starting and
ending at itself, as shown in the following sequence diagram:
107
VB6 UML
Remember, from Chapter 2, that we are using a disconnected recordset. All of the
customers will already be within the customer object. Finding out whether a
customer exists will not therefore require communication with the database.
Let's say that when the system tries to save a Customer record, it first checks to see whether this is an
existing customer. If this is an existing customer, it will send back an error indicating this condition. How it
checks for the existence of a customer is not important at this stage. We do need, however, to indicate in
our sequence diagram that there is a possible error if there is already an existing customer. We can do this
by letting the Customer object return an error message to the Interface.
This is another situation where branching occurs. The Customer object either proceeds to save the
information or it informs the interface that it isn't possible because the customer already exists. Let's
develop our sequence diagram accordingly:
Alternative Flow #2
The second Alternative Flow of Event is:
108
Interaction Diagrams
The actor enters an improper value for one of the fields. The system will not allow the update until a
proper value for the field is entered.
We will not show this in our sequence diagram because it specifies the internal behavior of the Interface
object. This problem should be tackled when we have more details.
Ending Point: The actor's request to create a customer is either completed or canceled.
This requires no more messages and concludes this diagram. So that's it - we have successfully created the
Create New Customer sequence diagram by methodically going through the stages outlined by the use
cases and asking ourselves how we can represent that information in a sequence model.
In this example, we have had a good look at the creation of a sequence diagram from the corresponding use
case. We can go ahead and create sequence diagrams for the rest of use cases. I will not explain every step
or message again, only those that are significantly different from what we have covered here.
However, this precondition does not specify any action required. Therefore no message is created from it.
Compare now the Create New Customer sequence we just created with this finished Modify Customer
sequence diagram:
109
VB6 UML
You'll undoubtedly notice that both sequences have a lot in common. Keep watching this space, and let's
move on to the next sequence.
110
Interaction Diagrams
Notice here that the customer can only be deleted if they have no unpaid debts and no outstanding orders.
Therefore, it's necessary for the Customer object to check this out. Our Customer object does this by
obtaining the order information for the particular customer from the Order object. The appropriate checks
are then made by the Customer object – represented in the diagram by those arrows that double back on
themselves.
If there are no unpaid debts or outstanding orders then the Customer object will send a Delete Record
message to the DB Business Logic object. If, however, there are unpaid debts or outstanding orders, the
Customer will not attempt to delete the record; instead, it will send back an error message to the Interface
object.
In this sequence diagram, I have chosen not to show the cancel operation. This is because I wanted to show
you that it not essential to include the whole of the use case within a sequence diagram. It's quite legitimate
to model only part of a use case, if that best suits your needs.
111
VB6 UML
Modify Order
Delete Order
112
Interaction Diagrams
So now you're probably thinking: OK I understand how to build these sequence diagrams and they look
pretty good. But why should I bother? What do they give me? To answer these questions, I need to present
a few more diagrams. Not only will they give you more practice if you work through them, but they will
reveal something very useful. Let me present these next diagrams, and I will be in a great position to
answer this question.
Overview
Primary Actor
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
113
VB6 UML
Flow of Events
This use case is started when the actor requests to add a new product. The system will
always accept the request and prompt the actor with a new product entry form. The actor
can then enter the product data. Once the edit is completed, the actor can request to save
the product. The actor can request to cancel the operation before the product is saved
The actor tries to create a product that already exists. The save operation will be
cancelled and the system will provide an error message indicating the cause of failure.
None
Outstanding issues
None
114
Interaction Diagrams
Overview
Primary Actor
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
Flow of Events
This use case is started when the actor requests to modify a product. The system presents
the actor with the product’s data. The actor can then request to delete the product. The
actor can request to cancel the operation before the product is deleted.
If the requested product does not exist, the system will return an error.
None
Outstanding Issues
None
115
VB6 UML
Overview
Primary Actor
116
Interaction Diagrams
Secondary Actor
None
Starting Point
Ending Point
Measurable Result
A product is deleted.
Flow of Events
This use case is started when the actor requests to see a product. The system will always
accept the request and present the actor with the product’s data. The actor then requests
to delete the product or cancel the request.
If the requested product does not exist, the system will return an error. If there is still
some of the product in stock, the system will return an error.
None
Outstanding Issues
None
117
VB6 UML
Sequence diagrams also convert the verbal use case into a picture that maps all of the
messages. A picture is always simpler to understand, and this will make it easier for us when
we come making class and activity diagrams.
When we are creating our sequence diagrams we often discover that the use cases have not answered all the
questions we need answering. Making sequence diagrams highlights these shortcomings and provides a
mechanism for iterative design.
Sequence diagrams can also show us the order of events a user goes through when interacting with the user
interface. This can be very useful when making the GUI. This is an important point - and I devote whole
chapters to designing the GUI later in the book.
Another great reason for creating sequence diagrams is that they can help us identify patterns. Let's explore
this idea in light of our sequence diagram analysis.
118
Interaction Diagrams
Please go back and take a look at the Create New Customer sequence diagram. Now look at the Create
New Product sequence in conjunction with Create New Customer diagram. It is easy to see that these
diagrams are similar. In fact, they are practically identical.
Now take a look at the Delete Customer, Delete Order and Delete Product sequence diagrams. A
pattern is emerging here as well. The same is also true of the Modify Customer, Modify Order and Modify
Product diagrams.
If we were to look at the properties of these Customer, Order and Product components, they would all seem
very different. A sequence diagram, though, only looks at the messages being passed back and forth, which
means that it only looks at the object’s public methods. When we view these three objects' methods, we
see that they are all very similar. We can also see that the DB Business Logic component works in an
identical manner for all three objects. What does it mean when things have the same methods but different
properties?
To answer this question, let's step back for a moment and use cars for our next example. Each car is an
object with its own set of properties, but every car shares the same underlying functionality.
It might be worth just stopping and thinking about this for a moment. Every car has a unique set of
properties that helps us to identify them: color, shape, type of wheels, manufacturer, etc. Yet, even though
cars have different properties, they all perform pretty much the same activities: accelerating, braking,
turning corners, indicating at junctions, etc. Although every car has a different set of properties that makes
it unique, they all function in largely the same manner. We do not have to find a whole new way of making
a car each time we want to make a new, unique car. We can use the same technique to build a car whether it
is going to be red or metallic silver, four seats or two. Since the way every car works is essentially the
same, we only need one way to build an infinite number of unique cars. The way a car is put together
(wheels, steering wheel, lights, dashboard…) is a pattern. It is because of this pattern that we can use the
same technique to build an infinite number of unique cars (just look down your street).
Components work in the same way. Even though they may have different properties, they can still work in
exactly the same way. If we know how to design or build one of these objects, we know how to build all of
them. Once we figure out how to build my Customer object, we will also know how to build Order and
Product components, too. By building similar sequence diagrams from our Modify and Delete use cases, we
have found that these tasks are also alike for our Customer, Order and Product components.
119
VB6 UML
Interfaces
There's a lot of confusion about Visual Basic interfaces. A Visual Basic interface is a class with methods
and properties with no code. Another Visual Basic class can then implement this interface. The class that
implements an interface will get all of the methods and properties that are in the interface. These interfaces
allow you to have one standard set of methods and properties for a group of similar components.
For example, I could create an Animal interface that has a Move and a Sleep method. I could then
implement this interface into a Rabbit component. I would then have to write the code to make the
Rabbit object move or sleep the correct way for a rabbit when these methods are called. I could also make
a Cat component that implemented the Animal interface, and write the appropriate code for that object. If
I had an object built from the Animal interface, I could call the Move method and that object would
respond in the way that was appropriate; that is, the rabbit would jump and the cat would walk. This is a
form of polymorphism.
While this form of polymorphism could be useful in our current example (since we do want all of our
components to have the same methods), it will not offer us any advantage in actually coding our
components. Remember: an interface has no code within it, the classes that implement the interface must
write all of the code for the methods and properties in the interface. We are looking for a solution where we
can write code once, and reuse it for all of our components. Interfaces will not provide this type of solution.
While interfaces could apply here, they will greatly complicate the code and offer only a small
improvement in our coding solution. It is for this reason we will not discuss them any further.
Object Hierarchies
The other option is to use an object hierarchy to build our
objects. The hierarchy would look something like this:
The hierarchy here is between the Managing Class and the Managed Class.
On the bottom of this hierarchy is a class that could contain the properties of the object (the Managed
Class). As all of these objects have different properties, this class will be completely different for each of
our objects.
Next up in the hierarchy, is a class that will manage the Bottom class, which we will call the Managing
Class. This class will contain all of our client side methods, such as Move to the Next Customer,
Add a New Product, Count Customers, Update Products, etc.
When considering this hierarchy, we discover one amazing fact: the Managing Class would be 99%
identical for every one of our objects! This is really something - a few hours of work talking to our users,
converting this information into use cases and sequence diagrams, has helped us find a pattern within our
system. Once we have the Top class coded, we can then build all of the rest of our client side objects by
simply making the Bottom classes - which is a relatively easy task.
120
Interaction Diagrams
The Managing Class performs the same basic tasks for all of these objects.
We can see that our client components will have many methods that will be the same for all the
components. Using Visual Basic, we can build one of these client components, such as the Customer
component, and use it as a code template to build the other client components – such as the Order
component, the Product component, etc. When we look at the sequence diagrams for different components
and find patterns like this, we can turn these patterns into code templates. The relationship between patterns
and code templates is very similar to the relationship between a component and a class. We will discuss this
in a later chapter – it's going to be an important part of our project design.
Without this ability to identify patterns, we would have been making these components separately… almost
certainly we have saved time and development funds by identifying patterns.
For example, the diagrams we've just created are quite detailed. This was because I wanted to give you a
thorough grounding in building sequence diagrams.
Let's pull out from these diagrams, and take look at some of them in less detail. Below are the three less-
detailed sequence diagrams for Modify Customer, Modify Product, and Modify Order:
121
VB6 UML
Clearly they don't have as much information on them as those we created earlier. However, on inspection,
some common patterns become evident between them.
UML is a tool that is supposed to simplify our project design, not cloud the issue. It makes sense, then, to
include only as much detail as we need. We might not get this balance right first time, but after review and
iteration we should find these diagrams are a great help.
From now on, the sequence diagrams we encounter through the book will be just detailed
enough to suit our purposes.
122
Interaction Diagrams
Collaboration Diagrams
As our use cases become more complicated and we refine our sequence diagrams, it's quite likely that it
will become difficult for us to keep track of all the objects in the interaction. Collaboration diagrams, which
model an interaction among objects rather than the sequence of events, present a view of the system from a
different angle.
Collaboration diagrams emphasize the nature of the object interaction, rather than
the timing of the interaction.
Collaboration diagrams do show how messages pass back and forth between objects, and between the
objects and the primary actor, however - just like a sequence diagram. A collaboration diagram can still
represent an entire use case, or a portion of a use case.
As before, the starting point for this diagram is a use case. The first thing we need to do is identify the actor
and the objects. The actor is again represented as a stick figure, and each object is represented as a square
with the object name - Object1, Object2 and Object3. We then place them on the diagram as shown.
You will note that the actor and objects are no longer positioned horizontally, and that Object 3 is drawn
even though it does not exist at the beginning of the interaction.
Lines are then drawn between the actor and objects that interact with one another, representing the links
between them.
123
VB6 UML
Arrows show messages being passed back and forth. Up to now, there is no reference to time, so we need to
number the messages in the order they will occur. Note that a reflexive message is represented by a loop.
This diagram shows the same information as the generic sequence diagram. If you look at them together
you can see how they emphasize different aspects of the same interaction.
: Interface
: Customer
: Sales
Representative
: DB Business Logic
124
Interaction Diagrams
You'll have noticed the first difference between the collaboration diagram and the sequence diagram: the
collaboration diagram does not show objects with timelines. This is because the timing is no longer a
crucial part of the diagram. When we show messages passing among the objects, we simply use a number to
indicate the event in the sequence.
: Interface : Customer
: Sales
Representative
: DB Business Logic
: Interface : Customer
: Sales
Representative
: DB Business Logic
125
VB6 UML
The Interface object then sends a Request to Create a Customer message to the Customer object.
Again, we need to draw a link between these two objects and a directed line from the sending object to the
target object:
: Interface : Customer
: Sales
Representative
: DB Business Logic
When the Customer object receives the request, it sends a Request Accepted message back to the
Interface object. This time, we direct the message from the Customer object to the Interface object:
: Interface : Customer
3: Request Accepted
: Sales
Representative
: DB Business Logic
126
Interaction Diagrams
The Sales Representative can then enter the customer data to the Interface object. This is our fourth
message:
: Interface : Customer
3: Request Accepted
: Sales
Representative
: DB Business Logic
When she is done, the Sales Representative can send another message to the Interface object, indicating
that they wish to save the customer data. This is our fifth message in the diagram:
1: Request To Create a Customer
: Interface : Customer
3: Request Accepted
: Sales
Representative
: DB Business Logic
Now the Interface object will pass the request, along with the data, to the Customer object. This is
message number six:
1: Request To Create a Customer
: Interface : Customer
3: Request Accepted
: Sales
Representative
: DB Business Logic
127
VB6 UML
Now the Customer object can send back two messages. The first is a Save Customer Record message
that indicates the success of the request operation. The second is a Customer Exists message indicating
that the save operation failed.
Because the consequent events are different based on the returned message, we'll analyze
the success scenario first, and then move on to the fail scenario that occurs if the customer
already exists.
: Interface : Customer
3: Request Accepted
: Sales
Representative
: DB Business Logic
Note that we reorganized the objects so that the diagram fits in the page. With a Modeling
tool, this should be as easy as dragging the object box and dropping it to a suitable place.
When the Customer object receives the success message, it sends a Customer Saved message to the
Interface object. These are messages eight and nine:
1: Request To Create a Customer
: Interface : Customer
3: Request Accepted
: Sales
Representative
9: Customer Saved
: DB Business Logic
128
Interaction Diagrams
The Interface object now needs to send a message to itself to update the customer listing. We model this by
creating a link from the Interface object back to itself. We then add a directed line representing the Update
Customer Listing message:
1: Request To Create a Customer
10: Update Customer Listing
: Interface : Customer
3: Request Accepted
: Sales
Representative
9: Customer Saved
: DB Business Logic
Where the arrow points to is not important, nor is the placement of the link, above or below the object box.
This completes the flow of messages for the successful creation of the customer record.
You could argue that it should be number seven, since it is really an alternative message to the Customer
Record Saved message. It does not really matter though, as the timing of the events is not important in the
collaboration diagram.
You might think that this is a strange thing to say. If the numbering of the messages is not important, why
do we bother to number them at all? Well, we want to display as much information on a diagram as
possible, but not at the expense of the diagram's primary purpose. In this case, the collaboration diagram is
supposed to help us to understand the organization of the objects for a particular interaction. If we were to
split the objects into two parts to ensure that the numbering was consistent, it would defeat the object of the
diagram.
One way to circumvent this problem would be to make two diagrams, one for each scenario. That is
precisely what we are going to do. That is one of the beauties of UML: its fluidity.
We are not constrained to model the entire use case in a single diagram. We may
choose the most appropriate way for our particular problem.
129
VB6 UML
So we need to revert to the diagram where only the first six messages were displayed. It is shown here, with
the addition of the message from the Customer object telling the Interface object that the customer already
exists (message number 7):
1: Request To Create a Customer
: Interface : Customer
3: Request Accepted
: Sales
Representative
7: Customer Exists
: DB Business Logic
The last event of the Create New Customer use case is when the Sales Representative cancels the
operation to the diagram. This will involve a message being sent to the Interface object. It's obvious that
this will add nothing new to the structural organization of our objects. We therefore do not include this
diagram.
That concludes our first collaboration diagram. It might seem intimidating when you look at the end result
if we hadn't built it ourselves. However, as you should have realized by now, there is really no magic in
building such a diagram.
1: Request a Customer
Customer
: Interface
: Sales
Representative 2: Get a Customer
3: Customer Record
: Customer
: DB Business Logic
130
Interaction Diagrams
Note that the placement of objects and messages is not consistent with the previous diagram. I did this just
to demonstrate that we can have a different configuration of drawing diagrams. However, when you apply
this to real world projects, you should define a consistent presentation for your diagrams and stick to it.
I don't want to dwell on collaboration diagrams for too much longer now, as we won't be pursuing them any
further in our Northwind project. I've shown you enough to get a feel for what they're about, and whether
you'd like to research them further for yourself.
Summary
Collaboration and sequence diagrams provide a bridge between our text based use cases and our pictorial
UML diagrams. Either one of these diagrams can be used to show the dynamic behavior of your system.
Every use case should be represented by either a sequence diagram or a collaboration diagram. It's
important to find a method that works for your project and your team. Take some time out and try to build
some additional sequence diagrams and collaboration diagrams.
Using these UML diagrams, we can find patterns that we can use to simplify coding our Visual Basic
applications. Finally, these diagrams place our use cases within our frameworks, determine whether our use
cases are complete enough to perform all of the system's required tasks, and see if everything can work
together.
The eagle eyed among you will have noticed that we have not built the Create an Order sequence diagram.
This is because one of the other uses of sequence diagrams is to help build the GUI. These diagrams allow
us to maintain form control and build forms based on how a user does a task. We shall make and use the
Create an Order sequence diagram in Chapter 6, where we explore this idea of creating a GUI prototype
from a sequence diagram.
In the next chapter, we turn our attention to another UML model: the class diagram. This will take us to the
next stage of the UML Process for our Northwind project.
131
Class Diagrams
We have now spent quite a lot of time with UML in the early design stages of our project. We've examined
the user requirements, and what objects we'll need to fulfill those user requirements. It's now time for us to
delve a little deeper, and turn our attention to system design. To do this I will need to introduce to you a
new UML model, the class diagram.
In Chapter 1, we overviewed the whole UML Process and saw how we would gradually refine our
UML design to be increasingly more detailed, until the point where we should be ready to translate our
UML into Visual Basic code
In Chapter 2, we considered good project management, and made some decisions about the
technologies we'll be using for our Northwind project.
In Chapter 3, where we developed use cases, we identified a list of objects in our application domain
In Chapter 4, we created our sequence diagrams, and specified how actors and objects interact with
each other. This level of analysis supplied enough information for some comprehensive discussion
between our customer, the Northwind company, and ourselves, the development team.
Typically, we shall have several meetings with our customer on the functionality and the external behaviors
of the system. During those meetings, we should reach agreement on how the system works from the user's
point of view. That is, we should have presented enough information for our customer to judge the
usefulness of the system in relation to their business requirement.
VB6 UML
It's very likely that such meetings would expose some of the shortcomings of the initial
requirements. The customer might, for instance, find that the flow of operations does not
match their business procedures. They could also discover some of the implicit requirements
of the system. For example, our analysis tells us that a Sales Representative will find a
product by first selecting a category and then selecting a product from a list of products that
belong to that category. This means that someone in the Northwind company must be able to
create, edit, and delete the product categories. But this would lead to a whole new set of use
cases concerning the maintenance of product categories. We may have to go through several
such iterations to further polish our system analysis.
Once we've understood the system well enough, however, we should move on to the system design. We will
need to analyze the internal working of our system. In the object-oriented analysis and design paradigm,
this means that we must start to create a hierarchy of classes that make up the backbone of the system.
So this brings us to our next UML model, the class diagram. From our use cases and sequence diagrams,
we can create classes that will provide the infrastructure of the Northwind ordering system. So, let's move
right on to the next stage of the UML Process and take a look at these class diagrams.
In UML, a class is the description of a set of objects with the same attributes, operations and relationships.
Each object is an instance of a particular class. For example, we do not say that we order a book, we say we
order the "Visual Basic 6 UML Design and Development" book. Here, Book may be a class, and the VB6
UML book is then an instance of Book.
Classes do not interact with each other. We create instances of particular classes to
instantiate objects. It is our objects that interact with each other.
A class diagram shows a collection of these classes and how they are related to one another. It offers a
simple static view of the classes of a system. Class diagrams show the Public and Private methods and
properties of our classes. The number of class diagrams required to model a system will depend on its size
and complexity.
In a class diagram, a class is represented in its most simple form as a box with the class name within it, as
shown here:
ClassName
134
Class Diagrams
As it stands, however, this is not very useful. We need some additional information about the class. We
display this information by dividing the class diagram into three sections:
We place a symbol next to each of these properties and methods to indicate its level of visibility. The plus
signs + indicate Public properties and methods, while the negative signs – indicate Private properties
and methods. We use the hash sign # to indicate Friend properties and methods.
Friend properties are properties that can only be seen by other modules within the
component.
A component using another component cannot see any Friend properties within
the component it's using.
Class Types
Classes can be broken into four different categories, depending on what type of object we intend these
classes to become. These four categories of class are:
135
VB6 UML
Business Entity Objects generally contain very few methods, and consist mostly or completely of
properties.
Business Service objects generally contain very few properties, and consist mostly or completely of
methods.
We will only build Business type objects in our project, since we're focusing on the
information in the database for our Northwind application. A more complete
application, however, would require all four types of objects, and UML diagrams
could be used to build all of them.
Identifying Classes
We can extract a collection of classes from the list of objects that we identified in the previous chapters. At
the end of Chapter 3, we had the following object list:
136
Class Diagrams
Customers
Products
Orders
Order Details
Reports
Shippers
Categories
Clearly in a book like this, we will not have time to take an in-depth look at every single
object in our project. For example, we have not considered the Shipper or Supplier objects
in detail. Similarly, although we have not previously examined the Employees object in
previous chapters, it should be clear that we will need one for our full application. After all,
we have many different types of employee: the Sales Representative, Sales Manager etc.
These objects will be included in our class diagrams. You can make these other objects if
you feel you need some more practice at the UML models we've looked at so far.
In Chapter 4, where we developed our sequence diagrams, we added two more objects. The first of these
was the Interface object, which was the visual interface of the system. The second object we introduced
was the DB Business Logic object, which enabled other objects to communicate with the underlying
database.
As we will see later, we can break each of those two objects into a collection of smaller objects. For now,
though, we will treat them as single objects that provide a set of functions for other objects. How they
actually work internally is not relevant at this stage.
The first stage of building a class diagram is to display all the classes, as shown here:
Interface
Employee
DB Business Logic Supplier
This class diagram incorporates lots of different classes, and will eventually show how these classes are
related to one another; it will also, when we have finished our analysis, display the properties and methods
for each of these classes. There's a lot to do then, at this stage of the UML process - so let's get started by
learning about the different types of associations that may exist between classes.
137
VB6 UML
Ordinary associations
Inheritance
Aggregation
Ordinary Associations
Ordinary associations are relationships between two classes where one class does not require the other class
to exist. Each class has equal responsibility in the relationship. Ordinary associations are represented by a
line connecting the classes:
Class 1 Class 2
Inheritance
An inheritance relationship exists when one class object is a specialized version of another class object.
Inheritance relationships are represented with a line with a triangle in it:
Class 1 Class 2
Aggregation
There are two types of aggregation:
Composite aggregation
Simple aggregation
A composite aggregation is a relationship between two classes where one class cannot exist without the
other class. This is usually a one-way relationship.
The Order Details class requires the Orders Class. An Order Detail cannot exist without an Order,
but an Order can probably exist without an Order Detail. A filled in diamond in the line connecting the
classes represents a composite aggregation:
138
Class Diagrams
A simple aggregation is a more general type of relationship where one of the classes simply has more
responsibility than the other class. A diamond that is not filled on the line connecting the classes represents
a simple aggregation:
Class 1 Class 2
Class hierarchies can be represented as composite aggregations, since the each class in the hierarchy is
dependent on the class above it. In Visual Basic we will deal primarily with composite classes.
When building our class diagrams, we look through our use cases and sequence diagrams to find the
properties, methods and relationships between classes. Normally, we should find these properties from our
uses cases, and the methods from our sequence diagrams.
Strictly speaking Business Rules are not part of the UML standard. They do, however,
complement UML by helping us get the most out of our UML models. Before we can use
Business Rules to our advantage we need to find out a bit about them.
Business Rules
Generally speaking, Business Rules are any rules that place restrictions on a use case, or have an effect on
the outcome of a use case. There are five types of Business Rules:
Requirement rules
Definition rules
Factual rules
Constraint rules
Deviation rules
139
VB6 UML
Example: "Customers are billed on the last day of the month." This rule would be derived from several
other rules, such as: "customer's bills include new charges and unpaid balances, calculation of customer's
unpaid balance, calculation of unpaid charges, etc."
Example: The customer has a customer billing address where the customer prefers all billing
correspondence to be sent. It can be different from the shipping address.
In this example the "customer" is the business term and the "customer billing address" is the attribute.
Format: Every (Each) <business term> <relationship to> <another business term>
Example: "Every ordered item is a product" or "Every customer has a billing address."
Example: "A customer's state must be one of the 50 states" or "The state name must be two letters".
140
Class Diagrams
Definition and Fact Business Rules can be used to determine the properties of our
objects, and which objects contain other objects, i.e. object hierarchies.
Using Business Rules with our use cases will greatly simplify coding. It's important to make sure that all of
the Fact, Constraint and Derivation Business Rules are implemented in our code.
The easiest way to do this is to put a comment into the code with the name of the Business
Rule that the code is based on. We can later do a search on the code for every one of these
rules; if we do not find a reference to the Business Rule in our comments, then we know we
forgot to implement a rule.
Since we are now creating class diagrams, we can associate the Business Rules with the properties and
methods of our classes. In this way, we will know exactly what rules apply to each class and where they
should be placed. A significant portion of the code we will need to write for our objects will be defined by
these rules.
The Business Rules will determine the correct values for the properties, what properties an object will have,
and methods of deriving the information needed by the class.
The methods of our components, and how our components communicate with the rest of the system, will be
determined by sequence and/or collaboration diagrams. Hopefully, you're beginning to see how UML
diagrams can be used to carefully map out how we will build our objects and how we will write our code.
So let's get straight on now and develop some Business Rules for our Create New Customer use case.
Each component in our system should have a Definition Business Rule that defines it. From this Definition
Business Rule, we can get the properties of the object that will be an instance of this component.
141
VB6 UML
Our first port of call, therefore, is a Definition Rule. We need to find out exactly what constitutes a
customer. Because this is the first Business Rule that we've encountered, I am going to show you what it
looks like and then explain each section individually.
Overview
Rule Type
Definition
Rule
CustomerID
Region
CompanyName
ContactTitle
ContactName
Address
City
PostalCode
Country
Phone
Fax
142
Class Diagrams
Required
Yes
Default Values
None
Customer Fields
Let's go through each of these sections in turn, now, and follow the analysis through so that we are quite
clear about how we built this Business Rule.
We are interested in the information associated with a Customer. So let's think back to the interview and
the use case we created from it. We know that when a Customer telephones in, the Sales Representative
asks for their Name, Address, City and other pertinent details. Therefore a Customer has these attributes
(as listed in Customer Business Rule):
Rule
Region
CompanyName
ContactTitle
ContactName
Address
City
143
VB6 UML
PostalCode
Country
Phone
Fax
We also know that each Customer has a unique identifying name, the CustomerID, so we've also included
that in our Rule section:
Rule
CustomerID
Region
Company Name
etc.
If we refer back to the information that the Customer is asked for by the Sales
Representative, we'll notice that we've already covered most of the elements in this
Customer Business rule.
Required
Yes
It's worth noticing, by the way, that the Required section only features in the definition type of Business
Rule.
Customer Fields
We also know that Customers have orders associated with them. Entering these orders is, after all, the
primary purpose of the Sales Representative. This is represented in our Associated Rules by the Customer
Can Place Orders rule:
144
Class Diagrams
Customer Fields
Customer Can Place Orders
Finally, one other thing we know is that there are certain restrictions for the Customer. For example, we
will only allow a Customer's details to be added to the system if they don't already exist in the sytem. This
is represented in our Associated Rules by the Customer in System rule.
Now you might be thinking to yourself: "I already knew we needed an Order object. I didn't need a
Business Rule to tell me what I already know." To some extent, that is true. We could get this information
directly from the interview. However, Business Rules provide a logical way of extracting such information.
Without these rules we are much more likely to miss something important. As we encounter different type
of Business Rules that serve different purposes, you will come to realize just how useful they are.
Customer Fields
Customer in the System
Customer Can Place Orders
Let's take the Customer Fields rule first. We know, from our earlier analysis, that there is a restriction
placed on each of the customer fields regarding its format. Therefore, our rule should be a Constraint Rule.
By considering all of the customer fields in turn, we would arrive at a Business Rule that looked something
like this:
Overview
These are the conditions for the fields associated with a Customer.
145
VB6 UML
Rule Type
Constraint
Rule
CustomerID will be a unique five character string based on the customer name.
ContactTitle will be a string less than 30 Characters long, acceptable values are:
Accounting manager
Marketing Assistant
Order Administrator
Owner
Owner/Marketing Assistant
Sales Agent
Sales Associate
Sales Manager
Sales Representative
146
Class Diagrams
None.
This Customer Fields Business Rule might look good, but it has some problems. It's actually made up of
several smaller Business Rules! There should actually be a Definition Business Rule for each field giving
details of that field.
While this may seem like a lot of work for very little gain, looking at the Northwind database
you will discover that Customers are companies, not people! We need to be rather careful,
and Definition rules will help us to focus on anomalies like this.
The identifying field for a Customer is CompanyName. Having a Definition Business Rule for
CompanyName will make it easy for everyone to understand this. Once all the smaller Business Rules are
created, we'll then be able to refer to them in our (rewritten) Customer Fields Business Rule, and make
Customer Fields itself a Requirement Business Rule rather than a Constraint rule. Our Customer Fields
Business Rule then looks like this:
Overview
These are the conditions for the fields associated with a Customer.
Rule Type
Requirement
Rule
The fields associated with a customer will be constrained as described by the rules listed
in the Associated Business Rules section.
147
VB6 UML
Customer CustomerID
Customer Region
Customer CompanyName
Customer ContactTitle
Customer ContactName
Customer Address
Customer City
Customer PostalCode
Customer Country
Customer Phone
Customer Fax
These Associated Business Rules would all be of the same form. This allows us to focus on the process
itself for the purposes of this book – so I'd like to show you just one of these for the moment, the Customer
Customer ID Business Rule:
Overview
Rule Type
Definition
148
Class Diagrams
Rule
Required
Yes
Default Values
None
Valid Values
Attribute Class
Customer
None
As you can see – nothing very surprising here: we've simply created a Definition Business Rule that defines
the valid values of the Customer name. The key point about this Rule is how it fits together with our
Customer Fields rule of course.
Once again, we've seen here that creating UML diagrams is an iterative process. We began
with our initial ideas, refined them, expanded them, and corrected them. We've taken our
original Customer Fields Business Rule, thought about things, and developed a new set of
Business Rules from it.
We'll now examine the other two rules associated with our original Customer Definition Business Rule
Customers in System
Customer Can Place Orders
149
VB6 UML
Here are the Business Rules that I derived for these rules:
Overview
This Business Rule limits the number of times a customer can be listed in the system.
Rule Type
Constraint
Rule
Class
Customer
None
This Business Rule is quite simple; but as I mentioned earlier, we need to ensure that code is written to
implement it – it could be catastrophic to underestimate how important this rule would be to our system!
We now look at the Customer Can Place Orders rule, and its associated Orders Relationship rule:
Overview
Rule type
Fact
150
Class Diagrams
Rule
Orders Relationship
Overview
This Business Rule shows the relationship between a customer and an order
Rule Type
Fact
Rule
Every customer has at least one Order that is in progress, complete or canceled.
None
The Orders relationship provides us with something new. It does not give us an attribute of the Customer
object, but it does intimate how the Order and Customer objects are related.
Looking at the rule: Every Customer has at least one Order that is either in progress, complete or
cancelled, this means that without an order the Customer would not exist:
151
VB6 UML
Customer Orders
So we can see how useful Business Rules can be. They help us to extract the properties of our objects in a
logical and consistent manner. They also help us to identify the associations between our classes.
There are many Business Rules associated with the use cases and our objects. You should try to create those
that are the most appropriate to your project. This is not always straightforward and, as with so many
aspects of project design, this will be an iterative process.
Deriving these methods will lead naturally to a discussion of class hierarchies. For the moment, I'd like us
to concentrate on our class diagrams. So let's put together everything that we've learned so far, and create a
class diagram for everything we know about the system up to this point.
If we apply the same analysis that we used to derive our Customer class diagram, to then derive a class
diagram for the whole system, we should find ourselves having arrived at a diagram rather like this:
152
Class Diagrams
In this, our system class diagram, we've have a very detailed explanation of the relationships between all of
the classes in our system as we understand it so far.
We've posited all the objects we've been talking about, identified their relevant properties, and drawn
appropriate associations between them. Let's explore this diagram in a little more detail now though: you're
bound to have seen some new features in this diagram that we haven't talked about yet.
153
VB6 UML
Take a look at the roles between the OrderDetails class and Orders class. The arrow pointing from the
Order class to the OrderDetails class says: has which means that the Order object will have an Order
Details object associated with it. The arrow pointing from the Order Details object to the Order object
says: is part of showing that the OrderDetails is part of the Order. This is a composite aggregation,
which is represented by the filled in diamond attached to the Orders Class.
Cardinality
When we're using an association we can display the cardinality between the classes. Cardinality shows
how many of one class is associated with another class. Cardinality can have the following values:
One class is associated with zero or more of the other classes. This is the default and is not specified (so
there's nothing to add to the diagram).
One class is associated with one or more of the other classes. This is represented by a 1+.
One class is associated with either zero or one of the other classes. This is represented by an open circle.
One class is associated with exactly one of the other classes. This is represented by a closed circle.
There are other cardinal symbols, but these are the most common.
Now take a look at the association between the Orders object and the Employees object. We can see that
there is a filled in circle next to the Orders object. This means that the Orders object is associated with
exactly one Employees object. There is nothing next to the Employees object, however. This means that
the default cardinality is in place here, and that the Employee object is associated with none or more Order
objects.
154
Class Diagrams
A class diagram is a very powerful tool, giving us deep insight into the relationships
between the classes in our system.
Remember, however, that class diagrams are built from all the other UML diagrams that we've created
previously. As we continue using UML diagrams to design our system, we'll be adding more information
into our system class diagram.
In a sequence diagram, one object interacts with another by sending it a message. For instance, in the
Create New Customer sequence diagram, the Interface object can send a Save Customer message to the
Customer object. From that detail, it becomes fairly clear that we're going to need a SaveCustomer()
method. The question is, which class should provide this method, the Interface or the Customer?
There's no hard and fast rule for deciding this. One great technique we can use here
though, is to let the class with the most knowledge about a particular task handle
that task.
So for the SaveCustomer() task, which class knows most about the customer? It has to be the
Customer class! Now I'll grant that this particular example wasn't very difficult. Sometimes, we will find it
harder to make this type of decision in more complicated situations, but it's always a useful technique to
apply.
As you may have guessed, deriving class methods is no small topic. It requires good
understanding of object-oriented analysis and design. You will also need to understand
Design Patterns. An excellent book on that subject is 'Design Patterns – Elements of
Reusable Object-Oriented Software' by Erich Gamma, et al. (Addison-Wesley).
In this chapter, we'll be using one of the common approaches for deriving class methods from interaction
diagrams. This approach runs as follows:
For example, the Customer class should implement a SaveCustomer() method, and an Interface
object should then send the Save Customer message to a Customer object by calling its
SaveCustomer() method.
155
VB6 UML
Now that we've established some of the theory behind deriving class methods, it's time to put this into
practice.
In Chapter 4, we went on to convert these use cases into the following sequence diagrams. Let's see what
each of these diagrams can tell us about class methods. Here's our Create New Customer sequence
diagram:
From this diagram, we can see that the Customer object must be able to take a request to Add a New
Customer, be able to Return (a New) Customer, and Save (a New) Customer.
You'll notice that I'm presenting less-detailed versions of our sequence diagrams. If you
remember back to Chapter 4, we discussed how it's useful to be able to increase and
decrease the detail in these diagrams to suit our needs. This less-detailed view will suit our
needs for deriving class methods.
156
Class Diagrams
From this sequence diagram, we see that our Customer object must be able to take a Request (for an)
Existing Customer, be able to return an Existing Customer, take a Request to Edit a Customer, and
take a Request to Save a Customer. Returning an existing Customer means we also have to be able to
move through Customer records and retrieve a customer by their ID or name.
157
VB6 UML
This sequence diagram shows that our Customer object must also Delete a Customer.
We can now map all of these responsibilities of our Customer object to methods for the Customer object.
Here's a table that presents the information we've just gathered from our sequence diagrams:
Task Method
Add a New Customer AddNew
Return a New Customer AddNew
Save a New Customer Save
Request an Existing Customer Move Previous, Move Next, Move First, Move Last, Move Item
(because we need the ability to select a customer)
Delete a Customer Delete
Edit a Customer Edit
Save Edit Save
Now we've derived these methods, we're ready to add them to the Customer class diagram that we started
earlier - we've established the properties for this class diagram, so let's go ahead now and add the methods:
When we previously looked at the sequence diagrams for our components, we noticed that most of these
components required these same methods. Since these methods were the same for several components, we
showed that they formed a pattern. To demonstrate this pattern, we'll now look at the methods of the
Product class, create our Product class diagram, and see what emerges.
158
Class Diagrams
From the Create New Product diagram we can see that it will require an Add New Product and a Save
Product method.
159
VB6 UML
From this sequence diagram, we see that our Product object must be able to take a Request for an
Existing Product, be able to return data on an Existing Product, Edit an Existing Product, and Save a
Product.
From this diagram, we find that we also require a Delete Product method.
From this brief look at the Product sequence diagrams, we can see that the Product object will possess
many of the same methods as the Customer object. Here's a table of the methods we've derived for the
Product sequence:
Task Method
Add a New Product AddNew
Return a New Product AddNew
Save a New Product Save
Request an Existing Product Move Previous, Move Next, Move First, Move Last, Move Item
(because we need the ability to select a Product)
Delete a Product Delete
Edit a Product Edit
Save Edit Save
Now we've derived these methods, we're ready to add them to the Product class diagram. We didn't
develop the Product class diagram step-by-step earlier, but we did draw up a version of it in our Northwind
system class diagram. Here's the final Product class diagram:
160
Class Diagrams
Now we've completed our Customer and Product class diagrams, we'll be able to compare them both and
analyze the pattern between them that's emerging.
As you can see, we've simply put all of the common methods between our Customer and Product objects
into the Managing Class.
161
VB6 UML
The Managing Class for the Product component will be nearly identical to the component we'll develop for
the Customer component. This will be true for many of our components. The different components will
share some common methods in the Managing Class, but the properties in the Managed Class will be
different for each component (such as Customer and Product, for instance).
Using class diagrams, we have managed to precisely define the pattern that's been
emerging from our sequence diagrams, and create a documented class coding
strategy for when we come to develop our actual project.
Summary
We've covered a lot of ground in this chapter. Not only have we introduced class diagrams, but we've also
turned our attention to Business Rules and the emergent patterns that we noticed in our sequence diagrams
some time ago. By developing our class diagrams like this, we've managed to take our design of the
Northwind project a lot further, defining classes and strategies to take advantage of patterns. This will have
real benefits when we come to develop our Visual Basic project.
Class diagrams can be considered as the natural progression from our use case and sequence diagram
models. They are the next stage of the UML Process. In this chapter, we were able to use the information
from use cases and sequence diagrams to help us create the class model diagrams.
Our UML diagrams (use case, sequence and class diagrams) have helped us to get a great deal of
information about our Customer and Product components. From the information we've gathered so far
from the UML Process, we'll be able to move on now to actually prototype a GUI (graphical user interface)
for our system in the next chapter.
162
Prototyping the GUI
In Chapters 3 and 4 we learnt about use cases and sequence diagrams. These UML models help us to
understand the user requirements, to visualize the sequence of events that occurs during an interaction, and
to find patterns common to these interactions. Another important role that these models serve is to help us
to build a graphical user interface.
Once our use cases and sequence diagrams are complete, we have sufficient information to make a general
GUI prototype. By creating a GUI prototype, which can be shown to the user before the final release of the
application, we can make sure that it fulfills all of the user's requirements.
See what problems arise when the GUI is not based on the users' needs
See how our UML diagrams can help us create a more logical, user friendly design
Implement this design to build a better GUI prototype for our Order Entry application
I won't show you how to actually build the prototype in this chapter, as there is little implementation we can
actually perform behind the scenes at this point. In Chapter 14, we'll build the final GUI design.
Form Control
UML use cases and sequence diagrams will allow us to follow one of the most important standards of
Visual Basic, maintaining form control. Form control should be used to keep the tasks the user has to
perform as simple as possible.
I'm sure you've seen applications where there is a multitude of forms to complete a single task and the poor
user has to constantly jump back and forth from one form to another in order to complete a single task.
VB6 UML
Those sort of applications usually come about when the developers have designed the forms around a
database rather than what the user actually has to do. For example, a project designed around a database
that has a Customer, Employee and Order table would have forms to maintain each of these tables. If a
user wanted to update a customer while entering an order, they would have to leave the Order Entry form
and go to the Customer form. Once the customer had been entered into the system, the user would then
have to return to the Order Entry form and refresh that form.
Clearly, this is an overly complex, labor-intensive process. There must be a better solution.
And of course, there is a better solution. It is only common sense that the forms should be based on the
roles the system's users perform and what each of these roles entails. Use cases can provide us with the
actors, which represent the different roles, and also a description of what each of the actors does in that
particular role. Sequence diagrams provide a picture of how each of these tasks is performed and the order
of events.
It's for this reason that we will build our GUI prototype based on the actors we have created and the tasks
that they perform. In this way we will produce task-oriented forms.
By a task, I mean a series of events as described by one or more use cases. It's
possible that one task will contain several other smaller tasks. For example, we
know that Order Entry contains the Add and Modify Customer tasks.
As I mentioned earlier, the purpose of this diagram is to help us visualize the communication
between our components. We could make a more detailed diagram showing all of these
possibilities in full, but for the moment we should just be trying to get an overview of the
process of making an order. Editing and creating a customer are not important to our
overview of creating an order. For these reasons, we should leave these possibilities out of
the diagram.
166
Prototyping the GUI
The Customer component then passes the CustomerID onto the Order object, as this object will need to
know which customer this Order belongs to. The Order object will also need to create an OrderID. Once
the Customer object has returned the details about a particular customer, the Sales Representative requests
the user interface to get products that belong to a certain category. The Interface passes this message along
to the Product component, which returns the products to the user. Let's take a look and see how our
sequence diagram is coming along:
:Order
:Sales Rep :Interface :Customer :Products :Order
Details
Request Customer
Request Customer
Create
Return Customer Add/Edit OrderID
Customer
Request Customer
Get Products by Category
Return products
The Sales Representative will then choose a particular product and add it to the Order Detail component.
The Sales Representative can then select more products from this category or make another request for
products from a different one. The Sales Representative will repeat this process until they have completed
the Order that the customer desires. This iterative process is shown by a dashed line at the Interface object.
Once the Sales Representative has finished, they can tell the Interface to save the order. This request is
passed to the Order component and the Order Details components, which will then add this new record to
the disconnected recordset and send the recordset to the middle tier. From here, it is passed to the database
so that the database can be updated. As this final part of the operation is outside the realm of our client
objects, it will not be shown our diagram:
167
VB6 UML
:Order
Sales Rep :Interface :Customer :Products Details :Order
Request Customer
Request Customer
Create
Return Customer Add/Edit OrderID
Customer
Request Customer
Get Products by Category
Return products
Add Product
Add product
Save Order
Save Order
Save Order
168
Prototyping the GUI
At a glance, this prototype seems quite reasonable. It appears to have all the basic elements. There's a drop
down box for choosing the categories and a grid to choose the particular product from. We can add new
products to the order with the Add to Order button. There's a section for the order details such as the
OrderID and the order date, and essentially there is an identifier for the customer in the Bill To drop down
box. Our IES developer thinks that this is a pretty good-looking form.
However, this Order Entry prototype would never get to the client because it has some serious flaws. Let's
step back and look at this interface with our Create Order use case and sequence diagram in mind.
We now know that adding and editing customers is a subtask of entering an order. Do you see any way of
adding or editing a customer on this form? This form only has the Bill To section to select a customer.
There is simply no way to add or edit a customer.
I call this user-abusive rather than user-friendly. Clearly the developer is not always the best person to
create a GUI. It should be the users that dictate its design.
Since the customer information is only modified or added while adding an order, it would make sense to
add and modify customers on the Order Entry form. Now a customer can be added or edited without
interrupting the flow of the order by moving in and out of several forms. This part of the GUI could look
like this:
169
VB6 UML
Everything that the Sales Representative needs to carry out this initial part of her task is available to her on
this form. There is no need to swap between forms to complete this part of the task.
The next part of the Order Entry task is to pick out the products that the customer wants and to add them to
the order. This sub-task is quite distinct from taking the customer details and so should occupy a separate
part of the form. It could looks as follows:
Using Tabs
We have chosen to use tabs in this section. Using tabs on a form can prevent the form from becoming
overcrowded and difficult to follow. Here, our user needs to check information about a product, such as
cost and availability. So there is a tab to display this general information about the product. Sometimes,
particularly when new products have been entered into the system the Sales Representative is not sure to
which category the products belong. Therefore we have provided a tab to perform a product search. For
most of the time this option will not be used and the tab would not be selected. When it is needed it can be
pulled up quickly with a single click of the tab.
The alternative is to open a special product form that allows the user to search for a product. The Sales
Representative would then have to close this form when they had finished and return to the Order Entry
form. If the user needs to do this a number of times while taking an order, the solution would involve
moving back and forth between the forms a lot of times. This doesn't seem very efficient.
We could improve the situation by linking the product in the Product Search form to the
Order Entry form. Now we can open the Product Search form, find the product we want
and choose whether or not to add it to the current order. While this is not a bad solution, it
is still easier to click on a tab in the Order Entry form.
The final part of the Order Entry task is to select a shipper, check the order details with the customer and
that they find the total price for the order acceptable and then to cancel the order or go ahead and save it.
This information should therefore be grouped together and the final part of the form would probably look as
follows:
170
Prototyping the GUI
The Sales Representative can spend the entire day using this one form and never have to
move away from it.
Most of the information we used to create this GUI came from the use case Create New Order. However,
there is at least one aspect of the GUI that follows from the sequence diagram. We have learnt that
sequence diagrams place a temporal emphasis on an interaction.
They give us the order in which events occur. Bearing that in mind, take another look at the sequence
diagram that we created earlier. You can see that the user first selects a customer, then a category and then
a product. Once the product is selected, the user enters the required quantity of that product. If you look at
the form it is arranged in this order from the top down:
171
VB6 UML
This allows the user to easily perform the task without having to move all over the form. We are not only
using our UML diagrams to keep a user within a single form to perform a single task, but we are also using
our diagrams to arrange the sections on the form in the order the user is going to use them.
To address this issue, let's imagine that there is another use case called Review All Customers performed
by the Sales Manager actor. Part of this Review All Customers use case is to check if a customer's
information is correct. If the information is incorrect, then the Sales Manager has to modify the customer
record. If the only place to modify customer information were on the Order Entry form, the Sales Manager
would have to perform this task in the Order Entry form, which would make no sense, as this task has
nothing to do with Order Entry.
We could make a separate form that is specifically for Customer Review. While this may seem redundant,
as there are now two places to edit information, there are advantages to doing this. What if the Sales
Manager needed to look at a customer's past sales history, balance information, and products purchased as
part of the customer review? Now it makes sense to have one individual form to handle customer review.
Most likely the top half of the form will have customer information on it, the bottom half will be a tab
control: one tab for sales history, a second for balance information, and a third for products purchased:
172
Prototyping the GUI
Security
An application that has different actors with different responsibilities will usually have a log in screen and
some form of security. If our application employs the Review All Customers use case, the application
should have security added and only allow a person logged in as a Sales Manager to view the Customer
Review form. The Customer Review form would not be accessible to a Sales Representative.
The Sales Representative has one main role, entering orders. There will only be one form she should have
access to, the Order Entry form. The Sales Representative would come in at the beginning of the day and
log in as a Sales Representative. The application would go directly to the Order Entry form, as this is the
only form a person in this role can access.
As for our Sales Manager, when he logs in there will either be special forms related to his role, such as the
Customer Review form, or one form which will provide different functionality for different users, such as
the Order Entry form.
Internet Applications
There is one other advantage to this single user interface style of programming. Building Internet and
intranet applications is becoming more and more common. While we do have the ability to create dynamic
web pages that can build new pages on the fly, these only work with certain browsers. Users are usually not
too happy when they are constantly waiting for new pages to load. In this case, the usual standard is to
minimize the number of pages and try to complete a single task on one page. Internet solutions seem to
work best with a single user interface such as the one we have built above.
If we wanted to use an Internet browser to run this application on an intranet, we would not have to make
any major changes in the design of my user interface. As this application is being built from the three-tier
model, moving the entire project to an intranet solution would be fairly easy.
We must build our applications with a focus not only on current needs but also on future
needs. The technology is definitely moving toward building Internet and intranet solutions,
and our Visual Basic applications should be able to adapt to these changes.
This approach does not mean that two different actors may not use the same forms.
In our example, the Sales Representative is allowed to create an order, but not to delete or modify them.
The Sales Representative is also allowed to create and modify customers, but not delete them. The Sales
Manager, on the other hand, can modify and delete the orders and delete customers. It makes sense that the
Sales Manager uses the same form to edit and delete orders as the Sales Representative uses to create them,
as both of them will require the same information to perform their task, i.e. order details and customer
information.
173
VB6 UML
Even though they are using the same form, the form may present itself differently to the two actors. When a
Sales Representative views the Order Entry form, they would only have the ability to add a new order, and
edit or add a customer. The buttons to delete or modify an order or delete a customer will be invisible to the
Sales Representative. However, when a Sales Manager logs into the system and accesses the same form,
they will have the command buttons allowing them to edit and delete orders, and delete customers - as these
are tasks this actor can do.
Thus, we have one form that performs several different tasks for two different actors, but this form is
designed to perform these tasks in the most efficient manner. This one form will add, delete, and modify
both customers and orders.
With this type of form there is always the possibility that it will become quite complex. This is something
to take account of when deciding the best way to create the GUI for a particular application.
Although this choice may be guided by the amount of coding that it requires, the main criterion when
creating forms should be the user requirements.
Overview
Primary Actor
Sales Manager
Secondary Actor
Starting Point
174
Prototyping the GUI
Ending Point
Measurable Result
A customer is deleted.
Flow of Events
This use case is started when the actor requests to review an existing customer. The actor
then requests to delete the customer information.
If there are outstanding unfilled customer orders or debts for the customer the actor will
be advised of this by the application and the delete will not be allowed. If there are no
outstanding orders or debts the actor is prompted to accept or cancel the operation. If the
actor accepts the operation, the customer is deleted from the system and the customer list
is updated.
The actor tries to cancel a customer who has an outstanding order or debt. The customer
delete will be canceled.
None
Outstanding Issues
None
175
VB6 UML
This use case for deleting a customer shows that the Sales Manager is the actor for this task. As this task
requires a review of the customer's billing history, sales history, and current balance information, this
should also be performed on a separate form that has a tab control on the bottom showing views of the
information required to perform this task.
What I am saying is that a use case represents a task being performed by a user; it has a beginning and an
end. Once a user begins a use case, they must follow it through to the end - remember that use cases always
have cancel as one of the options for the end point. If that task requires three windows to be open at once,
fine, but the user is not allowed to move on to perform another unrelated task until the current one is
complete. This allows the user to focus on one task at a time and prevents the user from being in the middle
of a task and forgetting to complete it.
If one use case ends up using a large number of other use cases, it may be worthwhile to review the system
and see if the use case can be broken up. There is no magic number of use cases. There is no amount that is
too small or too large. Every system is unique.
Summary
In this chapter, we've explored the best ways to create a GUI prototype. Although there may be other
considerations that govern the GUI design, the most important factor should always be the requirements of
the user. Once a prototype has been made it must be shown to the relevant users. They can then review it
and the necessary changes can be made.
Later, when we build activity diagrams that will map out the sequence of events that will occur in each
section, we'll be able to order the section according to how the user enters information into that section. For
example, we can create an activity diagram showing the detailed set of steps the user takes to enter the
customer information. Using this information, we can further optimize the customer section on this form by
putting the customer textboxes in the order that the user normally will enter the customer information.
That is all in the future though. We have already covered a great deal in these first chapters of the book. In
the next chapter, we'll perform an important review of the stages of the UML design process that we've
been through so far, and see exactly where we're at and how we're going to move into the development
stage of our Northwind project.
176
External and Internal Factors
At this point in our design process, we've arrived at a critical junction. You may remember how I said that
the design of our project would be influenced by two sets of considerations:
External factors
Internal factors
The source of these external considerations has been the user requirements, which we began to draw up as
soon as we interviewed some of the employees at Northwind. The internal considerations will be
determined by the technologies that we choose to use. In this chapter, we will take stock of the UML
development so far, and ready ourselves for the coming together of external and internal considerations for
our Northwind project.
In fact, we are at the junction in our design process between user requirement analysis and
technology framework analysis.
The purpose of the internal design of the project was to fulfill the external requirements of the users. Put
more simply, we know what we want our project to do; and we must now discover how to make it do it.
Although designing the user requirements and designing the internal aspects of our system are quite
different activities, there will of course be some common ground between them:
The design of user requirements and the design of internal system requirements can both be
made easier with the use of appropriate UML models.
VB6 UML
In addition, both internal and external design activities are iterative processes. Just as we found the defining
the user requirements was not a one-step procedure, we shall soon see, over the next few chapters, that
internal design is far from a smooth ride. The iterative process will be paramount to finding the best way to
build out project.
Use Cases
The starting point for the external design should always be the use case model. To successfully conduct this
stage of the UML process, it is always necessary to find a representative group of users.
We should be able to find a representative group of users by talking to someone in management. From the
list of employees they give us, we then need to interview each person to find out exactly what they do.
From these interviews, we may find that some of these people perform identical tasks and may be
considered to be one user. We are also likely to find that many other tasks exist and that we have many
more users than we originally thought.
This is the first evidence that our design process will be an iterative affair.
We can then turn these interviews into a set of use cases, which are a verbal description of the tasks that
each user or actor will perform. We can also make use case diagrams, which show the relationships
between actors and use cases.
At this point, the use cases and use case diagrams should be shown to the users so that they can review
them. It is very unlikely that they will contain all the right information first time round. They can then be
amended as appropriate.
Interaction Diagrams
The next step is to build interaction diagrams. These can be sequence or collaboration diagrams.
Sequence and collaboration diagrams represent essentially the same information. However:
For Visual Basic projects, we found that sequence diagrams are more appropriate - since they better
represent the particular steps the user will follow.
Sequence diagrams are made by transforming the use cases from a verbal description into a pictorial
representation. These diagrams show the communication between objects that must take place for a task to
be accomplished, as well as the steps that the actor actually performs during these tasks.
Once again, these sequence diagrams must be reviewed and any amendments made. It is quite possible that
at this stage we may find that the use cases also need to change.
180
External and Internal Factors
Activity Diagrams
From our use cases and sequence diagrams, we then go on to make the class diagrams. A class diagram
shows a collection of classes and how they are related to one another. It offers a simple static view of the
classes of a system.
Class diagrams also show the methods and properties of the classes. The properties are derived from the use
cases via business rules, while the methods can be found from sequence diagrams. Once again it will be
necessary to review these diagrams to ensure that we know everything about the user requirement that we
need.
GUI Prototype
Finally, we can consider a GUI prototype. This is how we build a GUI prototype from the UML analysis
thus far:
The essential elements that the GUI should contain will depend on the tasks performed by an actor, and
are therefore found from use cases.
The order in which these elements appear on the form will be influenced by the order that the actor
would perform them in, and are therefore derived from the sequence diagrams.
Building a prototype GUI allows us to show it to the potential users. After such a review, alterations can be
made.
It would be great if we could have started from the very beginning with the perfect design.
Unfortunately, this is not realistic. Working through much iteration is a natural process of
designing any system. If we accept the fact that a project’s design will go through many
refinements, we can quickly see the advantage of using UML to design our project: diagrams
are easy to change.
UML diagrams give us a very powerful tool that allows us to map out the design of the project. These
diagrams give us a deeper insight into how the system will work. They will help us to find all the
components our system will require to perform its tasks. When the UML diagrams are shown to the user, he
or she can understand the diagrams and recommend improvements, refinements and changes. These changes
can be made with little effort.
If, on the other hand, we do not use UML diagrams to design our project - but instead just dive in and start
coding, we will need to change our code with each refinement of the design. At this point, we would have
to ditch the original code and start again. This is time consuming but will work. We could also try to revise
the original project by chopping out the bad code, adding new code and trying to revise the original project
to reflect the new design. This method, though very common, will always result in code that is
unmanageable and usually filled with bugs.
Hopefully, you're beginning to see how UML diagrams make our lives much easier as a developer.
181
VB6 UML
A few of the items that the user wants will be related to how the internal system will work. The user wants
our system to be able to operate over an intranet. To make this happen, the user effectively does not want a
continuous connection to the database. Instead, the user needs the information to be coming from a client
side component. Looking back at our sequence diagrams for adding or modifying a customer, we can see
that a request for a customer record only goes to the client customer component. The request for the record
is not passed back to the server component. There has been no discussion of how this will actually be done;
we have been strictly talking about how the user wants the system to work.
The second part of our design phase will center on finding out how we can perform the tasks that the user
requires. It is quite possible that what the user wants is simply impossible. Hopefully, we will know this
when the user makes the request, but sometimes things seem possible at first but then turn out to be
impossible, difficult, or inefficient.
What Next?
The rest of the design phase will focus on how the system can perform the user-required tasks. Just as we
had to go through a series of steps to find out what these tasks were, we will also need to go through a well-
defined series of steps to find out how the system can perform these tasks.
There is only one realistic way to find the best way to perform the different tasks required by the system:
build small test projects. These test projects will find the best techniques to build the most critical portions
of our system.
If we think about our Order Entry application, we have already said that we're going to use disconnected
ADO recordsets through RDS. What we have not said is how we are going to do this!
The solution to this and similar questions will come from building a test project using ADO and RDS. This
project will simulate our final project, performing the same critical tasks that our system will perform. The
test project will find the best way of performing these tasks.
182
External and Internal Factors
For every message, we must ask, “How will our method perform this task?" This
now becomes the critical question.
Wouldn't it be great if we could make a diagram to answer this question for every method? Well, guess
what? UML has a model specifically designed to turn these steps into a diagram. This diagram is the UML
activity diagram.
Activity diagrams will turn all of the 'how to' steps into a diagram that can show us the relationship between
each step. Activity diagrams show each sub-task that a method must perform to complete its required
overall task. Decision points will be easy to see, and we will be easy to understand the general flow of the
code.
Activity diagrams allow us to design the way a method will be coded, without writing a line of code. We
can easily create many different ways of performing a single task by making a different activity diagram for
each possible solution. When we have completed them, we can compare all of the activity diagrams and
find the best technique for coding the method.
I will supply a more detailed discussion of how to use activity diagrams, and the benefits of
using them, in Chapter 9.
Activity diagrams are very close to actually being code, but they still are not Visual Basic code. We still
have to make the final transition from the pseudo-code in our activity diagrams to real Visual Basic code.
This final transition, though, will be fairly simple. Every step has been mapped out: we just have to follow
the activity diagrams.
The first code we should produce, however, should not be for the final project - but for a test project. Each
system is unique. What works well in one environment may crash in a different environment. We test our
methods to see if our solution actually works, and how it can be improved. From these tests, we will find
out which technique really answers our question, "How do we perform this task?"
Using this information, we will go back and make a final set of activity diagrams that will reflect our final
design. Using these activity diagrams, we will make the final component.
As I said earlier, every step of the way the design process is iterative.
For the first stage of the design, we must keep refining our design based on the user requirements
In the second stage, we must refine our design based on the results of our test projects
The end result of this type of iterative approach is a well-polished design that has the maximum chance of
producing code that will work correctly and efficiently.
Now let's cross this junction and focus on answering the question, "How do we perform these tasks?" We'll
start by looking at the frameworks and technologies that we are going to use.
183
Frameworks and Technologies
So far, we've only really considered how our project design is shaped from the outside. If you recall from
previous discussions, this refers to the particular user requirements that we've been working on since we
interviewed the Northwind employees. However, this is only half the story, and before we begin to
implement our design we need to consider how our project is shaped from the inside; or more precisely,
how the technologies and frameworks we have chosen will impact on our design so far.
These technologies and frameworks are complex. Trying to understand them is difficult. Trying to work out
the best way to use them in your project is even more difficult. That is why this chapter will give a general
explanation of the frameworks and technologies that will be used in the second half of the book, when we
come to complete the design of our project and move into Visual Basic development.
As we meet these technologies or frameworks in the following chapters, we will build upon the general
information I present in this chapter, expanding and clarifying our understanding of them through more
detailed descriptions and code samples.
If you remember back to Chapter 2, we discussed frameworks and how to choose the correct frameworks
for our project.
Client-server architecture
The DNA architecture
ADO (ActiveX Data Objects)
RDS (Remote Data Services)
The Visual Basic Class Hierarchy framework
It may be that you are already familiar with the technologies that we'll be discussing here. If so you can
probably skip these descriptions, although you will probably want to read the sections on how the
technologies impact on our UML design so far.
We also made some preliminary decisions about some of the technologies and frameworks that we are
going to be using. We decided to build a three-tier system using ADO as our data access technology of
choice.
Now that we have a pretty good external design in place, we need to start thinking about how best to
implement it. This requires us to examine the technologies in more detail.
Client-Server Architecture
Basic client-server applications are built by creating a Visual Basic front-end on the client that keeps a
continuous connection to the database. The database on the back-end has stored procedures and views to
handle the database business logic and to limit the amount of information being returned to the client. As
many of you know, this client-server project has a few problems:
186
Frameworks and Technologies
This basic client-server relies on stored procedures. This makes the framework difficult to work, since
every database has its own way of building stored procedures and views. We would need to hire and
train developers in a wide range of SQL versions. SQL is a difficult language to work with, to debug,
and has many limitations.
In addition, applications that run over the Internet don't allow a client to maintain a continuous
connection to the database. Standard Internet applications allow the client to remain connected only long
enough to make a request to the web server and retrieve information. Once the web server has sent the
information to the client, the connection is severed. The client-server framework requires a continuous
connection to the database so this framework won't work over the Internet. As Northwind eventually
wants to add Internet capability to this project, the client-server framework is not a very good choice for
this project.
There is fairly new variation on the above framework, which would be ideal for our project, called DNA.
Let's take a look at the DNA framework now.
One layer is on the client, which is the interface that the user employs. The second layer comprises one or
more servers that contain the business logic, which deals with communicating all of the transactions
(editing, updating, adding and retrieving records) to the database.
Business logic is a set of rules that defines how the data will be presented, what type
of data is valid, how the value of something is calculated, etc. These rules are not all
the same and they don't all belong in the same place.
The third layer contains one or more databases. DNA therefore looks rather like this:
At first glance, it may not seem that we have gained too much. We have removed the transaction business
logic from the database's stored procedures, and put them into a component in the middle-tier. However,
take a closer look and you'll see that there are several important benefits to this approach.
187
VB6 UML
The Middle-Tier
Unlike a SQL stored procedure, a Visual Basic component on the middle-tier handles the transaction
business logic. It has code that is easy to read, write and debug and has very few limitations. One can also
create a single component that can work with a multitude of databases on different servers. A very powerful
component can be built that can even do the most complex manipulation and massaging of the data.
Extremely complex activities, which used to be done on the client in client-server applications, can now be
performed in the middle-tier. This makes the client smaller, lighter and easier to work over the Internet.
The middle-tier works as a middleman, taking requests from the client for a particular set of information
formatted according to a certain set of business rules. The middle-tier gets the data, transforms it according
to the business rules and returns the data to the client. The client no longer has to worry about database
connections, tables or transforming data. The client simply requests the information it needs and the middle
tier returns it. We now have a client that makes a request to the middle-tier for data, and once the client has
the data, it disconnects from the middle tier. This is how the Internet works, so it sounds like we are on the
right track.
We are now moving away from building one huge application that tries to do
everything and ends up doing nothing well. Instead, we are building components
that can be put together in different ways to build different applications.
We can arrange the components in one way for applications used by a company's internal workers; a
different arrangement for an application being used over the Internet; and a third way to build decision-
making applications for management. Building new applications will be easy and take little time. These
separate object-oriented applications will replace one huge application trying to perform many completely
different tasks, each with a different set of requirements.
Using DNA and OOP offers another advantage. If for some reason the business-logic changes, it is likely
only one, or perhaps two, components will have to be changed. This component should be in the middle-
tier, which means you will only have to upgrade the server. In a company where there are hundreds of
clients, this will mean simply updating one or a few servers rather than having to upgrade each individual
client.
DNA sounds really good: it is scalable, works with the Internet, works well with object-oriented
programming techniques, and allows a thin or fat client. Yet, unless we can come up with a good method to
implement this architecture, it is useless.
Now that we have a rough idea of the basic three-tier framework that we are going to be using, we need to
think about some of the technology frameworks that we are going to need to pull it off.
188
Frameworks and Technologies
ADO provides programmatic access to an underlying data access technology called OLE-DB. OLE-DB is a
defined set of interfaces that all data sources can implement through special drivers known as providers.
These data sources may be Access or SQL Server databases, but they might also be mail or video.
The ever-increasing role and importance of the Internet in application development has also driven the
design concepts of ADO. It provides a range of ways that remote data access can be achieved over the
Internet, using a Web browser or through custom applications written in a range of programming languages.
Prior to ADO, you would have used a variety of technologies to access the various data stores. RDO would
be used to access SQL Server, DAO would access your Microsoft Access database and if you wanted to
access data in Microsoft Excel, you'd have to use ODBC.
There are many downsides to this method. Technologies such as ODBC are difficult to use. Other
technologies such as RDO and DAO (which actually provide the user with another interface to ODBC) add
an extra layer of code to the application and performance can drop. An alternative access method was
needed.
OLE-DB is that alternative and is designed to be the successor to ODBC. OLE-DB provides access not only
to relational data sources such as databases, but also to non-relational data such as mail and text.
Additionally, we can use ADO to talk to data sources, via OLE-DB.
Using ADO
ADO is very simple to use. It equates well to DAO (Data Access Objects), where you create an object, and
call its methods and properties. ADO is quick and painless to use when programming, as data can be
retrieved from a data source with as little as one line of code.
189
VB6 UML
This has been achieved by 'flattening' the object model. In other words, although there is still an object
hierarchy, such as that of DAO or RDO, some of the lower level objects can exist on their own and the
higher level objects are built behind the scenes. This means that a programmer can use the object most
suitable for a particular task, without having to create a lot of objects that aren't really required in their
program. If ADO requires these objects then it creates them and uses them without the programmer needing
to know that they are there.
Connection
Command
Recordset
The object model in its strictest sense shows a hierarchical relationship between the Connection object
and the Command and Recordset objects, but these can exist independently of a Connection.
The Connection object is also where any errors that occurred during the ADO operation are stored. We
don't need to explicitly create Connection objects: we can simply pass a Connection String
directly to a Command or Recordset. However, creating a Connection object is worthwhile if you are
going to be getting data from the data source more than once, because the connection won't have to be
established each time.
190
Frameworks and Technologies
We use recordsets to examine and manipulate data from the data store. The Recordset object provides
the facilities to move about through the records, find records, sort records in a particular order and update
records.
Disconnected Recordsets
These allow the recordset to be disassociated from the server, and then re-associated later. A user can
update the disconnected recordset and save the changes locally. When it is reconnected later, the server can
be updated.
The use of client-side data manipulation also allows you to sort data, find records, and generally manage
recordsets without resorting to a trip back to the server. As you can see this idea primarily fits in with the
nature of the Web, but it can work just as well for the standard type of applications running on a LAN. So
you can see it is suited for both our thick and thin client models.
In summary, ADO is designed to access data from any data source (through special OLE-DB providers) and
you can use ADO from any programming or scripting language (as long as it supports ActiveX).
Applications that use ADO can also work in the disconnected and stateless environment of the Internet. All
this means that ADO is ideal for use as the method of data access in the Windows DNA architecture.
If you want to learn more about ADO, I recommend the 'ADO Programmer's Reference', also
published by Wrox Press.
A way to work around this problem was the use of the HyperText Transport Protocol (HTTP) to pass
information to a server and retrieve information from the server. The major advantage of this technique was
that the client only had to be configured to have an Internet or an intranet connection. Once the client was
set up to use HTTP, nothing else had to be done. Security, though, would now have to be dealt with inside
the component. Using the HTTP protocol shifted the responsibility of keeping the system secure to the
component. This was usually accomplished through log-in screens, UserIDs and passwords.
191
VB6 UML
Before RDS, making a connection to the server-object through HTTP was very complicated. To use the
HTTP protocol we needed to have a Web Server on the server. The client-side application had to pass
information to the server formatted for the HTTP protocol inside a query string. In order to pass this
formatted information to the Web Server the client had to use the WinInet APIs. When using an IIS Web
Server the following would happen when a request arrived at the server:
An Active Server Page would receive the request from the server and unformat the request
The Active Server Page would pass the request to an Active-X DLL server-object
The server object would retrieve data or perform a function
The server object would pass the information back to the ASP page
The Active Server Page would then pass the information back to the client
If this sounds very complicated, that's because it was. Passing information to a server object through HTTP
used to look as follows:
192
Frameworks and Technologies
While this technique made distributing the solution very easy, it made coding the client and server
components extremely complex. Information had to be formatted to the HTTP protocol and unformatted
back to regular text. This resulted in parsing routines in all of the components, and complicated code to use
the WinInet APIs.
Fortunately, Microsoft has offered us a much easier solution for connecting to IIS Web Servers: Remote
Data Services (RDS). RDS 2.0 makes connecting to an object on a Web Server simple. The connection to a
server object using RDS now looks as follows:
RDS will make a proxy of the server-object on the client. A proxy will be something that looks exactly like
the server-object, i.e. it will have the same public properties and methods as the server-object. As far as the
client application is concerned, the Proxy object is identical to the server-object. The client will make
requests to the Proxy object, and the RDS will pass this request to and from the server-object on the server.
This is called marshalling. When RDS uses HTTP, marshalling is done through an IIS Web Server.
The only disadvantage of using RDS with HTTP is that it requires an IIS Web Server. For connecting to
other operating systems, such as a UNIX system, we will still have to use the old work around. Fortunately
for us, we know our system will only be using a Windows platform so we don't have to worry about this.
Like our previous HTTP solution, RDS using HTTP allows users to access the database on any client with
an Internet connection. Another advantage of RDS is that it also allows the client to connect through
DCOM over an intranet, though this still requires a special configuration of the firewall on the client side.
When using RDS with HTTP the connection to the server may be made through a Domain Server Name
(DSN). Therefore, the client needs no knowledge of the server's actual address or, in fact, any information
on the server whatsoever. This allows the DSN address to act as a gateway that may actually represent any
number of actual Web Servers.
193
VB6 UML
ADO: This is the latest framework that Microsoft provides for connecting into any source of data. We've
already discussed ADO.
RDS DataSpace Object: RDS on the client is made from two objects: the DataSpace object and the
DataControl object. The DataSpace object allows you to access the methods of an ActiveX DLL on a
Server through the Internet using HTTP, COM, or DCOM. The DataSpace object has one method,
CreateObject(), which allows you to access an object on the server. There is one property,
InternetTimeOut, which is the amount of time allowed to make a connection to the server. On the
server, there is a corresponding DataFactory object that communicates with the data store.
Visual Basic ActiveX DLL: Although we don't have to have a business object, it is usually prudent to
separate the client from the business rules, especially those that deal mostly with database functions. You
can actually build your server components as a Visual Basic ActiveX DLL or as a Visual Basic IIS
application. The ActiveX DLL will give you a greater degree of control over who can access your data
over the Internet than an IIS application. IIS applications, though, give you more flexibility and perform
better when there are dozens of forms.
Microsoft Internet Information Server: This is required for RDS to connect to the server using the
HTTP protocol and the Internet. This must be an NT 4.0 Web Server with the Option Pack installed
(RDS is installed with the Option Pack by default).
Database: A database for which there is an OLE DB provider.
Client Machine: This computer must also be connected to the Internet - usually through an Internet
service provider (ISP). To deploy the client application, use the Visual Basic Package and Deployment
wizard, which will ensure that both ADO and RDS are installed on the client machine.
Security
We can secure an application at several different levels. The first is by building a password system into the
client form. A user must log in with a user name and password before getting access to the server-object.
The client object will have to save the user name and password in a private variable for subsequent reuse
with every transaction. This information cannot be saved in server-object as it is stateless, which we're just
about to discuss.
A higher level of security can be reached by using MTS. This will be described in a later section. Finally, if
the information you expect to send over the Internet requires security, such as passwords, you can create a
secure HTTP server that uses the HTTPS (Secure HTTP) protocol.
These passwords and IDs can be stored in a membership directory, like the one used in Microsoft Site
Server 3.0. Using this membership directory, the server component of an RDS application can verify if a
user is allowed access to the server component. In this way, we can build a security feature that is similar to
the typical Windows NT user security. For detailed information on membership directories see the Wrox
Book Site Server 3.0 Personalization and Membership.
194
Frameworks and Technologies
Stateless Transactions
When we build RDS applications using HTTP we must create a stateless project. This means that every call
the client application makes to the server-object is viewed as a new client. Afterwards, the server-object has
no memory of the client. This works the same way as an HTML page: using a browser you connect to a
server, which returns the page you are requesting. Once the data has been returned to the client, the server
and the browser have no knowledge of each other. Building a stateless project will mean that retrieving and
updating data from the database will have to be done in a completely different way from the standard client-
server project.
Therefore, we can see how simply choosing to use RDS is beginning to define how our project will be
constructed. Our server component will be an ActiveX DLL and we will have to design its behavior so that
it complies with the stateless model that RDS dictates.
As the names suggest, the Data Consumer uses data from an external source, while a
Data Provider is the source that supplies that data.
Data Providers have been around for some time. They are basically anything that provides data. For
example, any of the Data Control objects that you can drag off the Visual Basic toolbox and drop onto a
form act as a Data Provider. You can set the properties of the Data Provider so that it knows what database
and table to connect to, and then set a data source property on the control (such as a text box or grid
control) so that the control can get data from the Data Provider.
When the application is run, the data appears in the control. You can use the data source to move through
records and all of the controls bound to that data source refresh automatically. Data Providers allow you to
create running programs by setting properties and calling only a few methods, such as AddNew or Edit.
These data controls also have properties and methods that allow you to access the data in your code
directly, instead of binding a control to your Data Provider. This is a much simpler way to program.
195
VB6 UML
A Data Provider class will have a private recordset variable associated with it. A reference to this private
recordset variable is passed to the Data Consumer (a grid control, a text box, etc). The Data Consumer can
then retrieve data from the recordset, as well as update, edit, add and delete data from the recordset.
The recordset is passed from the Data Provider to the Data Consumer by an event called GetDataMember
that is automatically added to a Visual Basic Data Provider class. The GetDataMember event will be
raised whenever a Data Source binds to our Data Provider class.
It is possible to have more than one recordset object in a Data Provider class. Using the DataMember
property, one can determine which recordset the Data Source wants to bind to. This means that Data
Provider classes can be polymorphic: they can perform in different ways for different Data Consumers. This
is a very powerful feature.
The higher class can give information to the lower class, but can the lower class get
information from the higher class?
The lower class has no parent object. It has no reference to the higher class that created it and it cannot
access information in the higher class. The only thing the lower class can do is raise an event that the higher
class can then capture and respond to. However, the lower class cannot actually access any properties or
information from the higher class; it can only pass information to the higher class.
Let's look at an example to see how this works and see how communication goes up and down a class
hierarchy.
Imagine we have two classes, Class1 and Class2, which are in a hierarchy. Class1 is at the top of the
hierarchy and above Class2.
196
Frameworks and Technologies
m_objClass2.ClassID = 2
Rule 1: A class in a hierarchy can pass information into the class below it by using a
property in the class below it.
Dim a, b
m_ojClass2.DoSomething (a, b)
Rule 2: A class in a hierarchy can get a class below it to perform a task by using a
method in the class below it. The method can return no information, be a function
that returns one piece of information, or return many pieces of information by using
By Ref parameters.
197
VB6 UML
Rule 3: A class in a hierarchy cannot access information in the class above it,
through a property or through a method. Any information that a class may need
from the class above it will have to be passed to the lower class by the higher class
using properties in the lower class according to Rule 1.
RaiseEvent RaiseMe
Private Sub_RaiseMe()
End Sub
We can add code in the Sub to respond to the event. This gives us our final rule of class hierarchies.
Now that we have the architecture we need to think about how we can map the architecture to our existing
design.
198
Frameworks and Technologies
Architectural Views
We've come quite a long way with the design of our project and we have an idea of the frameworks that we
are going to be using. Now we need to combine these two sets of information so that we can "see" what are
final project is actually going to look like. This can be achieved by examining the project from a series of
architectural views.
UML provides us with package diagrams to represent logical software modules. Each module is a folder-
shaped icon. Links between packages are represented by dashed-arrows. Package names are generally
shown inside the package, although if there are sub-packages then the super-package name can be written
on the tag of the largest folder:
UML provides deployment diagrams to help describe this view. Each node or processing element in the
system is represented by a three-dimensional cube. If two cubes communicate, through a network for
example, then this relationship is depicted by a solid line:
199
VB6 UML
These views have a tendency to overlap as they are all essentially based on the same set of information.
Thus you may not find a need to use them all if indeed any. Typically though you would switch between
them when appropriate.
Placing the middle-tier component on the database server, on the other hand, means that the database and
the component can communicate directly to each other. However, now we run into the possibility of
overwhelming the database server. Whether the database server can handle the middle-tier component
depends on the amount of RAM on the machine, the speed of the processor and the other tasks the server
might be performing.
The position of the middle-tier component may also affect how we use ADO. ADO Connection and
Recordset objects both have properties that can be set to different values to optimize the performance of
the ADO. The size of the data, the location of the middle-tier component, and the capacity of the server and
network will all determine what the best values are for these properties.
There is not one solution that works for every three-tier project. Each project will
have its own arrangement of components using frameworks in a way that results in
the best performance for the system.
Working through a test project is the best way to find the best techniques for using
a framework in our project.
200
Frameworks and Technologies
Client-Side Components
A three-tier Visual Basic project will have the user interface on the client. In an Order Entry application,
the interface will allow the user to do things such as enter orders, add and review customers, etc. As this
will be an object-oriented project, we will create objects to represent things such as orders, customers,
products, etc. For example, we can have a Customer object that supplies customer information to the
GUI. All of the objects will supply information to the user interface, which in turn is supplying information
to the user. The user will also be providing information to the User Interface, which will pass this
information back to the objects.
What we are going to do with these objects is a bit strange. The object's properties (things that describe the
object or state) will be on the client. Properties for the Customer object might be the customer name, their
phone number, etc. It is the job of the client to keep track of what is being done to this object. Maintaining
a history of what has happened is called "maintaining state". When we say that the client maintains state, it
means the client remembers what it has been doing, and the objects on the client can remember the values
of their properties over time.
In a way we have spread our component over two or more computers, putting its methods and properties
where they work best. This client object will have nothing to do with connecting to the database, retrieving
records, sorting records or manipulating data. The only thing the client will be doing, with regard to the
database, is telling the business object in the middle-tier, "I need this particular record set" or "I need this
particular set of information". For example, the client may request all of the customers or all of the
customers whose title is 'owner'.
201
VB6 UML
For example, we may want to build a Customer component. On the client there will be a Customer
object that will have all of the Customer's properties and methods that do not communicate with the
database, such as Move Next to browse through the customer list.
Meanwhile, on the middle-tier, there will be methods to retrieve the customer records and update the
customer records. Putting these together will create the entire customer component, which is actually spread
across both tiers. The component itself will not actually be aware that it is split across two tiers. We can
build the Customer components so that they are separate from the graphical user interface. This will
allow us to use these components in any application, even one that does not have a graphical user interface.
Yet, this is not a very workable solution for the Internet that requires a thin client. If we wanted a thin,
dynamic, interactive Internet client, we could only allow very small sets of data to reside on the client. We
could also place the client-side objects onto a web server. These objects could pass the data to an ASP page,
which properly formats the information and passes it back to the client in a standard HTML page.
Remember, an ASP page is a web page associated with IIS, that allows a request to be
passed to a server, be processed, and a return page built on the fly.
In this way, we could create a Products object that works in our Order Entry project on the client,
but resides on the web server in an Internet application, which allows customers to view the products. The
Internet application will pass back HTML pages with product information to the client. Therefore, we can
use one object to build two applications that have completely different requirements, i.e. a thin and a fat
client.
Our middle-tier object has no properties and so it will have no history and no memory of what was
requested, i.e. the server does not have any state. The middle-tier objects are simply performing a task.
When the task is done they have no memory that they did it.
We have already seen, in Chapter 5, how the Customer component can be divided up into two tiers:
202
Frameworks and Technologies
However, consider the following hypothetical situation. Different users at Northwind may actually need to
look at different types of customers. For instance, a Sales Representative may need to view all customers
during their working day; but a Sales Analyst at Northwind might need to look at all the customers who
have made orders very recently. Or a rather interested Sales Manager somewhere may need to view all
customers who are regularly spending over $1000 an order, say. Clearly, what's happening here is that
different users would be viewing different collections of customers.
Now this is a hypothetical situation, I must stress: nothing like this came up in our interviews back in
Chapter 3 when we drew up the use cases. However, what quickly becomes clear, here, is that if Northwind
do ever require customer views like this, we are going to have not one collection of customers to deal with,
as we have currently thought, but three collections of customers:
How would we deal with not one but three collection of customers like this? The best way would be to
extend our original two-level class hierarchy so that there were three levels, like this:
203
VB6 UML
In this diagram, it's clear that we will have three different groups of customers:
All Customers
Customers greater than $1,000 in sales
Customers With Recent Purchases.
and that there are Customer Collections which will be three separate objects. So what's happening in this
diagram? Well if you remember back to our two-level class hierarchy, you'll recall that we had a Managing
Class that managed the Managed Class. In the diagram above, our Managed Class is represented by the
rightmost collections of actual customer details. And our Managing Class is right there in the middle of the
diagram, managing those collections of customer details. The problem is: if there are actually three
customer views in our hypothetical situation, how do we manage these views?
What's new in the above diagram is that we've now introduced a third level class - which itself manages
what was our original Managing Class. Clearly the hierarchy has become more complex!
Now you might be thinking: that was a hypothetical situation, we don't really need to change
anything in our Northwind system. In fact, I've presented in hypothetical terms something
that we were always going to have to address: that different Northwind users would need to
view different collections of customers, orders etc. As we approach the implementation of
our system, new issues become clear to us. As our design considerations and various
implementation issues - such as the class hierarchy framework - come together, we must
continue to accept the iterative process that is involved in designing and developing a real-
world project.
What was, in our two-level hierarchy, the Managed class, is now the Bottom Class - and this class will still
be managed by what was the Managing class in our two-level hierarchy. Meanwhile, what was our
Managing class in the two-level hierarchy, is now the Middle Class, and will itself be managed by a new
Top Class.
This is a fairly complex hierarchy. To actually build this component, we will need to map out what each
class will do and how it will communicate with the other classes. It might be useful, at this point, to make
what is commonly called a CRC card.
CRC Cards
We mentioned CRC cards earlier in our discussion of collaboration diagrams. CRC stands for Class,
Responsibility, and Collaboration (classes with which this class communicates directly). CRC cards are
usually made using an index card and look as follows:
204
Frameworks and Technologies
Class Name
Responsibilities Collaborations
As with other UML models, the CRC cards are just a tool. They help us to get things clear in our minds.
Making CRC cards forces us to consider in a logical way the methods and interactions that a class may
have. These cards will be particularly helpful when we're considering our hierarchy.
The diagram below depicts the CRC card for our Bottom Class, the Customer Managed class. This CRC
card supplies a description of the various responsibilities of the Customer Managed class plus any
classes with which it will directly communicate.
Looking at the Responsibilities section, we can see that the Bottom Class has three main responsibilities:
To provide data
To validate input
To communicate property changes to the Middle Class
The Bottom Class has direct communication with the Middle Class, and any Data Consumers for which the
Bottom Class is the source:
Responsibilities Collaborations
Provide direct access to Customer information Customer Managing Class.
through Properties.
Data Consumers.
Provide information about a Customer as a Data
Provider by supplying a recordset to the Data
Consumer.
Validate any changes made to a Customer property
or field by a data consumer.
Communicate all changes of properties to the
Customer Managing Class.
205
VB6 UML
Now let’s take a look at the Middle Class. It's clear that the Middle Class will have to act as bridge between
the Top Class and the Bottom Class. This will be reflected in its responsibilities and collaborations:
Responsibilities Collaborations
Provide access to the Customer Customer Managed Class.
Bottom Class. Customer Collection Managing Class.
Maintain a collection of Customer Data Consumer.
Objects.
Provide information on a collection of Customers
as a Data Provider by
Supplying a recordset containing all Customers in
the collection to a Data Consumer.
Validate any changes made to a
Customer field by the Data Consumer.
Manage Customer edits, add new Customers,
delete Customers, move through Customers in the
collection, Customer count, etc.
Must communicate all changes of properties down
to Customer Managed Class.
Our final CRC card is for the Top Class, the Collection Manager Class. Although this class will
communicate indirectly with the Bottom Class, this will be achieved through the Middle Class. Therefore,
the Bottom Class should not feature in the Collaborations section:
Responsibilities Collaborations
Retrieve a particular customer Client Application.
Collection. Customer Managing Class.
Pass requests, such as Add New or
Edit, to the appropriate customer collection.
We can see that the main responsibility of the Top Class, the Collection Manager Class, is to retrieve
a particular customer collection, which would be achieved by a method such as RetrieveView.
206
Frameworks and Technologies
From our UML diagrams and these CRC cards, we can see that all of the methods that we've found from our
sequence diagrams belong to the Middle Class, the Customer Managing Class.
There will also have to be a way to validate any changes, which will be made in the Bottom Class, the
Customer Managed Class. This class will mostly consist of Customer properties: CustomerID,
Region, etc. which came from our Customer Fields definition Business Rule. The validation rules for
these properties will come from the Business Rules that we made for each field.
We also know from our use cases that a request to Edit or Add a New Customer must be sent to the
Customer object before the activities can be performed. We will therefore need a property called
EditMode so that the class can determine if an edit or add new is in progress.
If you remember back to the description of the Data Provider technology, we considered the possibility of
using Data Source Classes. We will not be able to use them in our example though, as they would greatly
complicate the code and would not allow us to make the middle class generic.
Normally a class like the Customer Managed class would have private variables associated with each
property. The properties would set and get these private variables. The properties would also validate new
information and contain rules for accessing information
In the case of the Customer Managed class, however, we are going to do something a little different.
Looking at the CRC diagram for this class, we can see that its class responsibilities include:
To be a Data Provider, our class will need a private ADO Recordset, which it can pass to a Data Consumer.
Do we really want to keep the identical information in two places, i.e. the private variables (such as
m_strCustomerID) and a separate private recordset variable?
Keeping the information in two places means that we will have to keep the recordset and the private
variables synchronized. When a Data Consumer changes a field in the recordset, we will also have to
change the private variables associated with the property that corresponds to that field and vice versa.
If we have both private variables and a private recordset for each property, we are going to have to write a
great deal of code to keep everything synchronized. A golden rule of programming is to keep it simple.
207
VB6 UML
There is no rule written in stone, however, that says that every property must have a private variable
associated with it. Using private variables for each property is the easiest implementation and makes sense
for most classes. However, when it comes to our Data Provider class, it makes more sense to store the
information of the Customer Managed class in the private recordset variable. Then, when Customer
information is changed through a property, we simply set the recordset’s field associated with the property
to the new value. When we retrieve information about a Customer through a Get property, we will retrieve
the current value of the field associated with this property from the recordset.
Using this technique, we now only have one private recordset variable holding all of the Customer
information. This recordset can be passed to a Data Consumer or it can be used by the properties to get and
set customer information.
Let's now look at how we can use UML diagrams, combined with our knowledge of how to code a Visual
Basic class hierarchy, to further refine our design of the Customer component. We will look at the
Bottom Class to find some of the basic issues of coding the Customer component.
The Middle Class (Customer Managing class) provides a recordset containing all of the Customers
in the collection.
The Bottom Class (Customer Managed class) provides the view of a single customer. This single
customer is the current record of the Customer Managing class recordset.
We see that we once again have an issue of keeping two things synchronized: the
recordset in the Bottom Class, and the recordset in the Middle Class.
In my initial idea for this project, I tried to build the Customer Component with two recordsets: one in the
Bottom Class and one in the Middle Class. This leads to some major synchronization problems, however, a
few of which are listed here:
A Data Consumer bound to the Middle Class edits a record or moves to another record. (Example: A
grid control is bound to the Middle Class, and the user of the application edits a record in the grid
control.) These changes would have to be communicated down to the Bottom Class recordset.
The Client Application uses a Move method, which is located in the Middle Class. The recordset in the
Bottom Class would have to be rebuilt to reflect this new Customer.
A property in the Bottom Class is changed. This change would have to be passed back up to the
recordset in the Middle Class.
208
Frameworks and Technologies
A Data Consumer bound to the Bottom Class changes a field. This change would have to be passed back
up to the recordset in the Middle Class. (Example: A text box with the Customer Address is bound to the
Managed Class and the user of the application updates the information in the text box.)
I began to draw this all out with pencil and paper using sequence diagrams. Just by looking at those few
possibilities, I think you can imagine how complicated these diagrams were. This would be lead to very
complicated code.
Often the hardest part of finding a solution is not finding the right answer, but finding the
right question.
To find the optimum solution, I had to ask this question: “What type of components will be Data
Consumers for the Bottom Class?” Please consider this question. The Bottom Class is only going to provide
the Data Consumer with a recordset with one single Customer record.
If we were making an application and we wanted to show Customer information in text boxes, then it would
make sense to bind these text boxes to the Customer object. We only want information on one customer at
a time in these text boxes, so we'd want the recordset with only one customer record in it.
I hope that you agree with me on this; I am now making a design decision for us:
The Bottom Class (Customer Managed class) can only be used as a Data Provider
for textboxes; it cannot be used for grid controls.
Now, one of the nice things about binding textboxes to a data source is this: the recordset that the text
boxes are bound to can have more than one record. The record that you see in the text boxes is the current
record in the recordset.
What this means is that we don't have to pass a recordset with only one record back to the data consumers
bound to the Bottom Class. We could pass back a recordset with all the records in it - as long as the
recordset’s current record is the Customer we want to return.
Perhaps you see where this is going. The Middle Class will always have a recordset whose current record is
the Customer we want. In other words: we use the same recordset for the Middle Class and the Bottom
Class. Suddenly, all of the complexity of writing about twenty pages of code to keep two recordsets
synchronized disappears.
209
VB6 UML
This is obviously wrong. For our application, this would never be the case. A grid control should never be a
Data Consumer for the Customer Managed class. Grid Controls should be bound to the Middle Class,
which provides a collection of customers.
As we explore class hierarchy implementation, we find that some technologies are simply not
suitable, while others can be used but place constraints on our design. As we consider the
possibilities we need to weigh up how serious these constraints are and the potential
problems that may arise.
Let's look at the last problem that we came up against. Does it make sense that we should make the code
several times more complex and add many pages of code just because someone will get the wrong
information if they do something that they should not - such as connecting a Grid control to a Bottom
Class?
When we design an event driven project, we must always think of every possible thing the user can do to
blow up your application. It doesn't matter how ridiculous it is, if the user can do it, they probably will.
This is where use cases and sequence diagrams come in. They can show us all of the alternative flows.
When a user does something they should not, there can be two types of outcomes: catastrophic outcomes
and harmless outcomes. My list of catastrophic outcomes includes:
Catastrophic outcomes must always be handled. We must write code to prevent them from happening. We
must carefully design our project using UML, search for every possible alternative flow, and figure out
everything the user can do. From this list of alternative flows, we find the ones that are catastrophic and
prevent them. We have little choice here. Catastrophic outcomes can never be allowed to happen!
All outcomes that are not catastrophic, we can put into the harmless category. These are things that we
should prevent from happening, if it is possible. They are usually things that will annoy a user when they do
something wrong, but will not affect anything in the application.
As long as the cost is not too high to stop these things from happening, we should write the necessary code
to protect against harmless outcomes. While using one recordset would cause some weird behavior if for
some bizarre reason someone wants to see a single Customer in a grid control, this is a harmless outcome.
This is not a violation of a business rule, it does not affect the database or information integrity, and it will
not cause a GPF. It might annoy the users of our application, but it is does not do any damage.
To summarize what I'm saying here, we must not confuse our ideal world with the real world. In an ideal
world, we would never make such a compromise: we would make perfect code that did everything it was
supposed to do and that would never work in a way it shouldn’t. Harmless outcomes would never happen in
our applications. In the real world, we have deadlines, budgets and other things that limit us and prevent us
from using an ideal solution.
210
Frameworks and Technologies
From our earlier discussion of Visual Basic Class Hierarchy framework, we know that there are certain
rules governing the flow of information between classes. If the shared recordset is located in the
Customer Managing class, then there's no way to get access to this recordset from the Customer
Managed class.
It is for this reason that we must put the shared recordset into the Bottom Class - the Customer
Managed class. As we shall see, this will force us to add events into the Bottom Class to pass information
up to the Middle class. This will also affect the location of other methods.
These frameworks allow us to create server objects, in the form of an ActiveX DLL, which can be easily
scaled. In addition, the server component can minimize the amount of data that is being transferred back
and forth between the client and the server.
The ADO and RDS frameworks will be a critical part of our application. Here is how we will be deploying
these technologies in our Northwind project:
RDS will provide the connection between the client and the middle tier
ADO will provide disconnected recordsets that can be passed between the two
We can pass these disconnected recordsets from the middle-tier to the client-tier. The client-tier can then
look at these records, or update these records and return them to the middle-tier, which will then pass them
on to the database. Our client side objects will contain a disconnected recordset object that will be used to
review and update that object's information. This would look as follows:
211
VB6 UML
To summarize our architecture then: the user interface interacts with the appropriate object, such as a
Customer object. The Customer object has all of its properties on the client, and can give the user
interface any information on the client that it needs. If the user interface makes a request that requires a
connection to the database, the object will have to pass that request to the middle-tier. The middle-tier will
then connect to the database, get the information, transform it, and pass it back to the client.
We will be implementing this architecture throughout the second half of this book.
Summary
In this chapter, we've started to think about the implementation of our system design. Up until now,
although we had a pretty good idea of the design of the Northwind system, we didn't really know how that
design would be translated into code.
Although we haven't actually seen any code yet, we have begun the process of defining the technologies
and frameworks that we will be using to build our system. We have examined the technologies in more
detail to find out how they will affect our code when we come to the implementation stage.
Client-server architecture
The DNA architecture
ADO (ActiveX Data Objects)
RDS (Remote Data Services)
The Visual Basic Class Hierarchy framework
In the next chapter, we will start to refine this implementation stage by using activity diagrams to model
some of our components' more complex behaviors. We'll also see how some of the frameworks in this
chapter affect our coding.
212
Designing A Test Project With
Activity Diagrams
Using our UML use cases, we created sequence diagrams. These diagrams determined the public properties
and methods that our project's components would need to perform their required tasks. Sequence diagrams,
though, are very general and provide us with very little, if any, information on how to perform these tasks.
Therefore we now proceed to the next stage of the UML Process, activity diagrams, so we can find the
best way to code these public properties and methods.
Activity diagrams will also help to determine what private methods and properties our components require.
Since activity diagram deal with specifics, we're also going to start working towards the implementation of
our Northwind system. We're not just going to jump straight in to the main project code however. First,
we're going to design and build a test project to see how well our frameworks and UML design perform.
This test project will help us find the best way to implement our DNA architecture.
Using this information, we will design and build a final version of the server component in
Chapter 11.
Activity Diagrams
An activity diagram is the point at which the user requirements, as described in the use cases and sequence
diagrams, finally meet the requirements of our frameworks. It's at this stage of the UML process, therefore,
that we create a detailed mapping of the code within each Property and Method of the system's components.
Each message in our sequence diagrams can represent a Public method of one of the system's
components. We can use our activity diagrams to expand these messages out, showing how the component
will respond to the message and what tasks the component is required to perform when it receives that
message.
For example, the server object may get a request for a Customer object from the client
application. An activity diagram would show the series of activities the server object would
have to perform to get and return a Customer object.
Although we could create activity diagrams for every message we've developed, it is often unnecessary to
actually do this. Activity diagrams are good at capturing the details of complex operations. For simple
messages, then, sequence diagrams are often sufficient.
For more complex messages, especially between logical tiers, we really need a more detailed breakdown of
the process. We shall therefore only need to expand out some of the messages in our Order Entry
application in this chapter.
An activity diagram begins with a starting point and finishes with a ending point:
216
Designing A Test Project With Activity Diagrams
Each activity is placed in a lozenge shape. The text inside can be as specific or as general as we like:
An arrow connects one activity to another showing the movement of the procedure in step-wise fashion:
217
VB6 UML
If there is a decision point where the flow can go in more than one direction, then a diamond is used. The
two arrows coming out of a decision point will have text placed over them showing what is required to go
in one or the other direction. The text over the arrow is called a guard and must be Boolean in nature
(either the guard is True, or it is False):
For every split in the flow caused by a decision point, there has to be a place in the diagram where the
flows come back together, even if this is the end point of the diagram.
One of the powerful features of activity diagrams is that they are able to show a sequence of events
happening in parallel. This is modeled through the use of a horizontal bar, where the number of flows will
either join or fork:
218
Designing A Test Project With Activity Diagrams
Therefore, an activity diagram for making a cup of coffee might look like this:
Now let's look at how to turn our use cases and sequence diagrams into some specific activity diagrams so
we can start coding Visual Basic in the next chapter.
The first step in designing any project is deciding what the project is going to do. An experimental project
should test the part of the system you are about to build. Our test project will test the server component to
make sure that the frameworks we are using are actually going to work for our project.
The use cases that we built allowed us to group the user requirements into tasks. Each use case contained a
series of events that we modeled with sequence diagrams. The use case and sequence diagrams gave us a
good understanding of the user requirements, the components the system needs, and the public methods and
properties these components must have to accomplish the tasks the users require of the system.
219
VB6 UML
From this point onward, we must begin to use activity diagrams to map out the internal workings of each
method and property of the system's components. The choice of frameworks, whether we build our project
using MTS, RDS, ADO or any other technology, will shape the code that we model with the activity
diagrams. Therefore, before we can continue designing our project, we need to determine what technologies
will work best for our system. To answer this question, we have to create a test server component.
We have added to this sequence diagram the conditional step of retrieving the recordset if the client
Customer object is not initialized. When the Customer object is not initialized, we use the message
Retrieve Customer Recordset.
The server component will then have to retrieve the category information from the database and return this
information to the client in the form of a disconnected recordset.
Although every message in this diagram can be expanded out into an activity diagram, we are focusing on
the server component at the moment, so let's just look at the messages being sent to and from the server
component. These messages include:
220
Designing A Test Project With Activity Diagrams
We can group these first two messages into a Retrieve Recordset method, and the second two
messages into an Update Recordset method.
So this test server component will test retrieving and updating disconnected
recordsets.
These recordsets are not just any type of recordset: they are ADO disconnected recordsets. We made this
decision based on the needs of our system: primarily, the condition that our system should be stateless.
Building an ADO disconnected recordset is like building anything else: we must follow a
certain set of rules when we're putting it together for it to work properly. The hardest part is
finding what these rules are. Throughout the rest of book, I'll be explaining the rules of our
frameworks.
We will start with the knowledge that there are variables for a private ADO connection object and a private
customer recordset variable in our server component. The necessary steps for retrieving a customer
recordset are as follows:
These steps are very high-level. They do not really get into any discussion of the ADO or how the ADO
works. They are just a logical series of steps that we need to perform to get information from the database
and return it to the client. The next step is to determine exactly how we do each of these steps using the
ADO.
221
VB6 UML
Looking into the Wrox book on the ADO (the ADO Programmers Reference), we can determine how to
perform each of these steps using the ADO. For example, to create a disconnected recordset, we need to
create a client side cursor. We can go through the book and find what properties need to be set, how to
connect to the database, and how to get the records from the database.
I have done this for you, and below is a table of the steps required to perform the general tasks we listed
above using the ADO. These are the specific steps our server component must perform to get a customer
recordset and pass back a disconnected recordset with the client information in it using ADO. Each step
also includes the framework that places this requirement on the system, so you can see where you might
have found information on how to perform this step.
Framework that
Activity Requirement
requires this activity
Create ADO connection ADO Framework You must make a connection to the
database by either creating a
connection object prior to retrieving
data or passing in a connection
string in the open method of the
recordset
222
Designing A Test Project With Activity Diagrams
Framework that
Activity Requirement
requires this activity
Set customers recordset ADO framework To connect a recordset variable to a
connection database you must set the recordset
variable's connection property to a
valid connection string
If there is no error, set the ADO framework Must set LockType of a recordset
customer recordset before retrieving data from the
LockType to the appropriate database (not required for the
lock type and set the default optimistic lock)
Recordset's connection
object to the connection
object.
Open the customer ADO framework Must open a recordset to retrieve the
recordset object requirement data into the recordset
Close the ADO recordset ADO framework Failing to close the connection
and set the recordset and object leaves it open until it times
connection object to out. There is a limited number of
nothing. connection objects, so letting them
close by timing out ties up valuable
resources
This table says a great deal about programming in Visual Basic. To make this chart, I had to understand
three frameworks (RDS, ADO and Three-Tier). Programming Visual Basic goes beyond knowing just the
language syntax. To make Visual Basic projects, we now have to understand the frameworks that our
projects are going to work under. We therefore need to do the following:
223
VB6 UML
If you follow this guide you should end up with a table like the one above. It might take some
time but the rewards are worth it.
From this table, we can now draw out our activity diagram for Get Customers. Each activity in the chart
above will map to some element of the activity diagram.
The next two rows in the chart make up a decision point. Either an error occurs or it does not:
224
Designing A Test Project With Activity Diagrams
The rest of the chart can be mapped out similarly until the whole activity diagram is created:
The simultaneous flows that have arisen due to the possibility of errors will come back together at the thick
horizontal bar or concurrent join. Of course, as with any activity diagram, the ending point must be shown.
225
VB6 UML
Let's consider each of these in turn now. Once we're done here, we'll move on to create some more activity
diagrams and see if we can identify any patterns emerging.
When we're working on a team, we can map out a method's sequence of events together, or have a single
person work them out and then put them into an activity diagram. Once we have this diagram, the whole
team can then review the diagram and decide what they want to add, remove or simply change.
Trust me, it's far easier to look at an activity diagram and see the flow of a method than it is to try to look
through code to find the same flow. When I hand someone an activity diagram, I do not have to explain the
diagram; the diagram is telling him or her everything that is happening. If I hand you two pages of code,
you will be staring at it for ages. If the code is complex, with many alternative flows, it may take a very
long time to understand the code. Even when you have figured the code out, it is still very hard to visualize
the flow and to spot any step that might be missing.
Listing all of the steps of each of our methods gives a clear, readable way of seeing everything your code
must do to perform this task. An activity diagram can be passed around to all of the team members. Each
member of the team can quickly understand it, read it and determine if any changes need to be made.
Making changes requires adding activities, decision points, etc., and can even be made with a pencil on the
diagram and later added to the final copy in the repository.
Finding the best solution is usually an iterative process. First you have your initial concept, refine it, test it,
then you refine it more and test it more, etc. Activity diagrams will let us go through many refinements of
our object's methods before we even write one line of code. Removing something from an activity diagram,
means deleting a graphic image. This takes about a second. Changing code means deleting lines of code that
may have taken hours to create and adjusting the rest of the code to deal with the deletion. Which one
would you rather do?
Finding Patterns
There is one other thing activity diagrams are useful for: patterns. Just as our sequence diagrams revealed
patterns, which could be used to simplify coding, activity diagrams should also reveal patterns, but at a
more detailed level. To demonstrate this, we will go ahead and make the tables and activity diagrams for
Retrieve Product and Retrieve Category.
226
Designing A Test Project With Activity Diagrams
As you can see, it is almost identical. This means that not only are we identifying a pattern but also it
should only take a fraction of the time to produce that the first one did:
Framework that
Activity Requirement
requires this Activity
Create ADO connection Three-Tier framework No information can be stored from one call
to the next, therefore a connection object
must be created first for every request to
get information
If there is an error getting the ADO Framework All errors must be handled
connection, raise an error and
end
If there is no error, initialize Three-Tier framework No information can be stored from one call
the products recordset object to the next; must initialize recordset for
each request
Set products recordset source ADO framework If a recordset is going to retrieve data from
equal to the appropriate query the database, there must be a query string
string which specifies what information from
which tables will be retrieved
If there is an error getting the ADO Framework All errors must be handled
connection, raise an error and
end
Set the products recordset ADO framework Must set LockType of a recordset before
LockType retrieving data from the database
Open the products recordset ADO framework Must open a recordset to retrieve the data
object requirement into the recordset
Table Continued on Following Page
227
VB6 UML
Framework that
Activity Requirement
requires this Activity
If there is an error opening the ADO framework All errors must be handled
recordset, raise an error requirement
If there is no error, return the Three-Tier framework Return disconnected recordsets
recordset
Of course, this is almost identical to the activity diagram for the Retrieve Customer.
228
Designing A Test Project With Activity Diagrams
Is this looking familiar? Let's see how we exploit the emerging pattern.
229
VB6 UML
Putting the retrieve into one method could result in multiple Select Case statements, optional
parameters (products may have some special requirements for retrieval), and lines and lines of code to work
through all of this. Code should not only work, but also be readable. Keeping separate methods for
retrieving customers, products and categories will result in more methods, but is cleaner. Revising our
activity diagrams, we can make a general retrieve recordset diagram that would look as follows:
For each type of recordset that we need to retrieve, we would have a method like this. For example, we
would have RetrieveCustomers, RetrieveProducts and RetrieveCategories methods. As
each method would be used to retrieve only one type of recordset, we do not need any steps in the code to
determine what the correct recordset is.
As for the code that is the same in each of these methods, we could also create a separate, private method to
perform these tasks so the same code is not repeated within each method.
230
Designing A Test Project With Activity Diagrams
With regard to retrieving recordsets from and updating recordsets to the server component, we will need to
answer these questions:
How efficiently will disconnected recordsets work for the Northwind Order Entry system?
Does RDS actually work?
If RDS works, does it work efficiently enough for this system?
Does this system require MTS?
The test project should be designed to find the best way to retrieve a disconnected recordset from the server
component and pass it on to the client. It also must find the best way to pass a disconnected recordset to the
server component so it can pass any updates on to the database. We will want to put methods into the test
project that will retrieve and update disconnected recordsets.
For updating information, we find that we have something special to test because we are using disconnected
recordsets. What happens if two users both have disconnected products recordsets? Both users get the
product information, which is stored on the client machine. One makes a change to a product, soup, and
updates the database with this change. The second person now has old information, which is on their client.
If the second person updates this soup product, there will have to be some way to reconcile this
inconsistency. We will also have to include a test to find the best way to handle the inconsistencies in our
test project.
The next step is to determine is the nature of recordsets we will be retrieving and updating:
There is only one way to find the answers to these questions, and that is to look at the system and see what
type of recordsets the final project will be using. It would make little sense to be performing tests on
disconnected recordsets of ten thousand records when the final project will never use a recordset with more
than fifty records.
When considering how our recordsets should perform, we will look at the database that our final production
system will be using. We will then determine the conditions that will exist for the disconnected recordsets.
We must make sure that the retrieve and update methods in our test project meet all of these conditions. For
example, we could make the following chart for the different tables in the Northwind database:
231
VB6 UML
We could make a method to test retrieving a recordset from each table. If there is enough time to make a
test project this detailed, then this is the most accurate test. However, there is rarely time or a need to do
this.
We have made activity diagrams for retrieving a customer, product and category recordset. The three
activity diagrams show us that the task of retrieving a recordset will be basically the same whether we are
retrieving a customer, product or category recordset. We can choose to design and implement only one
Retrieve method in our test project, providing we test this Retrieve method over a wide range of
conditions that represent the full range of possibilities that will exist for all of the Retrieve functions.
Looking at the above table, we can see that our Retrieve methods will need to retrieve anywhere from a
few records to a few hundred. The Categories table has fields of many data types: OLE object, fixed and
variable length strings, and long data types. This is a small table with only a few records. Building the
RetrieveCategory method will allow us to test a small recordset with a wide range of field data types.
This makes the Categories table a good candidate for our test project. Therefore, we will use the
Retrieve Category method in our test project.
232
Designing A Test Project With Activity Diagrams
The Products table, though, is not very well represented by the Categories table. The Products table is
likely to have conflicts on an update, which is not true for the Categories table. The Products table will
also have larger recordsets built from it than the Categories table. We will therefore have to include a
series of tests on the Products table as well.
From this discussion, we design a test project that runs through the following sequence of events:
The client will request a disconnected category recordset using the server component.
The server component will get the recordset from the database and return it to the client.
The client will select a category.
The client will request a disconnected recordset containing the products in the selected category using
the server component.
The server component will get the recordset from the database and return it to the client.
The client will update the disconnected recordset.
The client will pass the recordset back to the server component.
The server component will check for any inconsistencies in the data.
The server component will try to fix any inconsistencies in the data.
The server component will update the database if there are no inconsistencies in the data.
These tasks will allow us to test retrieving and updating disconnected recordsets from the Products and
Categories tables.
In the next chapter, we will actually build this test server component project. We will need to design and
implement a GetProducts and GetCategories methods, and an UpdateProducts method. We will
also need several private methods to reconcile any inconsistencies. We will design these methods by
creating activity diagrams for each of them. We will also make an activity diagram for any additional
private methods that we will need for our server component.
Summary
In this chapter, I have given you an introduction to activity diagrams. These simple diagrams give us a
visual model of the flow of our code. Using these diagrams, we can find the best way to code our properties
and methods. We can carefully check the model and make sure every possible situation has been covered,
and that all possible alternative flows have been properly addressed.
Making a set of activity diagrams is only the first step in making our application however. The next step is
to turn these activity diagrams into Visual Basic code. In the next chapter, we will work out several more
activity diagrams for the server component of our test project. We will then see how we can turn these
activity diagrams into Visual Basic code.
233
Building and Testing A Server-
Object
In the previous chapter, we looked at how we can use activity diagrams to help us model some of the more
complex operations that we will be coding. We also saw how the choices of technology that we made
affected they way we intend to code these operations.
We've spent a lot of time discussing and planning the system and now it's time to get our hands dirty. As
you can see, we've come a long way without seeing any code - quite the opposite of the common
development strategy of diving straight into code.
Don't think we've finished designing yet though. We're building this test project to find out how feasible
our design is. As you'll find out, we still have some modifications to make. But I'm getting ahead of myself.
See how to convert our activity diagrams into Visual Basic code
Continue to design new operations with activity diagrams
See how our activity diagrams patterns can be used as a template
Learn the benefits of building a test project
Assess how well our experimental server project works
Build a strategy based on the results of this test
With all the discussing and drawing of diagrams, we've almost forgotten how to code. Let's jump right in
and get coding.
VB6 UML
Now we need to open the References dialog, and set a reference to Microsoft ActiveX Data Objects 2.0
Library. All connections to the database will be made with the ADO:
Add the following code to the General Declarations section of clsServerTest class:
Option Explicit
The variable m_objADOConn is an ADO Connection object variable that we will use to connect to the
current database. With each call to the server component, the Connection object variable is recreated.
We are creating a module-level variable for the Connection object, as it will be created in one function,
and then used in one or more other functions. Use a module-level variable whenever you don't want to be
passing the Connection object from one function to another.
236
Building and Testing A Server Object
If we do not close our Connection object when we've finished with it, the Connection object will
remain until it times out. The default timeout is sixty seconds. If we don't close Connection objects,
unused ones will quickly pile up and use up valuable resources. Creating the Connection object when we
need it, and closing and destroying it when we're done, will allow ADO to function more efficiently, which
in turn will make our system work better and faster.
We will create a Public function to make a connection to the database. We will use this function to
perform two services:
The final project will need to know which actor is using the system. To do this, a user will have to log into
the system. Therefore, we need to have some way of verifying if a user is allowed into the system and what
their role in the system is.
We will not determine the user's role in the test project, but we will verify if the user's
Password and UserID are valid. In the Northwind database, there are no logins, but in a
real application, there would be some form of security on the database.
The methods of the server component are going to be "connecting to the database" and "returning data to
the client". This function will provide a way of doing this.
We are only allowing for one database. If there was more than one database, we might need multiple
connection objects.
This is not coded in the best way possible: a function should only be performing one
task, not two completely separate tasks. However, it will provide a good example
that I will later use to go into a detailed discussion of why methods should only
perform one task. We will do this when we design the final version of the server
component. Writing the function this way will not affect the performance of the test
project, so it will not affect our tests of the three-tier framework.
The test project is not meant to be perfect. A test project is a first attempt, a rough draft that is more
concerned with finding out if a framework will work with our project, and, if it does work, how to make it
function efficiently. As we run through our tests we will find weaknesses in our code, poor decisions
concerning how we have chosen to implement our methods, and techniques that are unworkable. We should
document this information, and be sure not to repeat these mistakes in the final project. If these mistakes
will affect the performance of our test project, we will have to rewrite the test project to remove them. If
these mistakes have no effect on our tests, then we can remove them if we have time, or just leave them.
Writing code is an iterative process. It's better to work the kinks out in a small test project that takes a few
weeks to design, build, debug and rebuild, than a final project that has taken months to build and may
require many more to rebuild.
237
VB6 UML
This function will have two parameters: the user name and password. ADO may return an Errors
collection.
An Errors collection are errors that are concatenated and passed back to the calling
function (the client). The client can then pass the error to the user, or in this example, log it
to a file to be viewed later.
Create a Public function with two optional parameters, one for the user name and one for the user
password:
Please note that the section headings that follow correspond to the activity diagram we're
using to build this code. We're actually building one function through these sections.
238
Building and Testing A Server Object
For disconnected recordsets, we want to set the CursorLocation to the client otherwise we'll end up
with a continuous connection, which defeats the point.
Of course, you may need to modify the ConnectionString so that it points to the correct directory on
your computer.
If the DBMS requires a user name and password, these will be added to the connection
string. To keep it simple, we are using the Microsoft Access version of the Northwind DBMS,
which is not password protected.
ADOConnection = True
Exit Function
ADOConnectionError:
239
VB6 UML
End If
Exit Function
End Function
That's our first function complete, build directly from our activity diagram. This function now allows the
server object to make an ADO connection. However, as a final step, we must add a line of code to the class'
Terminate event to make sure the Connection object is closed and destroyed before we leave the
function:
End Sub
240
Building and Testing A Server Object
So create a new Public function with two parameters, one for the user name and one for the user
password.
Remember that the client has no memory of the UserID or Password from the
ADOConnection function because it is stateless. These two parameters must be passed
into every function that requires them.
241
VB6 UML
We will now create a local recordset variable that we can use to retrieve the category records into:
We did not include these previous two steps in our activity diagram. Having a recordset
variable is a prerequisite to this function. We cannot set properties of a recordset unless we
have a recordset variable to work with. We could therefore explicitly add "Create local
recordset variable" into the diagram if we felt that someone reading the diagram may not
figure this out.
As for error handling, the arrows going to "Raise Error" indicate that there will be an error
handler in the routine, so again we did not explicitly show "Create Error Handler". We can
make your activity diagrams as fine-grained and detailed as we like.
What we include in our diagram depends on how we will be using the diagram, and who will
be using it. If we were giving this diagram to an inexperienced programmer, we would
probably want to get very fine-grained and show every step. If this diagram were only going
to be used by experienced programmers, they would probably want to see the big picture,
and would be less concerned with the detailed steps that an experienced programmer would
expect. UML models are tools. It's up to us to determine the best way to use the tool in each
situation.
Once again, the section headings I've used here reflect the stages we've analysed in our
activity diagram.
With recCategories
.CursorLocation = adUseClient
242
Building and Testing A Server Object
As this is a test application, we'll hard-code the query into the code instead of using a string constant.
As a general rule, we'd rather the user didn't have the control over what information is returned or what
types of queries are performed. If we allow a user to pass in any query to the function, they could write a
query that deletes tables, records, or any number of destructive actions. It's critical to always keep security
in mind; there are some very malicious users out there!
The downside of not allowing queries to be passed in is that we now have to provide functions to handle
each query that may be wanted. Another solution to this is to allow an additional parameter to be passed in
that will determine what query is to be used, such as returning all customers, all customers who have orders
greater than one-thousand dollars, etc.
.CursorType = adOpenStatic
.LockType = adLockOptimistic
Again, I have not shown the steps of setting the two objects to Nothing; this step is a
standard part of programming objects and should not have to be explicitly shown. If you feel
you, or the developers reading the diagram, may not remember this, you can add these steps
into your diagram.
243
VB6 UML
Exit Function
GetCategoriesError:
End Function
244
Building and Testing A Server Object
This function is essentially identical to GetCategories, except that we have added a parameter for the
CategoryID to give the user some very limited control over the query. In this way, we can only
return the Product belonging to a particular Category:
With recProducts
.CursorLocation = adUseClient
245
VB6 UML
Exit Function
GetProductsError:
End Function
You'll also notice that instead of setting the recordset's properties, such as CursorType,
individually we are setting them as parameters of the Open command:
We now have the necessary routines to get data for the test project from the database. So let's turn our
attention to the reverse process of updating the database.
The secret is in the recordset object itself. ADO allows us to apply a filter to the recordset object so that we
can remove all the records that have not been changed. We can then try to update these records to the
database. When that fails, the recordset object will contain three copies of the information:
We will create a special function called Reconcile that will use these three values to reconcile any
changes that occurred to updated records while the recordset was at the client.
This is a fairly intense piece of code, but once you understand it, you will understand the basis for any
future three-tier reconciliations. As this is a complex function, it has been broken down into three activity
diagrams.
246
Building and Testing A Server Object
The second diagram will be checking for conflicting records and the third will be Reconcile itself. Both of
these will be presented below. Let's start creating the code.
247
VB6 UML
UpdateProduct
Begin by creating a Public function called UpdateProduct with our standard UserID and
Password parameters. We will also have an additional parameter, v_recClient, which will pass in the
client's modified Products recordset. We will use this recordset to update the database:
Also add the recordset variable and set up the error handler:
You're probably getting used to the idea now that my section headings reflect the stages in
our activity diagram.
The recProducts Recordset object does not have its Connection object set to the Connection
object we have just created. Before we can move on, we must do that:
recProducts.Filter = adFilterPendingRecords
248
Building and Testing A Server Object
This filter allows us to view only those records that have changed but not been sent to the server.
recProducts.UpdateBatch adAffectGroup
The adAffectGroup constant means that the UpdateBatch operation only effects the records specified
by the current filter.
recProducts.Filter = adFilterConflictingRecords
Our function is not yet finished; but we need now to look at the activity diagram for Checking for
Conflicting Records:
249
VB6 UML
We can tell if there are any conflicting records because the record count will be greater than 0. If there are
conflicts, we will call the function Reconcile that will try to reconcile these differences. Reconcile
will return True if the changes were successful and False if they fail. (We'll look at Reconcile in a
moment.)
We are using a feature of the Connection object called a transaction here. A transaction provides
atomicity to a series of data changes to a recordset within a connection, meaning that the entire operation
either succeeds or fails as a whole. The transaction starts before we call the Reconcile function. Any
changes that we make to the recordset in the Reconcile function will not be committed until we return
from the Reconcile function, and the function returns True. Only at that time, we will commit the
transaction, and the changes to the database.
If in the middle of attempting to reconcile the records in the Reconcile function, the function fails, then
it will return False. We can then rollback the transaction. The rollback causes any values in the recordset
that were changed after the transaction began to be returned to their values prior to the start of the
transaction:
' Reconcile
If Reconcile (recProducts) Then
' Commit Transaction
m_objADOConn.CommitTrans
Else
' Roll Back
m_objADOConn.RollbackTrans
End If
End If
Return Recordset
The recordset we now have may be different from what is on client, so we want to return a recordset to the
client that is complete and current. To make the recordset complete, we must remove all of the filters, and
then set the function's return value equal to the unfiltered recordset:
recProducts.Filter = adFilterNone
Set UpdateProduct = recProducts
Exit Function
Raise Error
Once again, we have a standard error handler:
UpdateProductError:
End Function
250
Building and Testing A Server Object
So now create a Public function called Reconcile that accepts a recordset parameter By Reference and
returns a Boolean. By passing in the recordset ByRef, any changes to the Product recordset in the
Reconcile function will also be made to the Product recordset in the UpdateProduct function:
As ever, my section headings here reflect the stages in our activity diagram.
Products Recordset Resynchronize
As I mentioned earlier, we're going to need three copies of the out-of-synch records:
251
VB6 UML
To do this we need to call the Products recordset's Resync method as follows (remember that this
recordset only contains records that were out of sync with the database):
We've already seen the adAffectGroup constant before so we don't need to discuss it here. However, the
adResyncUnderlyingValues constant is new to us. It means that the data is not overwritten and the
pending updates are not cancelled. (The default value here is adResyncAllValues, which does
overwrite any pending updates.)
The Resync method of the ADO Recordset object only works for recordsets that have a client
cursor and are being batch updated.
r_recProducts.MoveFirst
Order Entry Clerk, Mike, has a customer call in. Mike starts a new order on his client application. Mike's
client retrieves the current product information and stores it on Mike's client in a disconnected recordset. In
this recordset, the product called ACME has the value of 10 for the UnitsInStock field.
Order Entry Clerk, Dana, has a different customer call in. Dana starts an order on her client application.
Dana's client retrieves the current product information and stores it in a disconnected recordset. In this
recordset, the product called ACME also has the value of 10 for the UnitsInStock field (it has not yet
changed since Mike started his order).
Mike's customer makes a request for three cans of ACME. Mike enters them into the order. Mike's client
changes the field in his Product disconnected recordset to seven cans. This recordset is not connected to the
database, so this change only occurs in the recordset on his client.
Dana's customer orders two cans of ACME. Dana's client updates the disconnected recordset on her client
to eight cans. Dana completes the order and sends the disconnected recordset to the server object to update
the database.
The database now says that there are eight cans of ACME. How do we fix this when Mike's client sends its
update to the database?
It's harder to understand the problem than the solution. The original value in Mike's recordset was ten and
the current value is seven. If we subtract seven (the number on Mike's machine) from ten (the original value
sent to Mike's machine) we get three. Therefore, if we know the original value in the recordset and the new
value in the recordset, we know how many of cans have been removed. We only need to subtract the total
number removed from current value in the database to get the correct value in the database. Are you still
with me?
252
Building and Testing A Server Object
Note that it's basically the same process for adding stock - except that the sums go
backwards.
An ADO Recordset object with a client cursor, performing batch updates, will have three properties that can
be used to resolve conflicts:
Value - The current value of the field in the database at the time the batch update was performed
OriginalValue - The value of the field that was originally sent to the client
UnderlyingValue - The value the client changed the field to
In our case, the UnderlyingValue is 7 and the OriginalValue is 10, so we have 7 − 10= -3. There
were 3 items removed by Mike.
The current value in the database is 8, so 8 + -3 = 5. Hey Presto! We have the correct value.
253
VB6 UML
We are not taking into account the possibility of dropping below zero items. We will
address this issue when we are designing the final server-component.
r_recProducts.UpdateBatch
r_recProducts.Filter = adFilterConflictingRecords
Exit Function
Error Handler
Let's create the error trap:
ReconcileError:
End Function
Actually, this function is not complete. We have not dealt with the possibility of one of the other fields
changing. If the name or the price of a product changes, the entire transaction would be dropped. This
certainly is a bit poor, as the user will have to re-enter the order.
254
Building and Testing A Server Object
If there were any other changes, such as price, product name, etc. these changes don't need to be reconciled,
we only need to update the disconnected recordset to reflect these new values.
To do this, we could just set the Value property (the value of the field currently in the disconnected
recordset) to UnderlyingValue (the value currently in the database). The following code segment could
therefore have been added after we had reconciled the UnitsInStock field:
In this situation, if we attempted to the update and it failed again, there would have to be an error in the
UnitsInStock field that couldn't be reconciled.
If you're concerned that working with disconnected recordsets will result in pages of code to reconcile
inconsistencies, relax: this should not happen. In the Northwind system, only the Sales Coordinator makes
changes to the product. The only change that is likely to occur is a change in price. A method could be
added to the system to input these changes during business hours, but actually change the database after
hours. The only possible conflict that could exist for a Product recordset is the UnitsInStock field.
If the Order Entry clerks were working twenty-four hours a day, we would have to do the
update while orders were being taken. We can still do an update at a scheduled time, but in
this system the client component would automatically get a new version of the Products
recordset after the table update was complete. For example, the Product update could occur
at 12:00 a.m. and the client component can request an update of the Products recordset at
12:05 a.m. If you are worrying about what will happen in the five minutes between, you do
not have to.
Using disconnected recordsets requires a shift in thinking. We must look for ways of minimizing conflicts
in the records. A carefully designed system will only have a few possible circumstances where there can be
conflict, and they can be handled by writing methods such as the Reconcile method we have created.
Most of the possible conflicts can be easily resolved. For example, if there is conflict in the field containing
the name of the product, we can just change the value in the disconnected recordset to the new value.
GetServerTime Function
To properly stress test this component, we will want several copies of the client test application to be
running at the same time making requests to the server at exactly the same time. The only way several
different clients can make a request at exactly the same time is if they are all synchronized. If all of the
clients get the time from the server and set their clocks to the server's time, they will all be synchronized.
We therefore need to write a function that will return the time on the server for the clients (a property will
not work with RDS and HTTP):
255
VB6 UML
End Function
When using the Format function, the letter "m" is for month, "n" is for minute.
If we only had to compile our object once, we could leave this topic now. Unfortunately, we often compile
our DLL, find there are bugs, fix the bugs and recompile. If we don't make some changes to the way the
project is compiled, Visual Basic will create new registry entries every time we recompile our DLL. It
would be really nice if the old entries were removed from the registry when the new one is created; but it
isn't. As we compile our project over and over again, more and more entries will be associated with our
DLL in the registry. In theory, only the last entry should be used and everything should be OK. In the real
world, these old registry entries will usually cause our DLL to fail, usually in a way that is completely
unpredictable and incomprehensible. To prevent this from happening, we must compile the DLL once. Once
we've done that, we can bring up the Project Properties window and select the Component tab:
We can then change the compatibility to Binary and select our compiled DLL. This forces Visual Basic to
compile the DLL with the same registry entries as the DLL listed.
256
Building and Testing A Server Object
This works great as long as we don't change the public interface, i.e. the public methods and properties. If
we do change the public interface after we compile the DLL, there is only one option: to switch Version
Compatibility to No Compatibility, remove all references of the DLL from the registry, and recompile.
Once we've done this, we can set Version Compatibility back to Binary Compatibility.
To do this we can use the Package and Deployment Wizard to create a proper setup package, but this is
too much work for a simple test project. We would want to consider doing this for the final component of
course.
For our test purposes, we can simply copy the prjServerTest.dll to the server and use the
Regsvr32.exe program to register the DLL:
We're not quite ready yet though. The server component must be properly registered before RDS can create
the component. This is to keep our server secure. If any object on the server could be connected through
RDS, then anyone could get access to any application or DLL on a server simply by knowing the correct
name. There is a special key in the registry that must have the name of the server component as a subkey.
To register a server component, we must add this subkey.
Unfortunately, this registration does not automatically occur, we have to do it ourselves. We could do this
the hard way and add the key directly to the registry but that's a rather messy way to do it. Instead, let's
write a very small piece of Windows scripting that will allow us to register not only our test server, but any
object in the future.
257
VB6 UML
To run this script you will need to have the Windows Scripting Host installed on the server. It comes with
the Windows NT Option Pack:
You should have the Option Pack installed to access MTS for the next chapter, but in case
you don't, I've provided this registry hack in Appendix B.
If you haven't scripted before, don't worry: we're only going to be writing a few lines of code that resemble
Visual Basic very closely.
Using Notepad, create a new blank file and enter the following code:
Dim ProgID
Dim WSHShell
ProgID = InputBox("Enter the ProgID of the ActiveX server you're interested in?")
WScript.Quit
258
Building and Testing A Server Object
Save the text file with a .VBP extension. We can now simply double-click on it in Windows Explorer to
run it.
Enter the progID for the component we want to register – in this case it's our
prjServerTest.clsServerTest:
Hit OK and an empty key will be added to the registry. If you want to check if it's worked, refer to
Appendix B to find out where the key was entered.
At this point, our server object is ready to rock and roll. All we need is a client.
Personally, I find it rather fun trying to build an application that can kill the system. You can use your
imagination trying to find fun, new ways to put your system through a series of thorough, brutal tests. Just
don't get too carried away. You don't want Dr. Watson to become your best friend.
In a real project, we would ideally first make sure there are no bugs in our server code. This
is done by performing unit tests, i.e. testing the server-component by itself by adding
methods into the server component. Once the server component passes these tests, it is bug
free and ready to be hammered by our client stress test application. We will skip this step
here, but I will show you how to build a test module in a later chapter.
259
VB6 UML
The client component will make a call to the server object, request several recordsets, and then repeat this
operation a set number of times at a set interval (for example, every ten seconds). Once the client
application is built, it can be distributed to several testers who will start the program at the same time. At a
specified time, the application will begin making its calls to the server. In this way, there will be multiple
client requests coming into the server at exactly the same time. This application will not only determine
whether RDS works, but also how many users the server component can handle at one time, i.e. how
scalable is the server component? A sequence diagram for this project might look as follows:
In the References dialog, add references to the Microsoft ActiveX Data Objects 2.0 Library and the
Microsoft Remote Data Sources 2.0 Library:
260
Building and Testing A Server Object
The project will use two forms: a main form called frmTest and a log-in form, called frmLogin. These
forms will perform all of the tasks shown in the sequence diagram above.
This project will start up in a main form that will open the log-in form, allowing the user to try to log-in.
Name the default form frmTest. Add another form but use the Login dialog template:
Make sure frmTest is set as the Startup Object in the Project Properties dialog:
261
VB6 UML
If you open the code window, you will find that there is certain amount of code already there:
Option Explicit
The cmdCancel_Click event handler is suitable for our purposes, but we do need to modify the
cmdOK_Click event:
strConnectResult = frmTest.m_objProxy.ADOConnection(txtUserName.Text, _
txtPassword.Text)
LoginSucceeded = True
Exit Sub
cmdOKError:
End Sub
262
Building and Testing A Server Object
As we'll discuss in a moment, frmTest has an object variable called m_objProxy. This object is a
reference to the server object. Therefore:
frmTest.m_objProxy.ADOConnection(txtUserName.Text, txtPassword.Text)
calls the ADOConnection method of the server object, passing in the username and password. The
ADOConnection method will attempt to make a connection to the database using the password and user
ID that are passed in as parameters. If a connection is made to the database, this function returns True.
Otherwise, it returns the errors. The routine WriteInfo is also in frmTest and will write error
information to a log file.
263
VB6 UML
Control Name
Label lblIterationCount
Timer tmrStartTest
Timer tmrExecute
Data Grid grdProducts
Combo Box cboCategories
Command Button cmdStart
Command Button cmdStop
Command Button cmdExit
List Box lstError
Label lblStatus
Option Explicit
Public m_objProxy
264
Building and Testing A Server Object
If you wish to use HTTP, you will need IIS running on the server where your server object will be running.
If you do not have an IIS web server, you can run the server component using COM. COM will only work
with RDS when the server and client components are on the same machine. To use DCOM you will need to
configure the server and client to use DCOM. You can now see why we declared m_objProxy as a
variant!
265
VB6 UML
This sub will use the DataSpace object. This object contains the following:
The exact syntax for the CreateObject's server parameter depends on whether COM, DCOM, SHTTP or
HTTP is used. The proper syntax for CreateObject is, for each technology, as follows:
COM can only be used with RDS when the server and client components are on the same
machine.
Set the DataSpace object to a new DataSpace object. Once you've done this, set the
InternetTimeout property to 30 seconds:
m_objRDSDataSpace.InternetTimeout = 30000
Set the variable m_objProxy equal to the server object by using the CreateObject method.
If you have the server object running under IIS you can use the following code:
End Sub
You will need to replace ServerName, with the name of your server of course.
If you do not have IIS and have the server object running on the client machine, use the following code:
266
Building and Testing A Server Object
This is all there is to setting up RDS. If you've ever tried to configure hundreds of clients to connect
through DCOM to a server, you'll certainly appreciate how simple RDS is. While it is a bit of a nuisance
adding the registry entry to the server for RDS, once you've done this, you're done setting up RDS.
Of course, we could add extra security to RDS through MTS, but we will not cover that here. The question
is not whether RDS is a simpler solution to implement than DCOM, as it is obvious that RDS is easier, but
whether RDS will actually work. To establish that answer, we have created this test application.
m_strTimeToStart = cStartingTime
m_lngNumberOfTests = cNumberOfIterations
We've used constants because we can easily change their values (they are in the very beginning of our code
and easy to find). We are also using the two variables m_strTimeToStart and
m_lngNumberOfTests in case we want to override these default values. To keep it simple, we'll use the
default values for our code sample, but we can add text boxes so the user could input a value to override
them. This is a lot easier than changing the code for every test. Other options are using an INI file on a
public network drive that all of the clients have access to, or using the registry.
We next call the function GetProxy to set the variable m_objProxy equal to the Server object:
GetProxy
frmLogin.Show vbModal
If the login failed, LoginSucceeded will be False, and we will want to give the user a message and
exit out of the application:
End Sub
267
VB6 UML
cmdStart.Enabled = False
Initialize
End Sub
We can use this to synchronize the clients with the server. We'll also set up the log file by setting the
m_intFileNumber to the next free file number, and using this to open the file specified by the variable
cFileName:
m_intFileNumber = FreeFile
We next need to find the time difference between the server and the client. The time from the server is
formatted hhnnss, so we will format the client time to be the same. Then we can use the DateDiff
function to find the difference between the two times:
Next, we need to figure out how many minutes there are until the beginning of the test. This will be the
time the test begins minus the current time. To make this subtraction we will use m_strTimeToStart,
the time the test will begin, minus the current time:
We'll use the timer control, tmrStartTest, to count down the time between when the program actually
starts, and when the test is supposed to begin running.
This timer control's interval property will be set to one minute. When the timer control has run
m_lngStartingTime intervals, we know that we're within a minute of the starting time. We could set
the interval to every second, but this could use up a lot of the client's resources, and the start time may be
hours before the actual test time. We don't want the program interfering with the user's ability to work on
the client; so one-minute intervals will be better. Let's see what happens when we are measuring in minutes.
Let's say our test is supposed to start at 2:00 p.m. From 2:00 p.m. on the clients are all supposed to be
accessing the server every 30 seconds for one hour. The time the first client starts the program is 1:00:45
p.m. (45 seconds after 1:00 p.m.).
The time difference between the start time and the current time is 59 minutes and 15 seconds (2:00:00 –
1:00:45 p.m.).
268
Building and Testing A Server Object
Since our time is measuring time in one-minute increments, we could begin the test when the timer has run
59 times. Yet, 59 times is 59 minutes. Adding the starting time (1:00:45) to 59 minutes means we will start
the test at 1:59:45, fifteen seconds early.
The whole point of the test is to see how the server handles simultaneous requests, yet using a one-minute
interval on our counter means that our clients can be starting the tests at different times. A few minutes of
calculations will show you that the clients could begin anywhere between fifty-nine seconds early and right-
on-time.
To catch up on these missing seconds, we'll have a second timer control, tmrExecute, that will have its
interval property set to a second. We'll initialize this timer when the number of times
tmrStartTest_Timer event has been raised m_lngStartingTime intervals. Using this control, we
can delay the start of the test to the actual time the test is supposed to begin. If we limit our start times to
being exactly on the minute, we can find the number of seconds till the start of the test by calculating the
number of seconds to the beginning of the next minute.
We're going to use tmrStartTest to count the minutes until the test will begin. We will now initialize
the timer and set the interval to one millisecond so the timer will run immediately. We'll reset the time to
one minute the first time the timer event fires:
tmrStartTest.Interval = 1
tmrStartTest.Enabled = True
Exit Sub
Finally, we'll put in the error trap, and write all errors to the log file:
InitializeError:
End Sub
We still have to make up for those missing seconds we ignored when we started this timer in the
Initialize sub. The number of seconds to the beginning of the next minute will be sixty minus the
current number of seconds.
Open the tmrStartTest_Timer event. If this is the first time the event is being called, the timer will be
set to one millisecond. We'll therefore need to reset the interval to one minute, so we begin by adding code
to set the interval to one minute if that is not already the value:
269
VB6 UML
We now check to see if enough minutes have passed to start the test. If it's time to start the test, we set the
Interval on the tmrExecute timer to be the number of seconds until the beginning of the next minute.
If it's not time to start the test, we change the status label to show the number of remaining minutes:
m_lngNumberOfIntervals = m_lngNumberOfIntervals + 1
Exit Sub
tmrStartTestError:
End Sub
Next, change the status label to Executing and disable the tmrStartTest timer:
PerformTest
270
Building and Testing A Server Object
Next, we increment the number of iterations by one, and change lblIterationCount to show the new
number of times the test has been run:
m_lngIterations = m_lngIterations + 1
lblIterationCount = m_lngIterations
The value in the variable m_lngNumberOfTests is the number of times we should run the test. When
m_lngIterations equals m_lngNumberOfTests we have completed the required number of tests and
must stop:
Exit Sub
tmrExecuteError:
End Sub
Firstly, create the sub called PerformTest that we called in the last routine:
In the first step of the test, we will get the Categories disconnected recordset from the server object:
Next, we place all of the records into the combo box and move to the first item in the combo box:
cboCategories.ListIndex = 0
271
VB6 UML
Next, we move the Categories recordset to the first record, and using the value for CategoryID from the
Categories recordset, we use the server object to get all the products with that ID:
m_recCategories.MoveFirst
Set m_recProducts = m_objProxy.GetProducts _
(m_strUserID, m_strPassword,m_recCategories.Fields("CategoryID").Value)
Finally, if we have the records, set the grid's DataSource equal to the Products recordset to display them:
Exit Sub
PerformTestError:
End Sub
This is a very generic simple sub. You could add another parameter to determine what type
of information should be written if you wanted. You could have error information, timing
information, and information specific to your application. You could write this information
to different log files. You could also write a specific application to analyze the data in the
log files and print summary reports. Use your imagination and make your own specialized
function.
Create a Public Sub called WriteInfo with two parameters for error details and error number:
Normally, this would be in a bas module so all forms could use it, but to keep it simple we
will include it in this form.
Here's the code to write the information to the log file and include the time this error occurred (this allows
you to compare log files and see if all the clients had a problem at this particular time):
Write #m_intFileNumber, "Error :" & v_strErrorDescription & " Number " & _
v_lngErrorNumber & " Time: " & Now
Exit Sub
272
Building and Testing A Server Object
Now we'll add an error trap. Since this means there was a problem writing to the log file, we will display
the error on the form itself (lstError):
WriteInfoError:
End Sub
The cmdStop button will stop the test. To do this, we must close all of the files with the Close command,
set the number of intervals back to 0, shut off the timers, and enable the cmdStart command button:
End Sub
We'll now go into the cmdExit_Click routine and write code notifying the user they are stopping the
application; then we'll set the server object m_objProxy to Nothing and end the application:
End Sub
Finally, we need two Property Lets to hold to set the UserPassword and the UserID:
273
VB6 UML
You can run through the server code by starting Visual Basic on the Server Machine, and
running the server project. You will need to have the server component registered, and
Binary Compatibility set for the server component. Once you have the server component
running under Visual Basic, begin the client object. You should be able to step through the
code in both projects.
This project is very simple, and at this point can only be used to determine if we can successfully retrieve
recordsets using RDS. We can run this test by distributing the client application to several different users,
have them run the application, and then review the logs. We can write the logs to a database on a server, or
to comma delineated text files that can be read into a database. We can then combine the information from
all the clients into one report and see how your different components ran.
The next step would be to add code to determine how efficiently RDS actually works. I modified the client
test application a little so we could measure the time it took to perform the test. The code looked something
like this, but you can design your own testing procedures:
cboCategories.ListIndex = 0
m_recCategories.MoveFirst
PerformTestError:
End Sub
274
Building and Testing A Server Object
Exit Sub
WriteInfoError:
End Sub
On my network with one user, the time to perform the test is always well below one second. When ten users
started making requests, the time to retrieve the Product and Category recordsets averaged between two and
three seconds, with a few responses as long as six seconds. It would seem that our system can only handle a
few simultaneous requests before it is bogged down. Of course, with clients using disconnected recordsets,
it is likely that they will be making requests fairly infrequently. One hundred users working on this system
may only occasionally result in ten simultaneous requests. It's possible that this server object, serving one
hundred or less users will be OK. Try to use only the tools that are required, and perform extensive tests to
find out what is really needed. To be safe, however, I concluded that we ought to think about hosting the
component under MTS to improve scalability. We'll explore MTS in the next chapter.
If you are working with Visual Studio Enterprise, than you will have a tool called the Application
Performance Explorer. The Application Performance Explorer does not come with the Visual Basic
Enterprise, only with Visual Studio Enterprise. This tool can do a thorough diagnostics of your components,
giving you enough detailed information to fine-tune your components.
Summary
Try expanding out the test application. Step through the server code. Run a few stress tests of your own and
see what is happening in the code. You can use INI files in a public directory that all the clients have
access to for the starting time, length of intervals between requests, and number of tests. The only limit is
your imagination. Try writing out what the larger functions and subs are doing in the client application, and
turning these into activity diagrams. Using your activity diagrams, add more functionality to the Client Test
Application. Take these changes in your diagrams and turn them in Visual Basic code.
275
VB6 UML
In answer to our questions, we can see that the RDS works as advertised. In regards to how well it works
without MTS, our component seems to be only able to handle a few simultaneous requests before it gets
bogged down. This may be acceptable with a small number of users, but this system would start having
serious performance problems if the number of users began to increase to hundreds or thousands of users. If
this system were required to handle a large number of users, we would have to upgrade our server
component to run under MTS and run another series of tests. Based on tests like these, we will find that
using MTS with RDS is a simple, workable DNA solution.
I will show you how to design a server component that runs under MTS in the next chapter. Once again, we
will determine what methods are required for the server component by looking at what messages are being
passed to and from the server component in our sequence diagrams. We will expand these messages out into
activity diagrams to map the internal working of our server component. Once the server component is
complete, we will build the Order Entry component of our client.
276
Building The Final Server Object
With MTS
In the previous chapters, we created UML diagrams that we used to design and build our first test version of
the server object. Based on the information we learned from our test project, we will now build our server
component with no public properties (so it can work with RDS and HTTP) running under MTS.
Once again, the design of our server component will focus on the two messages: return recordset and update
recordset. In the course of this chapter, we'll also add one more message for logging an Actor into the
database.
We're going to have to revise our activity diagrams for our server object because we're adding the MTS
framework to our other frameworks. The MTS framework will provide additional steps in our activity
diagrams. In addition, based on the test project we created earlier, we're going to make some improvements
to our code.
This chapter will be quite intensive, so strap yourself in and let's mush.
VB6 UML
MTS is actually something of a misnomer. Although it does having something to do with transactions, it
can do so much more.
Imagine a customer, named Mr. Anderson, who places an order for fifty cans of soup. The order is put in
the system by an Order Entry Clerk and sent to the server object to be saved to the database. The first thing
the server object does is remove fifty cans of soup from the Products table in the database. Next, the server
object sends the order to the shipping department. Finally, the server object tries to charge Mr. Anderson's
credit card for the fifty cans of soup. When the server object tries to do this, the credit card company rejects
the charge because Mr. Anderson has bad credit.
Without a transaction, we'd have to put the fifty cans of soup back into the Products table and notify
shipping to cancel the order. If there are many steps in a process, going backwards and undoing everything
could become extremely complex. Even worse, an error could occur in a step and the previous steps might
not be reversed. If shipping isn't notified, Mr. Anderson will get fifty free cans of soup.
If we make all of the steps part of a single transaction, we can undo all of the changes any time before the
end of the transaction. Our Add New Order transaction would now consist of the following:
If we cancel the transaction at any step before we reach the end transaction point, all the changes made after
begin transaction will be undone automatically. This is the power of transactions. When working with
databases and middle-tier business logic, transactions are an essential part of maintaining database integrity
and simplifying our code.
Another of the major benefits of MTS is that it provides an easy means of scaling our components without
worrying about implementing a complex infrastructure - all we need to add are a few lines of code here and
there.
280
Building The Final Server Object With MTS
It is only when we actually call a routine on the object that an instance of your object is actually created.
This is known as Just In Time (JIT) activation. This saves on resources because we don't actually have an
instance of the object hanging around until we actually need it. Finally, when the routine ends the 'real'
object is destroyed. This is known as As Soon As Possible (ASAP) deactivation. The client is completely
unaware of any of this happening because it's holding a reference to the Context Wrapper object and not the
'real' object.
Resource Pooling
Another resource saving performed by MTS is that it is able to pool resources. This allows limited
resources, such as database connections, to be shared amongst a greater number of clients. Traditionally, a
client would be allocated resources for the lifetime of the application. This quickly uses up the available
resources and hence limited the number of clients.
Under MTS, the resources are pooled so that clients only demand resources when they need them and
release them back into the pool when they are done.
Resource pooling, JIT activation, ASAP deactivation, and many other MTS features are all enabled by
another MTS object called the Context object.
Managing Transactions
In order for us to take advantage of many of the features that MTS offers, we need tell MTS when we have
finished a transaction and whether it was successful or something went wrong. In order to do this, we need
to use the Context object. Each of our objects created under MTS has its own Context object. This object
contains information about the 'real' object's execution environment, such as transactional status and
security information.
SetComplete
SetAbort
If the transaction was successful then we need call SetComplete, and the changes will be committed. If
for any reason an error occurred, then we need to call SetAbort and all changes will be rolled back.
281
VB6 UML
We only have to call SetComplete and SetAbort in public routines before we exit. In private methods
or properties, we don't have to worry about finishing the transaction because we are not ready to destroy the
server object. When the private method or property is done, control will be passed back to the calling
routine, which will be responsible for telling MTS to SetAbort or SetComplete. Remember this rule
for making MTS properties and methods:
Every public method or property must end the transaction, one way or another,
before they finish.
Think of our public methods as the entry and exit points of our object. When we enter, MTS will create the
object; when we leave, we'll be good guests and let our host know we are leaving, by calling
SetComplete or SetAbort.
If we need to call several properties or methods, there should be one method that starts the
transaction, calls all of the other private routines, and (if everything is successful) the initial
method will close the transaction when it is done.
The language that DTC uses to communicate with databases is based on a standard, and
most databases conform to this standard.
When a method or property of an object hosted in MTS makes a connection to the database, the DTC will
automatically start a database transaction. Yet, to end or cancel the transaction, the component must send a
message to the DTC through MTS that the transaction is complete, or has failed.
282
Building The Final Server Object With MTS
A sequence diagram for a component running under MTS performing updates to the database using an ADO
Connection object would look as follows:
Non-Transactional Objects
The best part of MTS is that it can manage any object, even one that is not participating in a transaction.
MTS does not care if our component does not have any transactions: it will still manage our component so
that many clients can access it efficiently. When our non-transactional component tells MTS that is done,
MTS will destroy our component at the appropriate time, just like it did with transactional components. The
only difference is that MTS will not pass any messages onto the DTC.
Fortunately for us, because RDS is also a stateless model, we have already designed our
object to be stateless.
283
VB6 UML
MTS works differently with the New keyword and the CreateObject method, and also introduces a third
method itself:
We'll run through these three ways of creating objects with MTS and Visual Basic now, and then move on
to consider how to make these objects support transactions.
Let me explain that a little more practically. Using the New keyword only creates a Private instance of the
object - that's why we must use it for creating instances of classes with an Instancing property of 1-
Private or 2 - PublicNotCreatable. And because it is a private instance, MTS won't know anything about
our object: a Context object won't be created for it, and our object won't be able to be involved in any
transactions. It will simply run as a regular code.
If an object in MTS is created using CreateObject then MTS will treat it as if it had been created by
client; that is, it will get its own context object. This means that it won't contain any information about the
context of the component that created it and so will outside of the current transaction.
Again, let me state that in a more practical fashion. When a new object is created with CreateInstance,
MTS copies the context information of the creating object into the new object's Context object. This means
the new object will inherit the same security and transactional environment from its creator.
284
Building The Final Server Object With MTS
Here are the possible values for this MTSTransactionMode property, and what these values mean to our
objects.
NotAnMTSObject
Specifies that the object will not be involved with MTS. This is the default option.
NoTransactions
The class will not support transactions. An object context will still be created, but will not participate in
any transactions. This value is useful for when we are just using MTS as a central repository for a
component class. With this value, there will not be a transaction as part of the class.
RequiresTransactions
This MTSTransactionMode value mandates that the object must run within a transaction. When a new
instance of the object is created, if the client has a transaction already running then the object's context will
inherit the existing transaction. If that client does not have a transaction associated with it, MTS will
automatically create a new transaction for the object.
UsesTransactions
This choice indicates that should a transaction be available when the object is created, then that object will
use the existing transaction. If no transaction exists then it will still run - but without any transactional
support.
RequiresNewTransaction
This MTSTransactionMode value indicates that the object will execute within its own transaction. When a
new object is created with this setting, a new transaction is created regardless if the client already has a
transaction.
285
VB6 UML
If you want to learn more about MTS, then I recommend you try 'Professional MTS and
MSMQ with VB and ASP', also published by Wrox Press.
The improvements in our code will include a more sophisticated technique for handling the updating of the
product Units In Stock, additional recordset retrieval methods, and additional update methods. We will also
add parameters to our retrieving and updating methods. Our last example used Private variables that
were being set and retrieved directly in various parts of the code. While this may be acceptable in a small
test application, it isn't good coding practice in a large, final application.
Using Private variables that can be changed at will by any method or property within the class, usually
results in a variable being changed incorrectly somewhere in the code. This is usually impossible to find
and can result in hours or even weeks of debugging. By using ByRef parameters, we can make sure
variables are only changed in routines where we want them to change. If the variable is not supposed to be
changed in a routine, we pass it in ByVal. If there is a problem with a variable, then the problem could
only be located in the places where the variable is passed in by reference. In general, this makes a more
readable, less error-prone component. We will discuss this in more detail later.
286
Building The Final Server Object With MTS
Open up the References dialog and add a reference to the Microsoft ActiveX Data Objects 2.0 Library,
Microsoft Remote Data Services Server 2.0 Library and the Microsoft Transaction Server Type
Library:
Option Explicit
Enum TableName
e_Customers = 0
e_OrderDetails = 1
e_Orders = 2
e_Products = 3
e_Shippers = 4
e_Employees = 5
e_Suppliers = 6
End Enum
287
VB6 UML
Implements ObjectControl
288
Building The Final Server Object With MTS
We are using the * in our queries to return all of the fields. In general, this is not a very
good practice. In a final release application there will be many different queries, some of
which will return only certain fields and others where you will want all fields. To keep it
simple we are using the *.
Also, instead of using string constants for the queries, we could have also used stored
procedures in the database.
For example, we may want to debug our application without the component running under MTS. We may
also want to test the component running under both circumstances. There is also the possibility that for
some reason MTS is not running on the server. If the object is supposed to run under MTS, but MTS has
failed for some reason, we will get an error when we attempt to set any of the Context object properties, or
use the Context object methods.
We can code for the two possibilities by checking for the presence of the Context object. If it doesn't exist,
we have two options: either to raise an error, or work without the object context (that is, run our component
without MTS).
289
VB6 UML
Which option we choose here depends upon the system itself. If the object can still function
without MTS, but perhaps very slowly, we might want to allow the component to still run. If
there are many simultaneous hits and the object will simply lock up without MTS, we would
probably want to raise an error. We can, of course, choose to do both: allow the component
to run but still create an error message. The error message could be placed in the event log,
written a function to e-mail the administrator, or whatever is appropriate.
For this project, we will allow the function to run without MTS, as the current system doesn't have that
many users. If the system later changes, we may have to upgrade the component so that it raises an error
when MTS is not running.
We can avoid this problem by placing any code that was in the Initialize and Terminate events into
the Activate and Deactivate events, respectively, of the ObjectControl interface we
implemented.
The Activate event is fired the first time a client invokes a method on the object. The
Deactivate event is fired every time the method is complete.
End Sub
the call to GetObjectContext returns a reference to the Context for our object, if it's running under
MTS. If it's not running under MTS, it will return Nothing. Since GetObjectContext doesn't raise an
error when the object is not running under MTS, we can still run our component without MTS.
If we could pass in the UserID and Password to the Activate method, we could create the ADO
Connection object in this method. Unfortunately, we cannot add parameters into the Activate event. This
means that all of our public methods will need a UserID and a Password parameter to create the ADO
Connection object.
In the Deactivate event we need to release resources such as the ADO Connection:
End Sub
290
Building The Final Server Object With MTS
We're not quite done yet though. Because we are using the Implements statement Visual Basic is
expecting us to provide implementation for all the methods on the ObjectControl interface. There is one
remaining method: CanBePooled.
The CanBePooled method allows us to set whether our object can be pooled, rather like other resource
type can be pooled. Unfortunately, the current version of MTS has no support for this facility. We do still
have to implement is though, so we shall set it to False:
ObjectControl_CanBePooled = False
End Function
As we discussed earlier, when using MTS, objects should be created the CreateInstance
method of the Context object.
Else
Case "ADODB.Connection"
Set CreateInstance = New ADODB.Connection
Case "ADODB.Recordset"
Set CreateInstance = New ADODB.Recordset
End Select
End If
Exit Function
CreateInstanceError:
End Function
291
VB6 UML
End Sub
End Sub
This was because this one function had to do two completely different things:
It was being used by the client to test if a user's ID and Password were valid
It was being used by the server object to set the ADO connection
These are two, totally unrelated tasks. If a routine has to perform more then one task, these tasks should at
least be related to each other. This is not the case here, so we should make them into two separate routines.
We'll first create a function to set the ADO Connection object and then later we'll create a separate function
to test if the UserID and Password are valid.
292
Building The Final Server Object With MTS
With m_objADOConnection
.CursorLocation = adUseClient
.ConnectionString= "Provider=Microsoft.Jet.OLEDB.3.51;Persist " & _
"Security Info=False;Data Source=" & m_cstrDatabasePath
.Open
End With
Exit Sub
If we wanted to connect through SQL Server to we would use the following ConnectionString:
Build the error trap like you did last time, except this time raise an error to be passed back to the calling
routine. We will create our own error number. This is not technically the best way to assign an error
number, but it will do for now.
We will discuss error handling in more detail in Chapter 12, as this chapter is already
complicated enough.
SetADOConnectionError:
End If
End Sub
Notice that we made the error source both the name of our function and the name of any source that may be
in the Error object. By combining sources like this, the final error message that reaches the client will list
all of the methods and objects that this error has passed through. This is extremely useful when debugging.
293
VB6 UML
Making the mistake of setting the variable equal to something that it should not be can often
result in one of the hardest bugs to find, as it is a subtle error that can often be missed as
you go through the code. By only allowing your private variables to be set and retrieved in
either a function or a property (or a sub by using by reference parameters) you can never
make the mistake of setting the routine on the wrong side of an equal sign.
We will create a function called GetADOConnection that will cause the Visual Basic compiler to raise
an error should this happen. Using functions or properties to set and retrieve the Private variables will
allow the compiler to make sure we do not place something on the wrong side of the equal sign. We can
also do validation checks in our functions and properties that we couldn't do if we just use Private
variables.
The function SetADOConnection must be called before calling GetADOConnection so that the
m_objADOConnection variable is set to a Connection object (we cannot get a connection that has not
been created yet!). Therefore, we will check to see if the variable m_objADOConnection has been set, if
it has not, we will raise an error.
Else
Set GetADOConnection = m_objADOConnection
End If
End Function
We have assigned a value of 2001 for the error. We can use this number on the client to handle this
particular error.
With GetADOConnection
294
Building The Final Server Object With MTS
End With
End Sub
Primary Actors
Sales Representative, Vice President Sales, Sales Manager, Inside Sales Coordinator
Secondary Actors
None
Starting Point
The Actor attempts to enter the system
Ending Point
The Actor is either granted or denied access to the system
Measurable Result
An Actor is granted access to a system
Flow of Events
The Actor enters their UserID and Password
The Actor submits the information to the system
The Actor is granted access to the system
Business Rules
Invalid UserID and Password
Outstanding Issues
None
You could add some more business rules. For example, a rule defining the UserID and
295
VB6 UML
Password.
The function that we'll create, to check the UserID and Password, will check if the user is allowed into the
database by attempting to make a connection to the database using the private SetADOConnection
function.
This brings up an interesting point. The SetADOConnection function will raise an error if the user is
denied access. This error will be passed up to our check UserID and Password function. It wouldn't be too
user-friendly to raise this error back to the client if the user is not allowed to use the database. It's possible
that the developer of the client may want to ignore an invalid sign-in, and they would have to use On
Error Resume Next and handle all errors themselves.
It's much friendlier to simply return a value of False for the function. If the programmer, making the
client, wants to raise an error when the function returns False they can. If they don't want to raise an
error, they can just ignore the False value.
We are still missing one thing, though. It's likely the programmer does want to inform the user that they
failed validation on the database. The best way to return the error values is to add an extra parameter that is
passed in by reference. We can then set the value of the error variable on the client by setting the by
reference parameter in our validation routine.
There is one word of caution here. If we stop to think about it for a moment, By Reference
variables are fairly complicated when we are working with a server object. The RDS
(DCOM) is providing us a connection between the server object on the server and the client
application on the client. A by reference parameter that is being updated on the server must
result in the variable on the client also being updated. This requires quite a bit of
communication. By Reference parameters are extremely expensive when we are using
DCOM or RDS with HTTP.
In this case, we only call this function once when the user initially logs-in. A by reference variable is
acceptable here, but by reference variables should generally be used sparingly with server objects.
296
Building The Final Server Object With MTS
Make a function called ValidUserIDPassword with two by value parameters, v_strUserID and
v_strPassword, and one by reference parameter r_strErrorDetails with a default value of
Empty. Add in an error handler:
Call the SetADOConnection function and try to make a connection to the database. If the connection
failed, SetADOConnection will raise an error, and we will jump down to the error trap
ValidUserIDPasswordError. If there is no error, then we will continue to the next line of code:
If we have not jumped into the error trap, there are no errors and we can set the return value of
ValidUserIDPassword to True and call SetComplete:
297
VB6 UML
ValidUserIDPassword = True
SetComplete
Exit Function
If there was an error making the connection, get the ADO error collection and set the by reference
parameter r_strErrorDetails equal to these errors.
ValidUserIDPasswordError:
When working with optional parameters, you can only use the IsMissing function to determine if an
optional parameter has been passed in if the parameter is a variant. IsMissing does not work with all
other data types. This is the reason why I gave the optional parameter a default value: Empty. If the value
of the optional parameter is equal to Empty, then we know nothing was passed into this parameter, and you
can ignore it. In our case, we are giving the user the option of retrieving the error details with this optional
parameter. The client application could then build a message box to inform the user the reason why their
log-in failed.
Next, we have to notify MTS that our object is no longer needed. We have had an error, yet do we really
need to call SetAbort? SetAbort is used to rollback changes, and we have not actually done any
updates or changes in this function.
We can use MTS as an object manager even when we are not technically doing a database transaction. In
this case, we are not doing any transaction; we are calling a method in our server object to find out if a user
is allowed into the database. We only need to notify MTS that we are done with the server object; both
SetComplete and SetAbort will inform MTS that we are done and the server object is no longer
needed. We will use SetAbort in all our error traps for consistency:
SetAbort
ValidUserIDPassword = False
End Function
We could also use MTS security to make sure a user is allowed to access the component.
MTS security is an advanced topic, so we won't discuss it here.
Our object now has all the basic functionality that it needs to handle MTS and ADO. Now let's move onto
creating the functions and methods that will allow our object to fulfill its main purpose.
298
Building The Final Server Object With MTS
Retrieving a Recordset
In the last chapter we drew activity diagrams for retrieving Products, Customer and Category recordsets. To
keep things simple in our test application, the three different types of data were all retrieved in the same
way.
We could build a generic routine to retrieve data for any type of data. This routine will use a Select
Case statement to determine the correct type of data to retrieve. Our generic routine would have an activity
diagram that would look as follows:
This diagram seems deceptively simple. You have to realize that everything here is pseudo code, and some
of these steps are a complex operation in themselves.
Take a look at Determine which Recordset to Retrieve. To determine the recordset, we would need a
parameter that would identify which records we want to retrieve. If we make it an enumerated type then the
programmer could select the correct type from a drop list if they were programming in Visual Basic. Using
that parameter in a Select Case statement we would be able to select the correct record. Sounds good so
far.
299
VB6 UML
Now, we'll also want to retrieve different views of the different tables. For example,
we may only want to retrieve an Employee's last name and ID when filling in an order, but we might not
want their address and other information. On the other hand, when the manager is editing an employee's
record they will want all of the information on the employee. This means that we might want several
different views for each table.
Let's say we want three different views of each table. Since there are seven tables, we will need seven times
three or twenty-one Cases in our Select Case. Figuring five lines of code per case that comes out to
105 lines of code just to figure out which recordset we need. Have you ever tried to debug a function with
over 100 lines of code? It isn't much fun. Once we start adding sophisticated error handling and checking,
then this routine could quickly bloat to several hundred lines of code. We may have made one super
function that can be used to retrieve any recordset, but we have also created a huge, bloated monster that
could be a maintenance and debugging nightmare.
A good rule of thumb is that functions should do as few things as possible. Everything in the routine should
be related to accomplishing one task. The task Update Order will add the order to the database, bill the
customer, and remove products from stock. There is no way to make it smaller without sacrificing good
design structure. Each activity, though, is part of one single task: Updating an Order.
This does mean that our server component will have a lot of separate methods in it to handle retrieving
different types of data. Yet, these methods are small, perform only one task, and it will be very clear to the
programmer using the object which method to use when. If two tasks are part of a larger task, they belong
in one routine. If two tasks do similar things (i.e. they are based on the same pattern) but are not part of
some larger task, they should be in separate routines and there should be a private function that performs
the tasks they have in common.
300
Building The Final Server Object With MTS
The activity diagram for the generic method GetRecordset looks as follows:
Exit Sub
GetRecordSetError:
End Sub
You'll note that we have passed our recordset parameter in by reference. This is so that any changes we
make will actually be made to our recordset variable.
301
VB6 UML
While passing parameters by reference between the client object and the server object is an
expensive task, when it is done within the server component there is no extra overhead. This
is because everything within the object is in the same process.
Now the generic method is in place we can start to deal with some more specific retrieval functions.
302
Building The Final Server Object With MTS
Begin by creating a function called ReturnProductRecordset with the three above variables:
Declare a temporary recordset variable that can be used to place the records in, and if successful, return to
the client. Put in an error handler:
We have to first create an ADO Connection object using the SetADOConnection object:
Notice that our activity diagram has a decision point here; if there is an error, we are supposed to raise the
error, cancel the transaction and exit the Function. Where is this code?
Well, we set an error handler in our first line of code. Therefore, if there is an error, the error will be passed
to ReturnProductsRecordset, which will then jump into its error trap. The error trap will provide all
of the activities listed in the activity diagram for the error path.
Next we call GetRecordset, passing in the temporary Products recordset and the query to retrieve the
record:
Before we leave the function, we must call SetComplete to let MTS know that we are done with this
object. Once again, we have not made any changes to the database. This method is using MTS to manage
the server object, not to manage a database transaction:
SetComplete
We must now close the connection, destroy our local variable and exit the function so we do not fall into
the error trap:
CloseADOConnection
Set recProducts = Nothing
Exit Function
303
VB6 UML
ReturnProductsRecordSetError:
CloseADOConnection
SetAbort
Err.Raise Err.Number, "ReturnProducts" & " " & Err.Source & " " & _
m_strErrorDetails
End Function
Now that we've built one of these Return Recordset functions we can knock up the others in no time. As we
saw with the Test project all these routines are very similar.
304
Building The Final Server Object With MTS
SetComplete
CloseADOConnection
Set recCustomers = Nothing
Exit Function
ReturnCustomerRecordSetError:
CloseADOConnection
SetAbort
End Function
GetADOConnection.Close
SetComplete
Set recOrders = Nothing
Exit Function
ReturnOrdersRecordSetError:
305
VB6 UML
SetAbort
Err.Raise Err.Number, "ReturnOrders" & " " & Err.Source, _
m_strErrorDetails
End Function
GetADOConnection.Close
SetComplete
Set recOrderDetails = Nothing
Exit Function
ReturnOrderDetailsRecordSetError:
GetADOConnection.Close
SetAbort
Err.Raise Err.Number, "ReturnOrderDetails" & " " & Err.Source, _
m_strErrorDetails
End Function
It's a short step to also add routines for Employees and Shippers.
Now we come to the tricky part - writing data back to the database. In the previous chapter we spent some
time discussing how to handle such issues as multiple disconnected recordsets and conflicts that might arise
as a result. You'll see some of what we developed in the last chapter here but we'll also consider some
additional problems that might occur.
306
Building The Final Server Object With MTS
When we start getting into updating records, we may have to start looking at the use cases
and the rules associated with them before we can decide how to make some of the update
methods.
Creating a generic Update routine that is Public allows us to make the more specific Updates Private so that
the transaction can be set and controlled by this routine.
307
VB6 UML
Begin by making a public method called UpdateRecordset that has the above parameters:
We are not passing the recordset variables in by reference as this function will be called
many times and by reference is very expensive when working with DCOM or RDS over
HTTP.
Next, we will determine which record to use with a Select Case. Once we know which records are
being updated we can call the appropriate method:
Case e_Customers
Set UpdateRecordset = UpdateCustomerRS(v_recClientRecordSet)
Case e_Products
Set UpdateRecordset = UpdateProductsRS(v_recClientRecordSet)
Case e_OrderDetails
Set UpdateRecordset = UpdateOrderDetailsRS(v_recClientRecordSet)
308
Building The Final Server Object With MTS
Case e_Orders
Set UpdateRecordset = UpdateOrderRS(v_recClientRecordSet)
End Select
SetComplete
CloseADOConnection
Exit Function
UpdateRecordsetError:
CloseADOConnection
SetAbort
m_strErrorDetails = "Error Number: " & Err.Number & " Error " & _
"Description: " & Err.Description
Err.Raise Err.Number, "ReturnCustomers" & " " & Err.Source, m_strErrorDetails
End Function
Now that the generic routine is in place we need to code the four Update methods that are called. Although
there is a great deal of similarity between them, they all have their own particular considerations.
The reason why we're not creating UpdateProductRS first is because Updating Products
has some new considerations but Updating Customers is almost identical.
To keep this function simple, we are not going to handle any conflicts arising from a Customer record being
edited by more than one person. This doesn't seem too much of a cheat because realistically, a Customer
record should not be being changed by two people at the same time. If a conflict does occur,
UpdateCustomerRS will simply raise an error.
309
VB6 UML
The activity diagram will look quite reminiscent to that for Update Product in the previous chapter:
Start the function by setting a local recordset variable to the passed in Customer recordset:
310
Building The Final Server Object With MTS
You'll see from the activity diagram that there is a decision at his point. This is because updating the
recordset can occur in two situations:
We can tell if the update is adding a new record or simply editing an existing record by checking the
EditMode property of the Recordset object.
Adding a new Customer is easy all we have to do is call Update. Other updates have a few more
complications, and we'll get to them in due time:
recCustomers.Update
Editing an existing record is trickier as we have to perform a batch update and then check for conflicting
records:
Else
On Error Resume Next
recCustomers.Filter = adFilterPendingRecords
recCustomers.UpdateBatch adAffectGroup
recCustomers.Filter = adFilterConflictingRecords
In this case, we simply want to raise an error if there are any conflicts. In some of the other updates,
however, we will need to do something about it:
End If
recCustomers.Filter = adFilterNone
Set recCustomers.ActiveConnection = Nothing
Set UpdateCustomerRS = recCustomers
Set recCustomers = Nothing
End Function
Now we've seen the basics of an UpdateRS function, let's consider a more complex example.
311
VB6 UML
Updating Products
The Products table can be updated in two different ways, changes in the Units In Stock field when an order
is placed, and when product information is changed because of a change in inventory, product name, etc. If
we look at the use cases associated with Products (Update, Edit and Delete Product use cases), we can see
that these can affect all of the Products table's fields except ProductID. It would be highly unlikely that two
people in charge of Products would change the same product at exactly the same time. In our case, there is
only one person in charge of products anyway.
Scenarios
What would happen, though, if someone accidentally knocked a whole case of Rössle Sauerkraut off the top
shelf while doing inventory, destroying five cans? The person in charge of products would have to remove
five cans of Rössle Sauerkraut from the Products table.
However, adding or removing items from the inventory is something that is not only done by the person in
charge of Products; the Order Entry clerks also do it when they are taking orders. Imagine the following
scenario:
The Products person goes to the Edit Product screen and pulls up information on Rössle Sauerkraut. The
Order Entry person takes an order for four cans of Rössle Sauerkraut and sends the order to the database.
Four cans of Rössle Sauerkraut are removed from the database. The Products person removes five cans of
Rössle Sauerkraut, but the number of products that is on the Products person's client computer does not
show the cans removed by the Order Entry person. When the Products person goes to update the
information, there will be a conflict.
A scenario shows the details of how an actor interacts with the system using real
data.
There are an unlimited number of possible scenarios. Working out a few, representative scenarios, helps us
see the different ways an Actor uses the system. Scenarios can be grouped by the tasks that they
accomplish. These tasks will then become a use case. There will only be a limited number of use cases that
describe these single tasks. A scenario represents one of the many different real occurrences of that single
task. By looking at several scenarios for a single task, one can see all the possible steps in the task and all
of the possible alternative flows.
We will have to add activities into our activity diagrams that deal with these conflicts, and add code into
our Update methods to reconcile these differences. Keep in mind that it is also possible that the Products
person makes the update first and that the Customer's update will need to be reconciled. We can also see
that our current Create Order use case is not complete. We have not mapped out any of the details of how
the database is actually updated. It's fairly common to find that your initial use cases are not complete, and
that you have to gather more information. Let us turn to our Create Order use case first.
As each order is placed and updated to the database, the products ordered must be subtracted from the Units
In Stock for that product. Looking back at our Business Rules associated with Order Entry, we see that we
do not have any rules about removing the ordered products from stock.
We are also missing Business Rules on what to do when there is not enough of a product in stock to fill an
order, an ordered product is out of stock, or a particular product is running low. These are all situations that
must have rules associated with them. We would obviously need to ask a few more questions to our users.
312
Building The Final Server Object With MTS
Overview
The purpose of this use case is to update the database with the Order information,
remove ordered items from stock, and bill the customer.
Primary Actor
Sales Person
Secondary Actor
None
Starting Point
The Actor requests to save an order
End Point
An Order is saved or not saved to the database
Measurable Result
The Order is added to the database
Flow of Events
The Actor requests an Order be saved
Business Rules
Remove ordered Products from Units in Stock
Bill Customer
Outstanding Issues
None
While it may seem strange to be removing items before they are actually shipped, this should not create a
problem. We can perform a query on the Order Details table that retrieves all of the products that have not
shipped, i.e. there is no entry for the shipped date. If this query is grouped by product, we can determine the
number of units of each product that have been ordered but not shipped. If we add this number to the
current units in stock, we will get the number of units that are actually in stock. Thus, if we need to do an
inventory of the products, we can quickly determine the number of units that are actually in stock.
313
VB6 UML
Overview
The main purpose of this use case is to create a new product order for a customer.
Primary Actor
Sales Representative
Secondary Actor
None
Starting Point
The Actor requests to make a new order.
End Point
An Order is either created or cancelled.
Measurable Result
An Order is created.
Flow of Events
The actor received the phone call from a customer. The actor is asked by the customer to
place an order. The actor opens up the order entry form. The actor retrieves the name and
address from the customer. The actor enters the name in the system. The actor confirms
that the customer's personal information is correct. If the information is correct, the actor
requests the order information. If the customer information is incorrect the actor updates
the customer information. The actor selects a product category. The actor selects the
products. The actor types a quantity for each product. The actor selects a shipper. The
actor confirms with the customer that all the information is correct. The actor completes
the order details. The actor saves to the database.
Business Rules
Restrict Order Create
Customer can place Orders
Order
Order Fields
Outstanding Issues
None
314
Building The Final Server Object With MTS
You may be wondering why we haven't created a use case for Remove Ordered Products
from Units In Stock. The main reason it cannot be a use case is that nothing external to the
system is requesting these items to be removed. An internal process (the request to update
orders) is making a request to remove items from stock. No Actor is actually making this
request. This has to be a Business Rule.
When we actually sit down and try to figure out how to code all of these rules, we want to try to keep things
as simple as possible. Let's imagine that we decide to keep a Products recordset on the client side, and
adjust this recordset's Units In Stock field as items are added to Order Details. When we return this Product
recordset to the server, we will first have to reconcile it with the information in the database (as it is
possible that the Units In Stock have changed by someone else placing an order).
On the other hand, we could also make no changes to the Products recordset on the client. In this case, the
client's Product recordset would only be used to let the Order Entry Clerk know what the different products
are and how many are in stock at the time the order began. The adjustment to the Units In Stock field would
occur on the server object instead. When an Order is sent to the server to be saved, the server object would
get a copy of the Products table, change the Units In Stock field for each item ordered, and then save the
changes. As long as the records were locked as the edits were made, there would be no need to for any
reconciliation. This is definitely a lot easier then making the changes on the client side and later reconciling
values. After an Order is completed, the client application could request an updated Product recordset from
the server object.
In the case of the Products person changing the Units In Stock, the changes would obviously have to be
made on the client. We will look at how both of these different situations are actually handled. Let's
continue with the Create Order. Using these new rules and use cases, and the MTS, ADO, and three-tier
frameworks we will make the activity diagrams.
Pass a copy of the client's Products recordset into the UpdateProductsRS function
Test to see if it's a new Product or an edit
If it's new then add new product
If it's an edit then create local Products recordset and set it equal to client recordset
Set local Products recordset ADO Connection
Set local Products recordset to PendingChanges to limit records to those that were changed
Batch update the Products recordset
Reconcile any conflicts. If there were no errors, set connection object to nothing, remove the filter,
return the recordset, and close the ADO connection
If there were any conflicts or error raise an error
315
VB6 UML
Reconcile Products is a complete function and will have its own activity diagram.
316
Building The Final Server Object With MTS
If we are working with an existing record, we must take into account the possibility of there being a
conflict in the records.
If we are adding a new record, then we do not have to be concerned about reconciling records.
Create a Products recordset variable and a long variable that we'll need later loop through the fields:
Then add the main decision branch for adding a new record or editing an existing:
We will begin with adding a new record. The Products table has an auto-incrementing ProductID field.
However, in order for this to work we need a continuous connection to the database. If we call the AddNew
method on a disconnected Products recordset, it can not assign a value to the ProductID field. We will
therefore have to create a recordset variable that we can attach to the database using a server-side cursor -
recProducts. We can then perform an AddNew on the recordset, and the database will supply us with
the next available ProductID. We can then set all of the fields in the connected recordset, recProducts,
to the new values that were passed in from the client in the disconnected recordset variable,
v_recClientRecordset.
We will create the connected recordset by performing a query on the Products table requesting the record
with ProductID = 0. As there is no record with this value, we will get a recordset with no values. We
can then use this recordset to perform an AddNew.
Begin by creating the new recordset using the CreateInstance function we made earlier:
Set the Connection, the LockType, the CursorLocation, and open the recordset:
With recProducts
.Source = m_cstrProductQuery & " Where " & _
m_cstrProductsProductIDField & "=" & 0
.ActiveConnection = GetADOConnection
.LockType = adLockPessimistic
.CursorLocation = adUseServer
.CursorType = adOpenKeyset
.Open
317
VB6 UML
Start an AddNew:
.AddNew
Loop through all of the fields of the disconnected recordset to map their values to the corresponding fields
on the connected recordset:
No we will attempt to perform an update. We don't have to worry about conflicts as this is a new record:
.Update
End With
Else
When we are editing, we could have a conflict. To handle this, we will have to check the records using a
reconciliation function.
Since the client recordset is passed in ByVal, we cannot make any changes to it. We will therefore use our
local recordset variable equal to the client variable that we can manipulate.
Start by setting the local recordset variable equal to the client recordset variable:
Shut off the error handle so an error is not raised if there are conflicting records:
recProducts.Filter = adFilterPendingRecords
recProducts.UpdateBatch adAffectGroup
Using the adFilterConflictingRecords filter we can reduce our recordset to rows containing
conflicts:
318
Building The Final Server Object With MTS
recProducts.Filter = adFilterConflictingRecords
We can test for conflicts by checking the RecordCount property of the recordset. If there no conflicts all
records would have been filtered out and so the count would be zero. A count of anything else indicates that
there are conflicts. We want to try and reconcile the conflicts and so we'll call a separate function (which
we'll build next) to handle the reconciliation for us.
However, suppose there was an error in the Reconcile function. This would also cause
UpdateProductsRS to fail, all because of a few records. This would rollback everything we'd
accomplished, i.e. all the records that didn't have a conflict. This seems counter productive and the most
obvious way to avoid this happening is if we started a new transaction for the reconciliation. So we could
code:
If ReconcileProducts(recProducts) Then
GetADOConnection.RollbackTrans
End If
End If
Looks great, but there is one serious flaw in this logic. MTS does not support nested transactions.
The above code tries to start a second transaction within the first. Remember, we built our component to
run under transactions, which means a transaction will have begun when our component was initialised.
This will be the first transaction. The above code attempts to start a second one. This is a nested
transaction.
MTS does not support nested transactions! You can run separate concurrent
transactions but not transactions with transactions.
There are some fairly complicated ways to perform separate transactions. These can involve creating
another component that runs under transactions, and calling this component from your current component.
We won't get into these methods, as we can solve the problem in a much easier way.
Our solution is to try to the fix the conflicting records one record at a time. If our attempt to fix the record
fails, we will move on to the next record. In this way, we'll only update the conflicting records that can be
fixed, and ignore the ones that cannot be fixed. The correct code would simply be:
319
VB6 UML
recProducts.Filter = adFilterNone
End If
CloseADOConnection
Set recProducts = Nothing
Exit Function
UpdateProductsRSError:
recProducts.Filter = adFilterNone
Set recProducts.ActiveConnection = Nothing
CloseADOConnection
End Function
Now we have to code the ReconcileProducts function that caused so much trouble earlier.
320
Building The Final Server Object With MTS
The function begins by building a local recordset variable. This variable will contain the product that has a
ProductID equal to one of the conflicting records. We'll perform an edit on the local recordset variable,
and set the value of the Units In Stock field of the local variable, the one we are checking for a conflict,
equal to the value passed in from the client.
321
VB6 UML
Call the Resynch method, like before, to get the underlying values currently in the database:
Start the loop to iterate through all the rows that are conflicting:
Do Until r_recProducts.EOF
For each row, rebuild the recordset variable by setting the ProductID of the query equal to the current
conflicting row:
With recProducts
.Source = m_cstrProductQuery & " WHERE " & m_cstrProductsProductIDField _
& "=" & r_recProducts.Fields(m_cstrProductsProductIDField)
.ActiveConnection = GetADOConnection
.LockType = adLockPessimistic
.CursorLocation = adUseServer
.CursorType = adOpenKeyset
.Open
Now we will set the Units in Stock Field in the local recordset variable equal to the value in the recordset
from the client:
If Not IsNull(r_recProducts.Fields(m_cstrProductsUnitsInStockField).Value) _
Then
.Fields(m_cstrProductsUnitsInStockField).Value = _
r_recProducts.Fields(m_cstrProductsUnitsInStockField).Value
End If
With .Fields(m_cstrProductsUnitsInStockField)
.Value = _
r_recProducts.Fields(m_cstrProductsUnitsInStockField).UnderlyingValue _
+ (.Value - _
recProducts.Fields(m_cstrProductsUnitsInStockField).OriginalValue)
End With
Note that we are only checking for conflicts in the Units In Stock field. We are ignoring the
other fields, but in a real application you should do something to check if perhaps another
field was the problem.
322
Building The Final Server Object With MTS
.UpdateBatch
We'll check for problems once again by checking for conflicting records. At this point, there should not be
any, as we have a lock on the recordset at the moment:
.Filter = adFilterConflictingRecords
We will simply build a string with the ProductIDs of the products whose update failed:
We will now cancel the update, which will throw away this change:
.CancelUpdate
End If
.Close
End With
Then we move to the next record and loop the whole process:
r_recProducts.MoveNext
Loop
We'll return the value of strError to the calling procedure. If there were no problems then this will be
empty, otherwise it will contain the ProductIDs of the problem items:
ReconcileProducts = strError
Exit Function
ReconcileProductsError:
End Function
323
VB6 UML
When a new Order is placed it actually updates both the Orders and the Order Details table. Therefore, we
need an update function for both tables. We will focus on the Order Details table first, as it contains
information on the Products.
This routine will actually be quite simple. This is because it requires two quite complex and separate
processes, each of which we'll split off into their own function. Therefore, all we really need to do is call
each of them.
I've chose to create a separate function, at this point, to deal with updating the Order Details. This is
because there are two scenarios in which Order Details need to be updated. It can either be updated directly,
as in this case, or, as we'll see later, by placing a order. Therefore, as the code would be the same, I've
chosen to create another function called SaveOrderDetailsRS that can be called when appropriate.
The second routine that we need to call deals with removing items from stock:
With recOrderDetails
End With
We'll now close the recordset variable and set the Connection and the object to Nothing:
recOrderDetails.Close
Set recOrderDetails.ActiveConnection = Nothing
Set recOrderDetails = Nothing
Exit Function
324
Building The Final Server Object With MTS
UpdateOrderDetailsRSError:
End Function
This function only deals with adding an entry and not editing an existing one. It should be
too much effort however for you to add the edit code.
Now that this function is complete we need to code the two routines it calls: SaveOrderDetailsRS and
RemoveFromStock method.
Once again, we'll create a local recordset variable, copy the fields into this variable from the client
recordset, and then update the local recordset to the database. The only real difference is that we also have
to set the OrderID.
The function has an optional OrderID parameter for when we are using this function to
add a new OrderID through the SubmitNewOrder method that we'll code later.
With recOrderDetails
.Source = m_cstrOrderDetailsQuery & " Where " & _
m_cstrOrderDetailsOrderIDField & "=" & 0
.ActiveConnection = GetADOConnection
.LockType = adLockPessimistic
.CursorLocation = adUseServer
.CursorType = adOpenKeyset
.Open
Do Until v_recClientRecordset.EOF
.AddNew
If LCase(v_recClientRecordset.Fields(lngFieldCounter).Name) <> _
LCase(m_cstrOrderDetailsProductNameField) Then
325
VB6 UML
End If
Next
v_recClientRecordset.MoveNext
If v_lngOrderID <> 0 Then
.Fields(m_cstrOrderDetailsOrderIDField).Value = v_lngOrderID
End If
.Update
Loop
End With
End Function
326
Building The Final Server Object With MTS
Create the Private subroutine RemoveFromStock with two parameters, both of long type, one called
v_lngProdID for the ProductID, and the other v_lngQuantity for the amount of the product to be
removed from the Products table:
Set all of the properties of the Products recordset. We are going to set the LockType to be pessimistic
because we don't want someone changing this record while we are editing it:
327
VB6 UML
With recProducts
.Source=m_cstrProductQuery & " Where " & m_cstrProductsProductIDField _
& "=" & v_lngProdID
.ActiveConnection = GetADOConnection
.LockType = adLockPessimistic
.CursorLocation = adUseClient
.CursorType = adOpenKeyset
.Open
End With
It's unlikely that the product was deleted while the order was being entered, but we should still check to
make sure a record came back:
If recProducts.RecordCount = 0 Then
Err.Raise 2004, "RemoveFromStock", "Product is not in the database"
End If
If there are not enough, add the amount that is missing to the On Order field:
intDifference = v_lngQuantity - _
recProducts.Fields(m_cstrProductsUnitsInStockField)
recProducts.Fields(m_cstrProductsUnitsOnOrderField) = _
recProducts.Fields(m_cstrProductsUnitsOnOrderField) + intDifference
recProducts.Fields(m_cstrProductsUnitsInStockField) = 0
If there are enough in stock, first remove the amount ordered from Units In Stock:
Else
recProducts.Fields(m_cstrProductsUnitsInStockField) = _
recProducts.Fields(m_cstrProductsUnitsInStockField) - _
v_lngQuantity
Check to see if this order has put the total units in stock below the reorder level:
If recProducts.Fields(m_cstrProductsUnitsInStockField) < _
recProducts.Fields(m_cstrProductsReorderLevelField) Then
328
Building The Final Server Object With MTS
recProducts.Fields(m_cstrProductsUnitsOnOrderField) = _
recProducts.Fields(m_cstrProductsUnitsOnOrderField) + _
v_lngQuantity
End If
End If
recProducts.Update
Set recProducts = Nothing
Exit Sub
RemoveFromStockError:
End Sub
I would like to make one comment here. If there are shortages, we are simply increasing the
number of units on order. While this good, it may not be good enough. The Products person
could periodically check the database for all products that have more than zero units on
order to find products that need to be ordered, but it would be better if our server object
notified the Products person. We could create another server component that sends an e-
mail message to the Products person, or sends a report to them. There are any number of
possibilities, just make sure whatever you choose works for your users.
This method will be similar to the other update methods. The Orders table also has an auto increment ID
field. We will have to perform the AddNew in the same manner as we did for Products and Order Details. If
there is a conflict with the records we'll just raise an error.
329
VB6 UML
With recOrder
.Source = m_cstrOrdersQuery & " Where " & m_cstrOrderDetailsOrderID & _
"=" & 0
.ActiveConnection = GetADOConnection
.LockType = adLockPessimistic
.CursorLocation = adUseServer
.CursorType = adOpenKeyset
.Open
.AddNew
For lngFieldCounter= 0 To v_recClientRecordSet.Fields.Count- 1
330
Building The Final Server Object With MTS
Next
.Update
End With
Else
recOrder.Filter = adFilterPendingRecords
recOrder.UpdateBatch
recOrder.Filter = adFilterConflictingRecords
recOrder.Filter = adFilterNone
End If
Exit Function
UpdateOrderRSError:
End Function
That's the last Update routine that I'm going to show. You should, however, be able to go
ahead and make ones for Employees and Shippers if you so desire.
Our server object is almost complete. We just need to add a single routine for adding a new order that will
deal with updating multiple tables.
331
VB6 UML
332
Building The Final Server Object With MTS
Create a new public subroutine called SubmitNewOrder. We will be passing in the UserID and password,
and the Orders and Order Details recordsets. We do not need the Products recordset, as we can determine
what items to remove from stock by using the Order Details table:
Create local recordset variables for the Order Details and Order recordsets:
We want to update the Order Details, Orders, and Products recordset under one transaction, however, as we
saw earlier we can't use nested transactions. To get round this we'll use the SaveOrderDetailsRS
routine we built earlier, you can see now why we added an Optional parameter.
With recOrderDetails
End With
SetComplete
CloseADOConnection
Exit Sub
333
VB6 UML
If there are errors, we call SetAbort and raise the appropriate error:
UpdateOrderError:
CloseADOConnection
SetAbort
m_strErrorDetails="Error Number: " & Err.Number & " Error Description: " _
& Err.Description
Err.Raise Err.Number, "ReturnCustomers" & " " & Err.Source, m_strErrorDetails
End Sub
The code for our server object is now complete. We still have to compile it and host it in MTS though. I
won't go into that here, as it's quite straightforward. However, you can refer to Appendices C and D for
information on running and debugging with MTS.
Summary
As you may have gathered, a server object built to run under the three-tier framework contains some very
complex methods.
No matter how complicated this process was using UML diagrams, trying to do the same thing without
them would have been a thousand times more difficult. When I was trying to figure out how to design the
system, I had to draw it all out. OK, I confess, I used a pencil and made some rather ugly looking activity
diagrams mapping everything out. The initial concepts were, well, extremely different from the final
diagrams above.
The process of mapping it all out using diagrams was fairly easy. If you don't believe me, sit down and try
to figure out how to code the methods like UpdateProducts without activity diagrams. I guarantee you,
a few hours of shuffling code around and you will quickly turn to UML diagrams.
For simple methods and properties, you can rely on other UML diagrams, such as sequence diagrams, to
map out your code. Complicated methods, though, need activity diagrams to help you visualize your
methods, see the flow of your code, and work out the best possible code solution.
Maintain the integrity of the database with methods that correct data conflicts.
Pass disconnected recordsets to the client through DCOM or HTTP using RDS.
Safely update the database
This is a very powerful server object, and I hope that you will be able to use it as the basis for your future
projects.
In our next few chapters, we will be designing and building the other half of our system: the client-side
objects.
334
Coding the Customer Client
Component
So far, we've coded the middle-tier, which handles the fine details of retrieving, updating and reconciling
recordsets. This server component has all the necessary implementation to handle almost every aspect of the
Northwind system. However, this server component is next to useless unless it has a client to talk to. This is
what we're going to build in this chapter.
In Chapters 5 and 8, we saw how we would be constructing a three-tier class hierarchy. Now we have to
think about how to implement it. This architecture has a few interesting points of its own that we'll be
exploring in this chapter.
How to apply the four rules of building Visual Basic Class Hierarchies
How to make Visual Basic 6 Data Provider Class Hierarchies
How to build a generic DNA Client Object
We'll concentrate, in this chapter, on building only one of the client components, the one that belongs to the
Customer. Once we've built one client component, the others are easy. When we come to building the other
components in the next chapter, you'll be able to see the power and beauty of our design.
Before we begin to code a client component, let's take a quick look at the design we've been developing
since the earlier chapters.
VB6 UML
Then in Chapter 8, we saw how we needed to add a third class at the top of the hierarchy to provide
different views of the Middle and Bottom classes.
We've got a lot of code to get through in this chapter, so let's get started.
This component, and the others, will be built using the class hierachy of two data source classes and a user
control that we developed in the last chapter.
I've chosen to build the components as ActiveX controls because we may later want to add a
user interface and property pages to our components. The user inteface could just be buttons
that allow move next, move previous, move first, move last like the standard Visual Basic
data control.
If you don't want to use the interfaces, you can make the controls windowless. However,
there's very little stopping you from building them as ActiveX DLLs.
So let's begin by creating a new ActiveX Control project in Visual Basic. Name the control
ctlCustomers and add two standard class modules. Name the first class clsCustomer and the second
clsCustomerManager:
338
Coding the Customer Client Component
Here is how these Visual Basic elements relate to our class hierarchy design, which we developed in
Chapter 8:
The ctlCustomers user control will be the Top Class of our hierarchy.
The clsCustomerManager class will be the Middle Class in our hierarchy. As we wish to reuse this
class for our other components, we'll try to use to make it as generic as possible.
Finally, clsCustomer will be the Bottom Class in our hierarchy, and it will mostly contain the
Customer properties.
Make sure both the clsCustomerManager and the clsCustomer classes have the following
properties:
Property Setting
DataBindingBehavior 0 - vbNone
DataSourceBehavior 1 - vbDataSource
Instancing 2 - Public Not Creatable
Persistable 0 - Not Persistable
The Data Source Interfaces reference will probably already be added because we set the
classes to be data sources.
Now that we have the basic object heirarchy in place we can move on to some code.
Add a regular BAS module to your project. Call this module basCMain.
339
VB6 UML
We are going to start using an enumerated type for our errors, so add the following code:
Option Explicit
We've just included a few of the error codes. These errors are:
errChangeFieldNoEdit - For trying to change a record without first starting an Edit session
errEditPrimaryKey - For trying to edit the primary key
errPrimaryKeyLength - For entering a primary key of the wrong length
errCustomerTitleType - For the wrong value of a Customer Title
In the final application, there would be many more possible errors, but this will be sufficient
to show how we'll handle errors.
We never want to place field names into the code, so we shall create constants for them; add the following
code right after the previous lines:
There are two ways that a user might request to view the Customer data. They will either want to see all the
Customers or just those with a particular Customer Title. For every different Customer view that the data
consumers might need, there will have to be a ClientDataMember type entry. Therefore, we'll be using
the DataMember property to determine the type of information a data consumer wants from our class.
To do this, we will create two constants representing the two possible values of the DataSource property:
340
Coding the Customer Client Component
We're also going to add in a function to retrieve error detail information from a resource file. A resource
file (RES file) is a special type of file that can be added to projects, which can contain string information as
well as images. We are going to use it to associate an error number with the string that contains the error
details for that number.
So let's create the following function to retrieve the error detail information:
GetErrorText = LoadResString(v_lngErrorNumber)
Exit Function
GetErrorText_Error:
End Function
LoadResString is a Visual Basic function that retrieves the string associated with a particular number
from the resource file.
341
VB6 UML
Now select Resource Editor from the Tools menu. When it fires up it will look like this:
342
Coding the Customer Client Component
These are the actual messages that we will show to the user when these errors occur. In this way, we can
use an enumerated type to identify the error in our code, then associate this enumerated type with a number.
This number is then associated with an error message in the RES file. This gives us the benefits of both
using an enumerated type in our code, and also the ability to store our error messages in one file.
Now we've got these preliminaries out the way, let's move on to develop the Bottom Class, which in Visual
Basic is our class called clsCustomer.
We'll need a fair few other supporting elements to get this class up and running, of course. So let's start by
adding the following module-level declarations to the clsCustomer class we created a little bit earlier:
343
VB6 UML
m_blnValidatingFieldChange = False
m_blnIgnoreFieldChange = False
m_blnInFieldChange = False
Next, we'll set the Edit mode to none and initialize the ADO Recordset variable:
m_eEditMode = adEditNone
We also need to load our array m_avarAcceptableValuesTitle with a list of Contact Titles that are
acceptable. We'll use this list later on when validating a new Contact Title:
Exit Sub
344
Coding the Customer Client Component
InitializeError:
End Sub
End Sub
ItemsDataMember is a property that we'll create to retrieve the value of the private variable
m_strDataMember, which describes which view this instance of the class is using.
End If
End Sub
345
VB6 UML
We have Business Rules that have placed limitations on the possible values of our Customer fields.
Therefore, we'll write validation code in the Property Lets that enforces these Business Rules.
This would be all very fine and well, except that this class will also be a Data Provider. This means the
fields can also be changed directly through the Recordset object. These changes also need to be validated.
The ADO Recordset object provides us with several events that get fired when a field is changed. Choosing
which one to use, however, proved to be somewhat tricky.
I had several problems. I began in the most logical place, the WillChangeEvent. It made
sense to check the value of the field before it was actually changed. I came across one
problem. I could not find any way to get the value that the field was about to be changed to
while in the WillChangeEvent. Without knowing the pending change, it was impossible
to provide any validation. So, I had to try something else.
The next choice was to try the RecordChangeComplete, but I had problems with that
too. In the end, I settled on using the FieldChangeComplete method. I am not saying
this was the best method. I could not find any documentation on validating fields and rolling
back invalid changes, so I settled on what worked. I admit that it was not the prettiest
solution, but it worked. You, of course, are more than welcome to spend a few weekends
playing with the ADO recordset events to try to come up with a better solution.
However, we certainly do not want to rewrite all of our validation rules in the FieldChangeComplete
event of the recordset especially as we already have the validation code in the Property Lets. Thus, to
avoid placing our Business Rules in two places, we'll call a function from our FieldChangeComplete
event that will check if the field's new value is valid. Since there is already validation code in the
Property Lets, we can use them to validate the new field values.
This allows us to localize all of our validation routines in our properties, instead of several different places.
We have localized all of our validation into our properties. Another possibility is to create a
separate function for every Business Rule that requires validation code. This allows you to
map your Business Rules directly to a function and easily make sure every rule has been
implemented. It also allows a Business Rule to be used by several methods or properties.
Obviously, if a Business Rule requires dozens of lines of code to enforce it, then it makse
sense to make it into a separate function. Nearly all of our rules associated with properties
though, require only a few lines of code to enforce, and they only relate to a single property.
For this reason I keep them inside the Property routines.
However, can you see the problem with using the Let properties to test if a field change is valid? Let's step
through this:
346
Coding the Customer Client Component
To prevent this, we must have a Boolean variable to tell the Property Lets that they are only being
used to validate a field, and that they shouldn't try to actually change the field in the recordset. This is what
the m_blnValidatingtFieldChange variable is for.
Begin by creating the function as a Friend, because we don't want to expose it outside the project. The
function will have two parameters: v_vFields and v_recCustomerRS.
The parameter v_vFields is an array of the fields that have been changed. It's actually a parameter from
the FieldChangeComplete event that we are simply passing directly to our function. As we are
checking when each field has been changed, we only need the last field that has been changed. This will
always be the first field in the array, v_vFields(0).
The parameter v_recRecordset is also from the FieldChangeComplete event. It's a copy of the
Customer recordset with the new underlying values:
To test the validity of the new field values, we shall attempt to set the property associated with the field to
the new value. If the value is invalid, the property will raise an error. So if we code an error trap in
ValidateFields, then we can know when a field is invalid by catching the error. The next step is to add
an error handler:
We now set the m_blnValidatingFieldChange variable to True, so that our function knows it's
only supposed to be performing validation:
m_blnValidatingFieldChange = True
Next, using the first entry of the v_vFields array, we can find out the field that has been changed, and
using a Select Case validate the field by calling its corresponding property:
347
VB6 UML
Case g_cstrFieldAddress
Address = v_recRecordset.Fields(g_cstrFieldAddress)
Case g_cstrFieldCity
City = v_recRecordset.Fields(g_cstrFieldCity)
Case g_cstrFieldCompanyName
CompanyName = v_recRecordset.Fields(g_cstrFieldCompanyName)
Case g_cstrFieldContactName
ContactName = v_recRecordset.Fields(g_cstrFieldContactName)
Case g_cstrFieldContactTitle
ContactTitle = v_recRecordset.Fields(g_cstrFieldContactTitle)
Case g_cstrFieldCountry
Country = v_recRecordset.Fields(g_cstrFieldCountry)
Case g_cstrFieldCustomerID
CustomerID = v_recRecordset.Fields(g_cstrFieldCustomerID)
Case g_cstrFieldFax
Fax = v_recRecordset.Fields(g_cstrFieldFax)
Case g_cstrFieldPhone
Phone = v_recRecordset.Fields(g_cstrFieldPhone)
Case g_cstrFieldPostalCode
PostalCode = v_recRecordset.Fields(g_cstrFieldPostalCode)
Case g_cstrFieldRegion
Region = v_recRecordset.Fields(g_cstrFieldRegion)
Case Else
ValidateFields = False
End Select
If the field name is not one of the listed fields, then there is an error and we set ValidateFields to
False in the Case Else statement.
We can now set our m_blnValidatingFieldChange event to False, since we have finished
performing our validation process:
m_blnValidatingFieldChange = False
Since there were no errors the field change was valid and we can set ValidateFields to True:
ValidateFields = True
Exit Function
If the change was not valid, we can set the ValidateFields to False and clear the error:
348
Coding the Customer Client Component
ValidateFieldsError:
ValidateFields = False
m_blnValidatingFieldChange = False
Err.Clear
End Function
Once again, there's the possibility of it creating an infinite loop. The sequence goes like this:
This is an infinite loop. We'll set the Boolean m_blnInFieldChange to True before we change the
field value so that the next time we come into the FieldChangeComplete event we can just exit out. If
the original value is wrong, we will ignore it.
There is still one more issue to be considered. When we change the value of a field in a property, we will
also raise the FieldChangeComplete event. As the code in the FieldChangeComplete event
performs validation, and the properties validate the code before setting the field, we don't need
FieldChangeComplete to validate the new value. We will need to set the Boolean
m_blnIgnoreFieldChange to True when we are changing a field value in a property.
There are a lot of parameters here. Let's look at what these values are:
349
VB6 UML
We will begin by checking whether the event has been raised by from property routine by checking the
m_blnIgnoreFieldChange variable. If m_blnIgnoreFieldChange is True, we have changed
the value of the field from a property and can just exit the sub:
Before we leave the sub, though, we need to update all of the Data Consumers by calling the
DataMemberChanged method and also raising the RefreshDataMember event. The
RefreshDataMember event will be trapped by the Middle Class, which will then refresh the controls
bound to it:
DataMemberChanged ItemsDataMember
RaiseEvent RefreshDataMember
Exit Sub
End If
As we saw, it's possible to get an infinite loop if an invalid field is entered. To prevent this loop we'll be
setting the m_blnInFieldChange to True before we change an invalid field back to its original value.
Therefore, we must check for the value of m_blnInFieldChange to see if it has been set to True. If so,
we must exit the sub:
Next, we need to check if there is anything that needs validation. If there are no records (RecordCount =
0) or the recordset has not been initialized (recordset state not equal to adStateOpen) then we have
nothing to validate and should just exit the sub:
350
Coding the Customer Client Component
If m_recManagedObjects.RecordCount = 0 _
Or m_recManagedObjects.State <> adStateOpen Then
Exit Sub
End If
If we are performing a Delete, we have no need to validate the field change; we will simply exit the sub:
Data Consumers can allow an edit without explicitly calling an Edit function. We will allow this to
happen, but we must set our EditMode property to adEditInProgress. We will check to see if the
property is currently set to adEditNone, and if it is change it to adEditInProgress:
If we've got this far then we're finally ready to do the validation. We begin by calling the
ValidateFields function that we created earlier. It will return False if the field is not valid, which
means we must roll back to the old value:
If we go into the If statement, we need to first set m_blnInFieldChange equal to True so we don't
loop when we come back into the FieldChangeComplete event when we set the field back to its old
value:
m_blnInFieldChange = True
As there is only one field that has been changed, that field will be the zero element in the Fields array,
v_vFields(0).We can use the UnderlyingValue property of the field to get the original value before
the field was changed:
m_recManagedObjects.Fields(Fields(0).Name).Value = _
pRecordset.Fields(Fields(0).Name).UnderlyingValue
Since we have changed the underlying recordset we must update the Data Consumers:
DataMemberChanged ItemsDataMember
RaiseEvent RefreshDataMember
Finally, we must set m_blnInFieldChange to False and exit the If statement and the sub:
351
VB6 UML
m_blnInFieldChange = False
End If
Exit Sub
FieldChangeError:
End Sub
These Property routines represent every possible property of a Customer. It's possible, though, that the
underlying recordset is not based on all of the Customer fields. We might only need a table with the Contact
and Company Name fields. If we retrieved a recordset that did not have the CustomerID field and we try to
retrieve the field, an error would be raised. The error would be 3265, which is raised when someone tries
to get a field that is not in the recordset. In this case, we want to ignore the error and just return nothing. In
this way, even if someone tries to get a field that is not in the recordset, they'll simply get back an empty
string.
We will turn off error handling right here within the property Get:
Next, we get the value of the CustomerID from the recordset using the parent function ItemsRecordset
that returns the Customer recordset:
CustomerID = ItemsRecordset.Fields(g_cstrFieldCustomerID)
We'll check to see if there was an error. If it was not 3265 (the field is not present) then we will raise an
error; otherwise we will ignore the error:
End If
End Property
352
Coding the Customer Client Component
We will allow the application using the control to build the CustomerID. Unfortunately, the CustomerID is,
according to our Business Rules, based on the Customer name. This is not the best choice, especially when
we are using foreign names with non-standard characters. A better choice would have been an auto-
incrementing number, but we have to work with what is already in the Northwind database.
Therefore, let's create the following Let property for CustomerID within our clsCustomer class:
Our Business Rule restricts the CustomerID to a field that is five characters long. If the value passed in is
not five characters, then it's not a valid value and an error must be raised:
If the Edit mode is adEditNone we want to raise an error, as we can edit the ID field:
If m_blnValidatingFieldChange is True (i.e. we're just validating the field, not trying to change it),
we can exit the property now as we have already performed the validation:
Finally, if the Edit mode is adEditAdd then we can set the field:
Exit Property
CustomerIDLetError:
End Property
353
VB6 UML
CompanyName = ItemsRecordset.Fields(g_cstrFieldCompanyName)
End If
End Property
The only Business Rule we have for the Company Name is: "the length of the field cannot exceed a certain
value". If the user enters a field that's too long, is it really one worth raising an error? The database will
simply truncate it to the correct length. While this may not be particularly user-friendly, raising an error is a
bit too severe for such a minor error.
You can deal with this situation by raising an event so that the mistake can be detected by
the application using the component. If the user of your control wants to raise the error,
they can do so. If they do not want to raise an error for this event, they can choose to ignore
the event. It's better to give the user options. To simplify an already complicated example,
we will not raise the event but simply ignore the error.
This leaves us with no validation to perform for CompanyName so we end up with this Let property for
Company Name:
We will need to make sure that before calling the Property Let the Edit mode has been set to
adEditInProgress or adEditAdd:
Exit Property
354
Coding the Customer Client Component
CompanyNameLetError:
End Property
ContactTitle = ItemsRecordset.Fields(g_cstrFieldContactTitle)
End If
End Property
Our Business Rule for this field limits the values of ContactTitle, so we must make sure that ContactTitle is
a valid value. We created the array m_avarAcceptableValuesTitle to hold all of the acceptable
values. We will use this array to check the value inputted. So now add the following Let property:
blnCorrect = False
lngUpperLimit = UBound(m_avarAcceptableValuesTitle)
For lngArrayCount = 1 To lngUpperLimit
If UCase(v_strNewContactTitle) = _
UCase(m_avarAcceptableValuesTitle(lngArrayCount)) Then
blnCorrect = True
End If
Next
355
VB6 UML
ItemsRecordset.Fields(g_cstrFieldContactTitle) = _
v_strNewContactTitle
End If
Exit Property
ContactTitleLetError:
End Property
We also need a Public property that allows the user to get the m_avarAcceptableValuesTitle
array. So add the following property to our clsCustomer class:
GetCustomerContactTitleValues = m_avarAcceptableValuesTitle
End Property
The rest of our fields do not have any constraints beside size, so we can code them without any specific
rules. They are presented below; add each on to our clsCustomer class.
ContactName = ItemsRecordset.Fields(g_cstrFieldContactName)
End If
End Property
356
Coding the Customer Client Component
End If
Exit Property
ContactNameLetError:
End Property
Address = ItemsRecordset.Fields(g_cstrFieldAddress)
End If
End Property
End If
Exit Property
AddressLetError:
End Property
357
VB6 UML
City = ItemsRecordset.Fields(g_cstrFieldCity)
End If
End Property
End If
Exit Property
CityLetError:
End Property
Region = ItemsRecordset.Fields(g_cstrFieldRegion)
End If
End Property
358
Coding the Customer Client Component
End If
Exit Property
RegionLetError:
End Property
PostalCode = ItemsRecordset.Fields(g_cstrFieldPostalCode)
End If
End Property
End If
359
VB6 UML
Exit Property
PostalCodeLetError:
End Property
Country = ItemsRecordset.Fields(g_cstrFieldCountry)
End If
End Property
End If
Exit Property
CountryLetError:
End Property
Phone = ItemsRecordset.Fields(g_cstrFieldPhone)
360
Coding the Customer Client Component
End If
End Property
End If
Exit Property
PhoneLetError:
End Property
Fax = ItemsRecordset.Fields(g_cstrFieldFax)
End If
End Property
361
VB6 UML
End If
Exit Property
FaxLetError:
End Property
We've now implemented Property routines for all the possible Customer attributes, but there are a few
additional properties that our Bottom Class (clsCustomer) requires.
It's impossible, though, for the Bottom Class to set the Edit Mode in the Middle Class (see Object Hierarchy
Rule 3). Whenever a record is changed, the FieldChanged event will be called in the Bottom Class. We
will set the EditMode to adEditInProgress. To tell the Middle Class that the EditMode property has
changed in the Bottom Class, we raise our user-defined event called EditInProgress.
This property will use the ADODB enumerated type for EditMode.
EditMode = m_eEditMode
End Property
In the Let property, we will raise the EditInProgress event to notify the Middle Class of the new
value:
m_eEditMode = v_eNewEditMode
RaiseEvent EditInProgress(v_eNewEditMode)
End Property
362
Coding the Customer Client Component
All customers
Customers whose title is equal a certain value
We will want to save this DataMember in a Private variable, and be able to retrieve it. We will make the
Property Let a friend property, so that the Middle Class can set this property.
m_strManagedObjectDataMember = v_strNewCustomerDataMember
End Property
ItemsDataMember = m_strManagedObjectDataMember
End Property
DataMemberChanged ItemsDataMember
End Sub
Every control bound to this Customer class which has its DataMember property set to
ItemsDataMember, will now know the recordset has changed. When the Data Consumers are informed
that the recordset has changed through the DataMemberChanged method (which is part of a Visual Basic
Data Provider class) they will call the class GetDataMember method.
We have nearly finished with our Bottom Class. The Middle Class will need to get the recordset from this
Bottom Class. As we only need to retrieve the recordset, and do not need to have the Middle Class perform
any actions, we will use Object Heirarchy Rule 1, and create a property to retrieve the recordset.
363
VB6 UML
End Property
While the Bottom Class will be holding the recordset, because of the rules of object hierarchies, the Middle
Class will be performing the Management for this class.
The functions of the Middle Class include starting edits, add news, and retrieving the recordset from the
server component. When the Middle Class retrieves the recordset, we will need to pass the recordset down
to the Bottom Class. Therefore, we will need a Property Set (a recordset is an object, so we need to use
Set, not Let).
End Property
This completes the Bottom Class. Now we will move on to the Middle Class.
As we are using RDS to connect to our server object, we don't need to add a reference to the server object
in this this project. However, we will need the Public enumerated types that are in the server component for
configuring our updates.
Therefore, copy the TableName enumerated type from the server object to the clsCustomerManager
class.
Or you may wish to just copy the relevant Enum TableName lines below, that I haven't
shaded.
Now add the following enumerated types to our clsCustomerManager Middle Class:
364
Coding the Customer Client Component
Option Explicit
Enum TableName
e_Customers = 0
e_OrderDetails = 1
e_orders = 2
e_Products = 3
e_Shippers = 4
e_Employees = 5
e_Suppliers = 6
End Enum
BOFActionType and EOFAction type will determine how our object will behave when we pass the first
and last records:
365
VB6 UML
Next, we will add some constants that will be used to initialize some of our variables. Add the following
lines to our clsCustomerManager class:
The next lines of code create the RDS proxy object - add them right along into our
clsCustomerManager class:
If you created a reference to prjServer, you can uncomment the part of this code that
reads As prjServer.clsServer. This allows you to run under straight COM and can
be helpful for debugging purposes.
366
Coding the Customer Client Component
Exit Sub
GetDataMemberError:
End Sub
We also have to initialize the Proxy object. We'll be coding a separate method in a minute that sets up the
proxy to connect with the server object through RDS, so we can just call it from here:
GetProxy
367
VB6 UML
If you made a reference to the server object in your Project's references you could alternatively add:
You can use this if you do not want to try using the RDS with HTTP, or you don't have an IIS web server to
run RDS with HHTP.
Now let's set the default values for our member variables:
m_eBOFAction = m_def_BOFAction
m_eEOFAction = m_def_EOFAction
m_strUserName = m_def_UserName
m_strPassword = m_def_Password
m_eEditMode = adEditNone
Next, we'll make the error handler. We must use a message box here, since we can't raise an error in the
Initialize event:
Exit Sub
IntitializeError:
End Sub
m_objDataSpace.InternetTimeout = 30000
End Sub
You'll need to adjust the CreateObject statement for your own particular setup.
End Sub
368
Coding the Customer Client Component
We will now set the clsCustomerManager class' private variables to these values:
m_eRecordsetName = v_eRecordsetName
m_strWhereClause = v_strWhereClause
m_strPrimaryKey = v_strPrimaryKey
m_strDataMember = v_eDataMember
m_objManagedObject.ItemsDataMember = v_eDataMember
End Function
ItemsDataMember = m_strDataMember
End Property
Middle Class: Get Properties for the Table, WhereClause, and PrimaryKey Information
The Get Properties for these would be as follows. Add them to our clsCustomerManager class:
PrimaryKey = m_strPrimaryKey
End Property
RecordsetName is the name of the recordet we are using. It will be used to retireve the correct recordset
from the database:
369
VB6 UML
RecordsetName = m_eRecordsetName
End Property
The WhereClause property will be built when a recordset is being built using a WHERE clause.
WhereClause = m_strWhereClause
End Property
End Property
ItemCount = m_objManagedObject.ItemsRecordset.RecordCount
End Property
There will be no Let property as this value cannot be changed by the user.
This will be a Friend Get Property, as we do not want this recordset accessed outside of the
component:
End Property
370
Coding the Customer Client Component
EditMode = m_eEditMode
End Property
The Let Property will be a Friend Property as the Edit mode should only be set internally - it can only be
set through either the Edit or AddNew methods that we'll code shortly. We need to sync the EditMode of
both the Bottom and Middle Class. So if the value of the EditMode in the Bottom Class is not the same as
that in the Middle Class, then we need to change the Bottom Class' EditMode so they're both the same:
m_eEditMode = v_eNewEditMode
End Property
We will want to begin by saving the value of the current record's primary key field. If the recordset is not
initialized or there are no records, there will be no current value.
We proceed by checking to see if the recordset has been initialized by checking the recordset's State
property:
371
VB6 UML
Next, we make sure there are records, and if there are, we will save the primary key value:
UpdateManagedObjects
If there is value for the Primary Key field, we can use it to move to that record:
End Sub
There is one little quirk with using the Find method. If the record is not found, it will move the recordset
to the last record. It might be better if you checked to see if this happens and, if it does, move to the first
record. I will leave this up to you.
If the EditMode is set to adEditNone, we need to set both the Middle and Top Class' EditMode to
adEditInProgress. We've already created a property to do this so it's quite straightforward. Add the
following code to the clsCustomerManager class:
End Sub
We're not going to handle any errors here, but you can add in an Else clause to do so. You would probably
want to ignore the error of being in an Edit, but handle the error of being in an AddNew.
372
Coding the Customer Client Component
We do not want to allow a delete if an AddNew or Edit is in progress. Because the EditMode property is
not set when a delete is performed, the EditMode property should be set to adEditNone:
If the delete is allowed, delete the current record and update the database:
Else
GetErrorText "CanNotDeleteDuringEdit/AddNew"
End If
End If
Finally, we'll exit the Sub and make the error trap:
Exit Sub
DeleteError:
End Sub
373
VB6 UML
m_objManagedObject.ItemsRecordset.AddNew
EditMode = adEditAdd
Else
End If
Again, you can add error handling to the Else statement if you wish.
Exit Sub
AddNewError:
End Sub
End Sub
374
Coding the Customer Client Component
EditMode = adEditNone
It's possible that there are other Customer objects that are using different Middle Class objects with a
different Customer Collection. As we just changed the Customer recordset, we need to raise an event to let
the Top Collection Managing class know to refresh all of the Customer Collections:
RaiseEvent ChangeManagedObjects
End If
And finally, exit the sub and put in the error trap:
Exit Sub
UpdateError:
End Sub
375
VB6 UML
Unfortunately, one of the Company Names, in the Northwind database, has an apostrophe character (‘) in it,
which will affect our queries. We'll simply truncate off the rest of the name after this character for
simplicity:
Now we must refresh all of the Data Consumers so they also move to this new record:
Refresh
End Function
RecordCount = m_objManagedObject.ItemsRecordset.RecordCount
End Property
Exit Sub
376
Coding the Customer Client Component
MoveFirstError:
End Sub
Exit Sub
MoveLastError:
End Sub
The rest of the Move functions are more complicated because they can move past the last or first record.
What we do depends on the BOFAction and EOFAction properties:
For the MoveNext we start by checking if the recordset exists. Carry on adding code to the
clsCustomerManager class as follows:
We check to see if the recordset has moved past the last record:
If we are past the last record, how we will handle this depends on the value of m_eEOFAction.
EOFAction is a public property that will be set by the user of the component:
377
VB6 UML
End If
Exit Sub
MoveNextError:
End Sub
If m_objManagedObject.ItemsRecordset.BOF Then
Select Case BOFAction
Case BOFActionType.adMoveFirst
m_objManagedObject.ItemsRecordset.MoveFirst
Case BOFActionType.adStayBOF
Exit Sub
Case Else
Exit Sub
End Select
Else
m_objManagedObject.ItemsRecordset.MovePrevious
End If
Exit Sub
MovePreviousError:
End Sub
378
Coding the Customer Client Component
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.ReturnCustomerRecordSet(UserName, Password, WhereClause)
DataMemberChanged ItemsDataMember
m_objManagedObject.RefreshDataMember
Exit Sub
UpdateManagedObjectsError:
End Sub
UserName = m_strUserName
End Property
m_strUserName = New_strUserName
End Property
Password = m_strPassword
End Property
m_strPassword = New_strPassword
End Property
BOFAction = m_eBOFAction
End Property
379
VB6 UML
m_eBOFAction = New_BOFAction
End Property
EOFAction = m_eEOFAction
End Property
m_eEOFAction = New_EOFAction
End Property
EditMode = v_strEditMode
End Sub
If the Bottom Class indicates that the recordset needs to be refreshed we can call the
DataMemberChanged method:
DataMemberChanged ItemsDataMember
End Sub
m_colCustomerAll
m_colCustomerTitle
380
Coding the Customer Client Component
m_colCustomerTitle will be a collection of customers whose title equals a certain value. The value
that the title should be equal to will be stored in this Top Class and accessed through properties. Any values
that will be part of WHERE clauses of our collections should be stored in the Middle Class and have
properties associated with them.
It's time to code the Top Class, ctlCustomers. So add the following declarations to ctlCustomers,
which we created earlier:
Option Explicit
Const m_def_NumberOfCustomerCollections = 2
Const m_def_ContactTitleEquals As String = "Owner"
381
VB6 UML
This is one very large potential danger of using disconnected recordsets. A user could easily use your
objects to create dozens of customer collections, which would quickly overwhelm the client computer and
the network. The users of your object have to understand that they must destroy a collection when they have
finished with it.
m_strContactTitleEquals = m_def_ContactTitleEquals
Redim the two arrays, they will be 1 based (they will start at element 1):
Place False into the array, which indicates if the collections have been populated with data:
m_ablnDataMembersInitialized(e_AllCustomers) = False
m_ablnDataMembersInitialized(e_CustomersWhoseTitle) = False
End Sub
End Sub
382
Coding the Customer Client Component
If the collection has not been initialized, initialize it by passing the appropriate parameters down to the
Middle Class using the SetProxyInformation function:
Case e_AllCustomers
m_acolDataMembersArray(e_AllCustomers).SetProxyInformation _
e_Customers, "", g_cstrFieldCustomerID, g_cdmAllCustomers
Case e_CustomersWhoseTitle
m_acolDataMembersArray(e_CustomersWhoseTitle).SetProxyInformation _
e_Customers, " WHERE ContactTitle='" & ContactTitleEquals & "'", _
g_cstrFieldCustomerID, g_cdmCustomersContactTitleEquals
End Select
m_acolDataMembersArray(v_eDataMember).Refresh
m_ablnDataMembersInitialized(v_eDataMember) = True
End If
End Function
383
VB6 UML
Create the sub with these variables, within our ctlCustomers control:
Now move through the Customer collections and call the Refresh methods of all the collections except
the one we are to ignore:
Next
End Sub
ChangeManagedObjects g_cdmAllCustomers
End Sub
384
Coding the Customer Client Component
ChangeManagedObjects g_cdmCustomersContactTitleEquals
End Sub
ContactTitleEquals = m_strContactTitleEquals
End Property
End Property
Summary
This has been a very long chapter - making a client component is not a simple task. By using UML
diagrams earlier, however, we were able to see a pattern in our Client component that allowed us to build
our components in an efficient and effective way. As such, we'll be able to use practically the same code in
all the Middle classes.
We can now crank out the rest of our Client components with very little effort. The total time to make this
component will probably be about a day. This is what patterns are all about. Once you find the patterns, you
can use them to make a framework to build your code from. Our Server and Client components make up a
Visual Basic DNA framework that allows you to quickly build projects.
In the next chapter we'll use the template we have made in this chapter to build three more Client
components: Order Details, Orders and Products.
385
Coding the Other Client
Components
In the last chapter, you saw how we coded the implementation for the Customer component on the client-
side of our Northwind system. We must now proceed with the implementation of the:
OrderDetails component
Order component
Product components
Fortunately, because of our design, coding these additional components requires less work than you might
think. Using a hierarchy means that there is little to change in the Middle and Top Classes for them to work
with any of our components. All we really need to concentrate on is the Bottom Class, which has properties
specific to its purpose. We've essentially created a code template that we can apply to all of our components
and then tweak for that component's specifics.
Therefore, in this chapter we'll be concentrating more on the differences between the components rather
than the actual implementation.
We'll be able to build this component using the code template that we created when we built the Customer
component. The majority of the code in the Customer component can again be reused when we build the
other component.
VB6 UML
We will be building all our components within the same project group to make them easier
to build and debug, but there's nothing to stop you creating them each separately.
To begin with, add a new ActiveX Control project to the Customer project.
Add two regular classes to the project. Call them clsOrderDetail and clsOrderDetailManager.
Set the properties of the classes as follows:
DataBindingBehavior 0 - vbNone
DataSourceBehavior 1 - vbDataSource
Instancing 2 - Public Not Creatable
Persistable 0 - Not Persistable
Here is how these Visual Basic elements map to our class hierarchy:
OrderDetails: basODMain
As with our Customer component, we'll need to create a bas module for constants and shared functions.
So add a standard BAS module to the project, and call the module basODMain.
We will first use an Enum for error and constants for all of the fields in the Order table:
Next, we'll use the same GetErrorText module as we used in the Customer component.
388
Coding the Other Client Components
GetErrorText = LoadResString(v_lngErrorNumber)
Exit Function
GetErrorText_Error:
End Function
You may want to think about creating a separate Error component that holds all error
messaging, etc.
Next, we'll code the OrderDetails Middle Class, which happens to be the VB class we've already created
and called clsOrderDetailManager.
In fact, you'll need to delete some routines such as Find or you'll get compile errors.
Although it wouldn't actually take much effort to configure them for this class component.
Copy all the code from the Customer component clsCustomManager class into our
clsOrderDetailManager class, and in the declarations section change:
to this:
389
VB6 UML
GetProxy
m_eBOFAction = m_def_BOFAction
m_eEOFAction = m_def_EOFAction
m_strUserName = m_def_UserName
m_strPassword = m_def_Password
m_eEditMode = adEditNone
Exit Sub
IntitializeError:
End Sub
The GetItem method is dependent on the name of the Bottom Class, so we need to change this line of
code:
End Property
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.ReturnOrderDetailsRecordSet(UserName, Password, WhereClause)
DataMemberChanged ItemsDataMember
m_objManagedObject.RefreshDataMember
Exit Sub
UpdateManagedObjectsError:
End Sub
When we created a new Customer, we created the Customer in the disconnected recordset, and then simply
sent the information to the server object to be stored in the database.
When an Order is created, however, many rows may be added to the Order Details table. It makes more
sense to wait until all the Order Details have been completed before we update the database. We only want
save these rows to the disconnected recordset.
390
Coding the Other Client Components
Therefore, we'll include a special Boolean, v_blnSaveToDataBase, in this Update sub to allow the
sub to know whether the update is supposed to be sent to the server object (v_blnSaveToDataBase =
True). If v_blnSaveToDataBase is False, the disconnected recordset will be updated and the
recordset will hold multiple updates waiting to be saved to the database:
First, check if we are performing a Batch update. If we are, just update the local recordset:
EditMode = adEditNone
Exit Sub
Else
If we're not doing a Batch update, and we don't update the local recordset, the EditMode property will be
set to adEditAdd or adEditInProgress. In this case, we can use the generic Update function:
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.UpdateRecordset(UserName, Password, _
m_objManagedObject.ItemsRecordset, m_eRecordsetName, _
WhereClause)
End If
EditMode = adEditNone
RaiseEvent ChangeManagedObjects
End If
391
VB6 UML
EditMode = adEditNone
Exit Sub
UpdateError:
End Sub
End Property
The declarations are practically the same except that we don't need m_avarAcceptableValuesTitle
and m_strManagedObjectDataMember:
Option Explicit
392
Coding the Other Client Components
EditMode = m_eEditMode
End Property
m_eEditMode = v_eNewEditMode
RaiseEvent EditInProgress(v_eNewEditMode)
End Property
End Property
End Property
m_strManagedObjectDataMember = v_strNewOrderDataMember
End Property
ItemsDataMember = m_strManagedObjectDataMember
End Property
OrderID = ItemsRecordset.Fields(g_odFieldOrderID)
393
VB6 UML
End If
End Property
End If
Exit Property
OrderIDLetError:
End Property
If IsNull(ItemsRecordset.Fields(g_odFieldProductID)) Then
ProductID = 0
Else
ProductID = ItemsRecordset.Fields(g_odFieldProductID)
End If
End If
End Property
394
Coding the Other Client Components
End If
Exit Property
ProductIDLetError:
End Property
If IsNull(ItemsRecordset.Fields(g_odFieldUnitPrice)) Then
UnitPrice = 0
Else
UnitPrice = ItemsRecordset.Fields(g_odFieldUnitPrice)
End If
End If
End Property
395
VB6 UML
End If
Exit Property
UnitPriceLetError:
End Property
If IsNull(ItemsRecordset.Fields(g_odFieldDiscount)) Then
Discount = 0
Else
Discount = ItemsRecordset.Fields(g_odFieldDiscount)
End If
End If
End Property
End If
Exit Property
DiscountLetError:
End Property
396
Coding the Other Client Components
If IsNull(ItemsRecordset.Fields(g_odFieldQuantity)) Then
Quantity = 1
Else
Quantity = ItemsRecordset.Fields(g_odFieldQuantity)
End If
End If
End Property
End If
Exit Property
QuantityLetError:
End Property
The Product Name is not actually part of the Order Details record. We added this field onto the Order
Details recordset by using the following query in the server component to build the Order Details recordset:
397
VB6 UML
This query is joining two tables: Order Details and Products. Order Details only has the ProductID, and it's
unlikely this number, by itself, would be of much use to the Order Entry clerk. Instead, the clerk would
probably use the Product Name. To get the Product Name, we'll have to do a join with the Products table
and associate the appropriate Product Name with the ProductID in each Order Detail row. Since, the
Product Name field does not really belong to the Order Details table, it can only be changed when updating
Products.
We rebuild the Order Details recordset on the server when we are updating Order Detail recordsets. When
we rebuild the recordset, we will just ignore the Product Name field. Thus, any changes made to the
Product Name field on the client are completely ignored. The name is only there to make it easier for the
user to read an order record.
ProductName = ItemsRecordset.Fields(g_odFieldProductName)
End If
End Property
End If
Exit Property
398
Coding the Other Client Components
ProductNameLetError:
End Property
m_blnValidatingFieldChange = False
m_blnIgnoreFieldChange = False
m_blnInFieldChange = False
m_eEditMode = adEditNone
Set m_recManagedObjects = New ADODB.Recordset
End Sub
With ItemsRecordset
End With
End If
End Sub
End Sub
399
VB6 UML
m_blnValidatingFieldChange = True
Case g_odFieldOrderID
OrderID = pRecordset.Fields(g_odFieldOrderID)
Case g_odFieldProductID
ProductID = pRecordset.Fields(g_odFieldProductID)
Case g_odFieldUnitPrice
UnitPrice = pRecordset.Fields(g_odFieldUnitPrice)
Case g_odFieldQuantity
Quantity = pRecordset.Fields(g_odFieldQuantity)
Case g_odFieldDiscount
Discount = pRecordset.Fields(g_odFieldDiscount)
Case g_odFieldProductName
ValidateFields = True
Case Else
ValidateFields = False
End Select
m_blnValidatingFieldChange = False
ValidateFields = True
Exit Function
ValidateFieldsError:
ValidateFields = False
m_blnValidatingFieldChange = False
Err.Clear
End Function
You may notice that for the Product Name field we are simply returning True. This is because the Product
Name field doesn't need to be validated here because this field will be ignored by the server component
during the update of an Order Detail recordset.
400
Coding the Other Client Components
DataMemberChanged ItemsDataMember
End Sub
If ItemsRecordset.RecordCount =0 Or ItemsRecordset.EditMode _
= adEditDelete Or ItemsRecordset.State <> adStateOpen Then
Exit Sub
End If
End Sub
401
VB6 UML
m_lngOrderIDEquals = m_clngOrderID
Set OrderDetailsCollectionOrder = New clsOrderDetailManager
ReDim m_acolDataMembersArray(m_def_NumberOfOrderDetailsCollections)
ReDim m_blnDataMembersInitialized(m_def_NumberOfOrderDetailsCollections)
m_blnDataMembersInitialized(e_OrderIDOrderDetails) = False
End Sub
End Sub
OrderIDEquals = m_lngOrderIDEquals
End Property
m_lngOrderIDEquals = v_lngNewOrderID
End Property
402
Coding the Other Client Components
Case e_OrderIDOrderDetails
m_acolDataMembersArray(e_OrderIDOrderDetails).SetProxyInformation _
e_OrderDetails, "WHERE OrderID =" & m_lngOrderIDEquals, _
m_cstrFieldOrderDetailsID, g_oddmAllOrderDetails
End Select
m_acolDataMembersArray(v_eDataMember).Refresh
m_blnDataMembersInitialized(v_eDataMember) = True
End If
End Function
Next
End Sub
403
VB6 UML
ChangeManagedObjects g_oddmAllOrderDetails
End Sub
This completes our implementation of the OrderDetails component. Keep a note of how many elements
were similar between the Customer and OrderDetails components – we're saving a lot of development time
through these patterns.
Add two regular classes to the project. Call them clsOrder and clsOrderManager.
DataSourceBehavior 1 - vbDataSource
DataBindingBehavior 0 - vbNone
Instancing 2 - Public Not Creatable
Persistable 0 - Not Persistable
Finally, we need to create a BAS module for constants and shared functions. Add a standard module to the
project, and call it basOMain.
Here is how these Visual Basic elements map to our Order component class hierarchy:
Order: basOMain
We will first need constants for all of the fields in the Order table:
404
Coding the Other Client Components
End Enum
Public Const g_oFieldOrderID As String = "OrderID"
Public Const g_oFieldCustomerID As String = "CustomerID"
Public Const g_oFieldEmployeeID As String = "EmployeeID"
Public Const g_oFieldOrderDate As String = "OrderDate"
Public Const g_oFieldRequiredDate As String = "RequiredDate"
GetErrorText = LoadResString(v_lngErrorNumber)
Exit Function
GetErrorText_Error:
End Function
405
VB6 UML
GetProxy
m_eBOFAction = m_def_BOFAction
m_eEOFAction = m_def_EOFAction
m_strUserName = m_def_UserName
m_strPassword = m_def_Password
m_eEditMode = adEditNone
Exit Sub
IntitializeError:
End Sub
The GetItem method will also depend on the name of the Bottom Class. We will need to change this line
of code.
End Property
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.ReturnOrdersRecordSet(UserName, Password, WhereClause)
DataMemberChanged ItemsDataMember
m_objManagedObject.RefreshDataMember
Exit Sub
UpdateManagedObjectsError:
End Sub
406
Coding the Other Client Components
m_strPrimaryKey = v_strPrimaryKey
m_strWhereClause = v_strWhereClause
m_strDataMember = v_eDataMember
m_objManagedObject.ItemsDataMember = v_eDataMember
End Function
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.UpdateRecordset(UserName, Password, _
m_objManagedObject.ItemsRecordset, m_eRecordsetName, _
WhereClause)
m_objManagedObject.ItemsRecordset.Update
EditMode = adEditNone
RaiseEvent ChangeManagedObjects
End If
EditMode = adEditNone
Exit Sub
UpdateError:
End Sub
407
VB6 UML
End If
m_objManagedObject.ItemsRecordset.Update
EditMode = adEditNone
RaiseEvent ChangeManagedObjects
Else
End If
EditMode = adEditNone
Exit Sub
UpdateError:
Resume Next
Err.Raise Err.Number, "Update: " & Err.Source, Err.Description
End Sub
408
Coding the Other Client Components
Option Explicit
OrderID = ItemsRecordset.Fields(g_oFieldOrderID)
End If
End Property
We will exclude a Let property. When a new Order is added, an OrderID is assigned by the server
component.
CustomerID = ItemsRecordset.Fields(g_oFieldCustomerID)
End If
End Property
409
VB6 UML
End If
Exit Property
CustomerIDLetError:
End Property
EmployeeID = ItemsRecordset.Fields(g_oFieldEmployeeID)
End If
End Property
410
Coding the Other Client Components
End If
Exit Property
EmployeeIDLetError:
End Property
OrderDate = ItemsRecordset.Fields(g_oFieldOrderDate)
End If
End Property
End If
Exit Property
OrderDateLetError:
End Property
411
VB6 UML
RequiredDate = ItemsRecordset.Fields(g_oFieldRequiredDate)
End If
End Property
End If
Exit Property
RequiredDateLetError:
End Property
ShippedDate = ItemsRecordset.Fields(g_oFieldShippedDate)
End If
End Property
412
Coding the Other Client Components
End If
Exit Property
ShippedDateLetError:
End Property
ShipVia = ItemsRecordset.Fields(g_oFieldShipVia)
End If
End Property
End If
413
VB6 UML
Exit Property
ShipviaLetError:
End Property
Freight = ItemsRecordset.Fields(g_oFieldFreight)
End If
End Property
End If
Exit Property
FreightLetError:
End Property
ShipName = ItemsRecordset.Fields(g_oFieldShipName)
414
Coding the Other Client Components
End If
End Property
End If
Exit Property
ShipNameLetError:
End Property
ShipAddress = ItemsRecordset.Fields(g_oFieldShipAddress)
End If
End Property
415
VB6 UML
End If
Exit Property
ShipAddressLetError:
End Property
ShipCity = ItemsRecordset.Fields(g_oFieldShipCity)
End If
End Property
End If
Exit Property
416
Coding the Other Client Components
ShipCityLetError:
End Property
ShipRegion = ItemsRecordset.Fields(g_oFieldShipRegion)
End If
End Property
End If
Exit Property
ShipRegionLetError:
End Property
ShipPostalCode = ItemsRecordset.Fields(g_oFieldShipPostalCode)
417
VB6 UML
End If
End Property
End If
Exit Property
ShipPostalCodeLetError:
End Property
ShipCountry = ItemsRecordset.Fields(g_oFieldShipCountry)
End If
End Property
418
Coding the Other Client Components
End If
Exit Property
ShipCountryLetError:
End Property
m_blnValidatingFieldChange = True
Case g_oFieldCustomerID
CustomerID = pRecordset.Fields(g_oFieldCustomerID)
Case g_oFieldEmployeeID
EmployeeID = pRecordset.Fields(g_oFieldEmployeeID)
Case g_oFieldFreight
Freight = pRecordset.Fields(g_oFieldFreight)
Case g_oFieldOrderDate
OrderDate = pRecordset.Fields(g_oFieldOrderDate)
Case g_oFieldOrderID
Exit Function
Case g_oFieldRequiredDate
RequiredDate = pRecordset.Fields(g_oFieldRequiredDate)
Case g_oFieldShipAddress
ShipAddress = pRecordset.Fields(g_oFieldShipAddress)
419
VB6 UML
Case g_oFieldShipCity
ShipCity = pRecordset.Fields(g_oFieldShipCity)
Case g_oFieldShipCountry
ShipCountry = pRecordset.Fields(g_oFieldShipCountry)
Case g_oFieldShippedDate
ShippedDate = pRecordset.Fields(g_oFieldShippedDate)
Case g_oFieldShipName
ShipName = pRecordset.Fields(g_oFieldShipName)
Case g_oFieldShippedDate
ShippedDate = pRecordset.Fields(g_oFieldShippedDate)
Case g_oFieldShipPostalCode
ShipPostalCode = pRecordset.Fields(g_oFieldShipPostalCode)
Case g_oFieldShipRegion
If Not IsNull(pRecordset.Fields(g_oFieldShipRegion))Then
ShipRegion = pRecordset.Fields(g_oFieldShipRegion)
Else
ShipRegion = ""
End If
Case g_oFieldShipVia
ShipVia = pRecordset.Fields(g_oFieldShipVia)
Case Else
ValidateFields = False
End Select
m_blnValidatingFieldChange = False
ValidateFields = True
Exit Function
ValidateFieldsError:
ValidateFields = False
m_blnValidatingFieldChange = False
Err.Clear
End Function
EditMode = m_eEditMode
End Property
m_eEditMode = v_eNewEditMode
420
Coding the Other Client Components
RaiseEvent EditInProgress(v_eNewEditMode)
End Property
End Property
End Property
m_strManagedObjectDataMember = v_strNewOrderDataMember
End Property
ItemsDataMember = m_strManagedObjectDataMember
End Property
The class events are the same as they were in Order Details:
m_blnValidatingFieldChange = False
m_blnIgnoreFieldChange = False
m_blnInFieldChange = False
m_eEditMode = adEditNone
Set m_recManagedObjects = New ADODB.Recordset
End Sub
With ItemsRecordset
End With
End If
421
VB6 UML
End Sub
End Sub
The rest of the methods in this class will be identical to the methods in the Customer component:
DataMemberChanged ItemsDataMember
End Sub
If ItemsRecordset.RecordCount = 0 Or ItemsRecordset.EditMode = _
AdEditDelete Or ItemsRecordset.State <> adStateOpen Then
Exit Sub
End If
If ItemsRecordset.Fields(Fields(0).Name).Value = _
pRecordset.Fields(Fields(0).Name).UnderlyingValue Then
Exit Sub
End If
End Sub
422
Coding the Other Client Components
Const m_def_NumberOfOrderCollections = 2
Const m_def_OrderID As Long = 0
m_lngOrderID = m_def_OrderID
ReDim m_acolDataMembersArray(m_def_NumberOfOrderCollections)
ReDim m_blnDataMembersInitialized(m_def_NumberOfOrderCollections)
m_blnDataMembersInitialized(e_AllOrders) = False
m_blnDataMembersInitialized(e_OrderWhoseOrderID) = False
End Sub
End Sub
423
VB6 UML
OrderIDEquals = m_lngOrderID
End Property
ChangeManagedObjects g_odmOrderWhoseOrderID
End Sub
ChangeManagedObjects g_odmAllOrders
End Sub
Case g_odmAllOrders
lngIgnoreOrderNumber = 1
Case g_odmOrderWhoseOrderID
lngIgnoreOrderNumber = 2
End Select
End Sub
424
Coding the Other Client Components
Case e_AllOrders
m_acolDataMembersArray(e_AllOrders).SetProxyInformation "", _
g_oFieldOrderID, g_odmAllOrders
Case e_OrderWhoseOrderID
m_acolDataMembersArray(e_OrderWhoseOrderID).SetProxyInformation _
" WHERE " & g_oFieldOrderID & " = " & OrderIDEquals, _
g_oFieldOrderID, g_odmOrderWhoseOrderID
End Select
m_acolDataMembersArray(v_eDataMember).Refresh
m_blnDataMembersInitialized(v_eDataMember) = True
End If
End Function
This completes our implementation of the Order component. As we saw, many of the Middle and Top Class
details were extremely similar.
Add two regular classes to the project. Call them clsProduct and clsProductManager.
DataSourceBehavior 1 - vbDataSource
DataBindingBehavior 0 - vbNone
Instancing 2 - Public Not Creatable
Persistable 0 - Not Persistable
Finally, we need to create a BAS module for constants and shared functions. Add a standard module to the
project, and call it basPMain.
425
VB6 UML
Here is how these Visual Basic elements map to our Product component class hierarchy:
Product: basPMain
We will first need constants for all of the fields in the Products table.
GetErrorText = LoadResString(v_lngErrorNumber)
Exit Function
GetErrorText_Error:
End Function
426
Coding the Other Client Components
GetProxy
m_eBOFAction = m_def_BOFAction
m_eEOFAction = m_def_EOFAction
m_strUserName = m_def_UserName
m_strPassword = m_def_Password
m_eEditMode = adEditNone
Exit Sub
IntitializeError:
End Sub
The GetItem method will also depend on the name of the Bottom class. We will need to change this line
of code.
End Property
Set m_objManagedObject.ItemsRecordset = _
m_objProxy.ReturnProductsRecordSet(UserName, Password, WhereClause)
DataMemberChanged ItemsDataMember
m_objManagedObject.RefreshDataMember
Exit Sub
UpdateManagedObjectsError:
End Sub
427
VB6 UML
End If
EditMode = adEditNone
Exit Sub 'CAB to here
Else
End If
Else
End If
EditMode = adEditNone
Exit Sub
428
Coding the Other Client Components
UpdateError:
End Sub
End Function
Also note that this time we didn't have to change to the SetProxyInformation method.
Option Explicit
429
VB6 UML
SupplierID = m_recManagedObjects.Fields(g_pFieldSupplierID)
End If
End Property
End If
Exit Property
SupplierIDLetError:
End Property
QuantityPerUnit = m_recManagedObjects.Fields(g_pFieldQuantityPerUnit)
430
Coding the Other Client Components
End If
End Property
End If
Exit Property
QuantityPerUnitLetError:
End Property
UnitPrice = m_recManagedObjects.Fields(g_pFieldUnitPrice)
End If
End Property
431
VB6 UML
End If
Exit Property
UnitPriceLetError:
End Property
UnitsInStock = m_recManagedObjects.Fields(g_pFieldUnitsInStock)
End If
End Property
End If
Exit Property
432
Coding the Other Client Components
UnitsInStockLetError:
End Property
UnitsOnOrder = m_recManagedObjects.Fields(g_pFieldUnitsOnOrder)
End If
End Property
End If
Exit Property
UnitsOnOrderLetError:
End Property
ReorderLevel = m_recManagedObjects.Fields(g_pFieldReorderLevel)
433
VB6 UML
End If
End Property
End If
Exit Property
ReorderLevelLetError:
End Property
Discontinued = m_recManagedObjects.Fields(g_pFieldDiscontinued)
End If
End Property
434
Coding the Other Client Components
End If
Exit Property
DiscontinuedLetError:
End Property
ProductID = m_recManagedObjects.Fields(g_pFieldProductID)
End If
End Property
ProductName = m_recManagedObjects.Fields(g_pFieldProductName)
End If
435
VB6 UML
End Property
End If
Exit Property
ProductNameLetError:
End Property
m_blnValidatingFieldChange = True
Case g_pFieldProductName
ProductName = pRecordset.Fields(g_pFieldProductName)
Case g_pFieldProductID
ValidateFields = True
Case g_pFieldUnitPrice
UnitPrice = pRecordset.Fields(UnitPrice)
Case g_pFieldDiscontinued
Discontinued = pRecordset.Fields(g_pFieldDiscontinued)
Case g_pFieldSupplierID
SupplierID = pRecordset.Fields(g_pFieldSupplierID)
436
Coding the Other Client Components
Case g_pFieldSupplierID
SupplierID = pRecordset.Fields(g_pFieldSupplierID)
End Select
m_blnValidatingFieldChange = False
ValidateFields = True
Exit Function
ValidateFieldsError:
m_blnValidatingFieldChange = False
ValidateFields = False
Err.Clear
End Function
EditMode = m_eEditMode
End Property
m_eEditMode = v_eNewEditMode
RaiseEvent EditInProgress(v_eNewEditMode)
End Property
End Property
End Property
m_strManagedObjectDataMember = v_strNewOrderDataMember
End Property
437
VB6 UML
ItemsDataMember = m_strManagedObjectDataMember
End Property
The Class events in clsProduct are almost the same as we've previously seen in the other Bottom
Classes:
End Sub
m_blnValidatingFieldChange = False
m_blnIgnoreFieldChange = False
m_blnInFieldChange = False
m_eEditMode = adEditNone
Set m_recManagedObjects = New ADODB.Recordset
End Sub
ItemsRecordset.Close
End If
End If
End Sub
The rest of the methods in our clsProduct class will be identical to the methods in the Customer
component:
DataMemberChanged ItemsDataMember
End Sub
438
Coding the Other Client Components
If m_recManagedObjects.RecordCount = 0 Or m_recManagedObjects.EditMode _
= adEditDelete Or m_recManagedObjects.State <> adStateOpen Then
Exit Sub
End If
End Sub
Copy the code from one of the other Top Class components (such as Order) into our ctlProducts
control and change this code:
Option Explicit
Const m_def_NumberOfProductCollections = 1
Const m_def_CategoryIDEquals As Long = 0
439
VB6 UML
m_lngCategoryIDEquals = m_def_CategoryIDEquals
ReDim m_acolDataMembersArray(m_def_NumberOfProductCollections)
ReDim m_blnDataMembersInitialized(m_def_NumberOfProductCollections)
m_blnDataMembersInitialized(e_ProductsCategoryID) = False
End Sub
ChangeManagedObjects g_pdmProductIDProducts
End Sub
Case g_pdmProductIDProducts
lngIgnoreProductNumber = 1
End Select
End If
Next
End Sub
440
Coding the Other Client Components
Case e_ProductsCategoryID
m_acolDataMembersArray(e_ProductsCategoryID).SetProxyInformation _
e_Products, " Where " & g_pFieldCategoryID & " = " & _
CategoryIDEquals, g_pFieldProductID, g_pdmProductIDProducts
End Select
m_acolDataMembersArray(v_eDataMember).Refresh
m_blnDataMembersInitialized(v_eDataMember) = True
End If
End Function
CategoryIDEquals = m_lngCategoryIDEquals
End Property
m_lngCategoryIDEquals = v_strNewCategoryIDEquals
m_acolDataMembersArray(e_ProductsCategoryID).SetProxyInformation e_Products, _
"Where " & g_pFieldCategoryID & " = " & CategoryIDEquals, _
g_pFieldProductID, g_pdmProductIDProducts
End Property
Summary
Congratulations! We have now coded the vast majority of the Northwind system. As you can see, once we
had coded one of the Client components, the rest fell into place in no time. In fact we could continue to
extend the implementation to include the Shipper and Employee components without much trouble, since
our design is taking advantage of the patterns we identified throughout the UML design process.
In order to see the project running, we're going to build a fairly simple Win32 GUI in the next chapter,
which will allow us to Add and Edit Customers - and place an order.
441
Creating the GUI Order Form
Now that we've created four of the client components, we can build our Order Form. Since our components
will be doing most of the work for our Form, such as retrieving, updating, moving through the information,
and so on, our forms will have very little to do. As we know, we now have the ability to build data provider
classes, which means we also will not have to write special modules to handle populating each control on
our form.
In Visual Basic 5, writing an application like this resulted in fifty to one hundred pages of code to populate
text boxes, grid controls, perform updates on these controls, etc. These days are gone.
Add a Standard EXE project to the OrderEntry.vbg group and set it as StartUp. Name the project
prjClient and the form frmClient.
One thing we can't really see here is that behind the Company Name and Contact Title text boxes there
are two combo boxes. We can see more clearly in the figure below, where they should be:
Apart from the five text boxes found in the Shipping section, the rest of the text boxes are a control array
called txtCustomer(), each of which has its Tag property set to the Field it relates to:
Name Tag
txtCustomer(0) CompanyName
txtCustomer(1) ContactTitle
txtCustomer(2) ContactName
txtCustomer(3) CustomerID
txtCustomer(4) Phone
txtCustomer(5) Fax
txtCustomer(6) Address
txtCustomer(7) City
txtCustomer(8) PostalCode
txtCustomer(9) Region
txtCustomer(10) Country
444
Creating the GUI Order Form
We'll be iterating through the text box Tags to find particular fields. Therefore, it
makes sense that the fields that we will be searching the most for should have lower
indexes. This way the iteration will never go beyond a few text boxes.
Set the Visible property for txtCustomer(0) to False, as it's only going to be used for editing or
adding a Customer.
The Shipping section text boxes are also a control array, but this time called txtOrder():
Name Tag
txtOrder(0) ShipAddress
txtOrder(1) ShipCity
txtOrder(2) ShipPostalCode
txtOrder(3) ShipRegion
txtOrder(4) ShipCountry
cmdCustomerEdit
cmdCustomerAdd
cmdCustomerUpdate
Set the Enabled property for cmdCustomerUpdate to False - we'll enable it when appropriate in the
code.
Also set the Visible property of cboContact to False, as it's only going to be used for editing or
adding a Customer.
445
VB6 UML
The things to note about this form are that the text box in the bottom right (we'll call it 'Unlabelled") should
have its Visible property set to False. Also I'm not going to use the Product Search tab, but I've put it in
for completeness.
Finally, the Category Description text box requires the Categories component that we haven't coded
together yet. Therefore, it won't display the relevant description unless you've coded the Categories
component yourself.
Tag Category
Text box Name txtCategoryDescription
The Products controls on the General Information tag again include a text box control array,
txtProduct():
The Quantity Ordered text box is not part of the control array but is called txtQuantity.
The Combo box is called cboProduct with its Tag property set to Product.
The Command button is called cmdOrderAdd.
446
Creating the GUI Order Form
Two of the text boxes (Subtotal and Total) are part of the txtOrder() control array:
Name Tag
txtOrder(5) Freight
txtOrder(6) OrderID
Name Tag
cboOrderShipVia ShipVia
cboOrderEmployee Employee
447
VB6 UML
You'll need to close all the code windows for the client components in order to enable their
icon on the toolbar.
By default, Visual Basic will rename each instance of your components by adding a 1 to the end of each
name:
448
Creating the GUI Order Form
For this form, we'll simply write one long initialization subroutine that sets all of the properties of the
form's controls. The rest of the code deals with filling combo boxes and performing the updates.
It would be very simple to add a function into our components that takes a list box or combo box as a
parameter (the name of a property associated with that component) and fill the combo box with all of the
records associated with that field. If we had done that, our code probably would have been virtually halved.
Considering what this application is doing, I would say that is pretty good going.
Looking back, we can see that our data provider classes consisted of the Bottom and Middle Classes. In
order to get at these classes we'll need to go through the Top Class. This is because the Bottom and Middle
Classes are not publicly creatable; they can only be accessed through the hierarchy we built. This
guarantees that the components will have control over how the component is used and accessed. We will
see how this works in a moment.
We have built this form with a text box array to make the form run more efficiently. Therefore, we'll create
a long counter to iterate through the text box arrays. The Tag property of the text boxes will be used to
identify which field each of the text boxes is associated with.
To keep things simple and clear, I have not placed constants in this form for field names, nor
made enumerated types for the possible data member names, or set the text box tags equal to
an appropriate constant for the name of each field. Realize that to make this form complete
we would have to make these final changes.
We will use a variable called lngTxtBoxIndex to iterate through the text boxes, and two long counters
to iterate through the values of the Product and Customer records to fill the combo boxes:
449
VB6 UML
The first thing we will do is fill-in the Customer drop-down combo box. Hiding under this combo box is a
text box. The combo box will be used when selecting a customer for an Order, the text box will be used to
enter a new Customer or edit an existing Customer. When we are in edit or review mode, the text box will
be visible, otherwise the combo box will be visible.
First, we will iterate through all of the Customers and add their Company Names to the combo box. We'll
use ctlCustomers to get access to the Customer component. To access the Middle and Bottom objects
we can use the GetCustomerCollection method. This method has a parameter that allows us to
specify the Customer collection we want. In this case, we want to retrieve all Customers:
With ctlCustomers.GetCustomerCollection(e_AllCustomers)
We will call the MoveFirst method of the Middle object to move to the first customer:
.MoveFirst
Now we'll iterate through all of the Customers using the RecordCount property of our Middle object to
determine the number of customers:
The Item property of the Middle object will return the Bottom objects, which will have information on the
current Customer:
cboCustomer.AddItem .Item.CompanyName
.MoveNext
Next
.MoveFirst
Now we will iterate through all of the Customer text boxes to set their DataField equal to their Tag, set
their data member to the appropriate value, AllCustomers, and set their DataSource equal to Item,
which is the Bottom object. Doing this will now bind all of our Customer text boxes to the Customer object.
We will also lock these text boxes, as we don't want the user to change information in them unless they are
editing or adding information:
End With
We'll do almost the same thing with the Order text boxes using the Order control to bind the text boxes to
the Bottom objects of the Order object, except that we won't lock them:
450
Creating the GUI Order Form
With ctlOrders.GetOrderCollection(e_OrderWhoseOrderID)
Once the binding is complete, we need to start adding a new order because this form can only perform an
Add New on the Order component.
Remember, our Sales Representative role is only responsible for entering the Orders, so this
is the only possibility. If you later expanded this form to be used by a person in the Manager
role, you would have to allow for the possibilities of editing and deleting an Order.
.AddNew
.Item.Freight = 0
End With
We'll set the caption so the user knows what the form is doing:
We've not written any code in our components for the ShipVia field. It's likely that we would not want an
entire component for something that has less than five entries. Realistically, ShipVia could become part of
the Order component. Since there are only three values, we'll just hard code the values in, but of course,
these values should come from the database through one of the components:
cboOrderShipVia.ListIndex = 0
We did not create an Employee component so we will just hard code an employee into Employee combo
box:
cboOrderEmployee.AddItem "Davolio"
cboOrderEmployee.ListIndex = 0
451
VB6 UML
We'll now bind all of the Order Details text boxes to the Order Details object using ctlOrderDetails.
In this case, we'll be using a grid control, so we want to bind to all of the Order Detail records in the
collection. To do this we bind the grid box to the Middle object:
With ctlOrderDetails
grdOrderDetails.DataMember = "OrderDetails"
Set grdOrderDetails.DataSource = _
.GetOrderDetailsCollection(e_OrderIDOrderDetails)
End With
grdOrderDetails.Columns.Item(2).Width = 950
grdOrderDetails.Columns.Item(1).Width = 950
grdOrderDetails.Columns.Item(4).Width = 0
grdOrderDetails.Columns.Item(5).Width = 0
Fill in the possible Contact Titles. We could add a method to our Customer component to take a list box and
fill it with the possible titles, too. This would eliminate the need for this code being in the form:
As we have not built a Categories component, we'll also have to hard code in the possible categories. The
CategoryID will be one less than the List Index we are assigning each product:
cboCategory.AddItem "Beverages", 0
cboCategory.AddItem "Condiments", 1
cboCategory.AddItem "Confections", 2
cboCategory.AddItem "Dairy Products", 3
cboCategory.AddItem "Grains/Cereals", 4
cboCategory.AddItem "Meat/Poultry", 5
cboCategory.AddItem "Produce", 6
cboCategory.AddItem "Seafood", 7
cboCategory.ListIndex = 0
We will use ctlProducts to get Product information, and fill in the Product combo box as we did before
for the Customer combo box. We will also bind the text boxes to ctlProducts in the same way we did
for the other objects:
With ctlProducts
.CategoryIDEquals = 1
With .GetProductCollection(e_ProductsCategoryID)
.MoveFirst
452
Creating the GUI Order Form
.MoveFirst
End With
cboProduct.ListIndex = 0
cboCustomer.ListIndex = 0
End With
Finally we will set our date pickers to an appropriate date and call a function that will place all of the
Customer Address information into the Ship To Field:
CustomerAddressToShipperAddress
End Sub
Initialize
End Sub
Finally, we will have the function to put the values into the Shipper fields:
For each Customer text box, iterate through the Order text boxes (which contain the Ship Address
information).
453
VB6 UML
With ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Item
Case "Address"
.ShipAddress = txtCustomer(lngAddressTxtCounter).Text
Case "PostalCode"
.ShipPostalCode = txtCustomer(lngAddressTxtCounter).Text
Case "City"
.ShipCity = txtCustomer(lngAddressTxtCounter).Text
Case "Region"
.ShipRegion = txtCustomer(lngAddressTxtCounter).Text
Case "Country"
.ShipCountry = txtCustomer(lngAddressTxtCounter).Text
End Select
End With
Next
End Sub
cboProduct.Clear
Before we do this, we want to make sure that the product information has been initialized, if it has not,
there will be no value for the DataMember property of the Product text boxes:
454
Creating the GUI Order Form
We will now reset the CategoryID to equal the new CategoryID that was selected, which will be the
ListIndex +1. Once we do this, we must refresh the Product control:
With ctlProducts
.CategoryIDEquals = cboCategory.ListIndex + 1
.GetProductCollection(e_ProductsCategoryID).Refresh
End With
We will now refill the Product combo with the new information:
With ctlProducts.GetProductCollection(e_ProductsCategoryID)
End With
cboProduct.ListIndex = 0
End Sub
ctlCustomers.GetCustomerCollection(e_AllCustomers).Find cboCustomer.Text
Next, we want to call the function to set the Shipper Address to the Customer Address:
CustomerAddressToShipperAddress
End Sub
With ctlProducts.GetProductCollection(e_ProductsCategoryID)
.Find cboProduct.Text
End With
End Sub
455
VB6 UML
Case "ContactTitle"
txtCustomer(lngTxtBoxIndex).Visible = False
cboContact.Text = txtCustomer(lngTxtBoxIndex).Text
Case "CompanyName"
txtCustomer(lngTxtBoxIndex).Visible = True
cboCustomer.Text = txtCustomer(lngTxtBoxIndex).Text
End Select
Next
We next want the update button enabled, and to disable the Add and Edit buttons:
cmdCustomerUpdate.Enabled = True
cmdCustomerAdd.Enabled = False
cmdCustomerEdit.Enabled = False
ctlCustomers.GetCustomerCollection(e_AllCustomers).AddNew
Next, we want to make the combo box for Contacts visible, and combo box for Customer Companies to be
invisible:
cboContact.Visible = True
cboCustomer.Visible = False
Finally, we change the caption of the form so the user can know they are doing a Customer Add New:
456
Creating the GUI Order Form
End Sub
Case "ContactTitle"
txtCustomer(lngTxtBoxIndex).Visible = False
cboContact.Text = txtCustomer(lngTxtBoxIndex).Text
Case "CompanyName"
txtCustomer(lngTxtBoxIndex).Visible = True
cboCustomer.Text = txtCustomer(lngTxtBoxIndex).Text
End Select
Next
cmdCustomerUpdate.Enabled = True
cmdCustomerAdd.Enabled = False
cmdCustomerEdit.Enabled = False
ctlCustomers.GetCustomerCollection(e_AllCustomers).Edit
cboContact.Visible = True
cboCustomer.Visible = False
End Sub
With ctlCustomers.GetCustomerCollection(e_AllCustomers)
457
VB6 UML
.Item.ContactTitle = cboContact.Text
When we are adding a new Customer, we'll need to create a new CustomerID. In this case, we are doing
this in the form, but it could easily be another responsibility of the Customer component.
Iterate through the text boxes, finding the text box with the Tag = CompanyName, and get the first
three letters of company's name
Take the first letter, and throw it into a Randomize function to get a random letter for the fourth letter
Repeat the last step to generate the fifth letter
Of course, it's theoretically possible that we could still end up with two identical IDs. To
allow for this possibility, the server component should have the ability to test if an AddNew
fails because of an already existing ID We didn't add this functionality to our server
component to keep the component as simple as possible.
In our case:
UpperBound is 90 for Z
LowerBound is 65 for A
The seed will be the first letter in the Customer Company Name
(Mid(txtCustomer(lngTxtBoxIndex).Text, 1, 1)
We will use the Visual Basic function Asc to convert the resulting letter to its ASCII number, and the Chr
function to convert numbers to characters. Thus:
458
Creating the GUI Order Form
Next
End If
Some of you may be thinking that the code would be much more compact if I just added
things together, like 90 – 65 +1. The code would be more compact but someone reading
your code would have no idea where 26 came from. Someone reading this code can figure
out that 90 is Z and 65 is A, and determine what the code is doing. Better a few extra lines of
code that can be read than very condensed code that no one can read.
If there is an edit, we already have a CustomerID assigned, so we can now just do an update, and add the
new customer to the combo box:
.Update
cboCustomer.AddItem .Item.CompanyNam
cboCustomer.ListIndex = cboCustomer.NewIndex
End With
We want to once again hide text boxes or combo boxes and reset the command buttons:
txtCustomer(lngTxtBoxIndex).Locked = True
Case "ContactTitle"
txtCustomer(lngTxtBoxIndex).Visible = True
Case "CompanyName"
txtCustomer(lngTxtBoxIndex).Visible = False
End Select
Next
cmdCustomerUpdate.Enabled = False
cmdCustomerAdd.Enabled = True
cmdCustomerEdit.Enabled = True
cboCustomer.Visible = True
cboContact.Visible = False
frmOrders.Caption = "Order Entry: Add"
Exit Sub
cmdCustomerUpdateError:
459
VB6 UML
End Sub
The GetOrderDetailsCollection method of the Order Details control can return the Middle object
of the Order Details collection that has its ID equal to some OrderID. Right now, the collection is set on
the OrderID =0 (the default we put in the Order Details component), so there are no records. This is the
OrderID we want for doing an AddNew:
With ctlOrderDetails.GetOrderDetailsCollection(e_OrderIDOrderDetails)
Start an AddNew:
.AddNew
Use the Item method to return the lower class that has the new Order Detail record:
.Item.Quantity = txtQuantity.Text
Iterate through the Product text boxes and set the appropriate fields:
Case "ProductID"
.Item.ProductID = txtProduct(lngIndexCounter)
Case "UnitPrice"
.Item.UnitPrice = txtProduct(lngIndexCounter)
End Select
Next
460
Creating the GUI Order Form
.Item.Discount = 0.1
.Item.ProductName = cboProduct.Text
Update the Order Details record. We want to pass in a parameter of False so that we only perform a Batch
update that will not go to the recordset. This allows us to add other items to the Order without having to
update the database each time:
.Update (False)
txtOrderSubtotal.Text = CCur(txtOrderSubtotal.Text) + _
CCur(.Item.UnitPrice * .Item.Quantity)
Next
End With
Finally, calculate the new Units In Stock and reset the Quantity:
With ctlProducts.GetProductCollection(e_ProductsCategoryID)
.Edit
With .Item
.UnitsInStock = .UnitsInStock - CLng(txtQuantity.Text)
End With
.Update (False)
End With
txtQuantity.Text = ""
Exit Sub
cmdOrderAddError:
461
VB6 UML
End Sub
With ctlOrders.GetorderCollection(e_OrderWhoseOrderID)
With .Item
.RequiredDate = DTPRequired.Value
.OrderDate = DTPDate
.EmployeeID = cboOrderEmployee.ListIndex + 1
. ShipVia = cboOrderShipVia.ListIndex + 1
End With
Call the special UpdateOrderDetail method of the Middle object, passing in the Order Details
recordset:
.UpdateOrderOrderDetail ctlOrderDetails. _
GetOrderDetailsCollection(e_OrderIDOrderDetails).ItemsRecordset
End With
462
Creating the GUI Order Form
ctlOrderDetails.GetOrderDetailsCollection(e_OrderIDOrderDetails).Refresh
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Refresh
ctlProducts.GetProductCollection(e_ProductsCategoryID).Refresh
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).AddNew
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Item.Freight = 0
CustomerAddressToShipperAddress
txtOrderSubtotal.Text = 0
txtOrderTotal.Text = 0
Exit Sub
cmdOrderSaveClickError:
End Sub
ctlOrderDetails.GetOrderDetailsCollection(e_OrderIDOrderDetails).Cancel
Next we will refresh the order detail object to get a fresh copy:
ctlOrderDetails.GetOrderDetailsCollection(e_OrderIDOrderDetails).Refresh
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Cancel
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Refresh
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).AddNew
ctlOrders.GetOrderCollection(e_OrderWhoseOrderID).Item.Freight = 0
Finally we will refresh the Products (items may have been removed from the
Units In Stock), and clear the text fields:
ctlProducts.GetProductCollection(e_ProductsCategoryID).Refresh
CustomerAddressToShipperAddress
463
VB6 UML
txtOrderSubtotal.Text = 0
txtOrderTotal.Text = 0
End Sub
The project, as far as we're concerned, is now complete. We are now able to run the application, then add
and edit Customers, and place a new Order.
Summary
The code in this form was relatively brief. I did leave out some of the error checking, such as checking if all
the Order or Customer information had been entered before doing a Save, as I wanted to focus on binding
the components to the form. With a few extra additions to our components, such as the ability to fill combo
boxes, we could even reduce the code in our form to only a few pages!
Going back to our original idea of DNA, building components that can easily be reused in a multitude of
places, we begin to see how our data binding classes help us fulfil this goal. Making the user interface that's
using these components, even for the most complicated task such as Order Entry, boils down to simply
setting properties and calling a few methods.
We could build a multitude of applications from these components, and all of them would require only a
small amount of coding to function properly. To me, this is incredible.
464
Testing and Quality Control
While good design may be the most important ingredient in a successful Visual Basic project, Quality
Control is nearly as important. Unfortunately, both Quality Control and Project Design are often
completely ignored by Visual Basic programmers.
Since this book is about UML design and development, rather than Visual Basic Quality Control, we're not
going to discuss it in much detail. What this chapter should give you is some helpful ideas on debugging
classes. I would therefore urge you to consider this an important topic – even if I am only able to introduce
you to it here.
Unit Testing
Integration Testing
Unit Testing
This level of testing involves a separate test of each individual component by itself to see if it works
properly. Every method, property and Business Rule should be tested. We should also attempt to follow
every alternative flow in our use cases and make sure that proper error handling is in place.
VB6 UML
Integration Testing
This level builds upon unit testing, where several components are put together and tested. The results can
be quite unexpected.
In this chapter, we're going to combine the unit testing with the integration testing. We can do this because
the only thing the Top Class does is retrieve a Middle Class. Therefore, the only thing to test in the Top
Class is its ability to retrieve a Middle Class and make sure that the Middle Class and the Top Class
communicate correctly.
It therefore follows that as the Top Class also communicates to the Bottom Class through the
Middle class, we are also able to test accessing the Bottom Class from the Top Class via the
Middle Class.
If we perform a series of tests for every method and property, we'll probably find many dormant bugs. By
this process, we can usually eliminate all major bugs, and all but a very few, harder-to-find minor bugs.
These last stubborn bugs will probably be found during the last stages of beta testing.
Quality Control
Testing should be based upon a careful plan. Our UML diagrams can help us make such a plan. We're going
to see a simple way to develop a plan and test our project that anyone can do.
This plan involves making a chart for every method and property of our class. This chart should have the
following columns:
There should also be a section for alternative flows. It should show how the system would respond to
alternative flows, which use case the alternative flow comes from, and the Business Rule associated with
the alternative flow.
468
Testing and Quality Control
Once all of these sections have been mapped out, we need to write code that will test each of the conditions.
For example, we could have the following chart for the CustomerID Property Let methods:
Alternative Flows:
From this information we can see that we should test the following:
Setting the CustomerID to a unique value that is less than or greater than five characters, see if an error
is raised.
Setting the CustomerID to a non-unique value that is less than or greater than five characters, see if an
error is raised.
Setting the CustomerID to a valid value and see if the disconnected recordset is updated.
Setting the CustomerID to an invalid value to ensure the disconnected recordset is not updated.
Setting the CustomerID to a new value during an Edit to see if an error is raised.
When we perform these tests on our Customer component, we discover that we don't actually check if a new
value is unique. Trying to add a non-unique value for the primary key would cause an error to be raised
from the server component when the database rejected the new CustomerID value. It would be better if the
error were handled by creating a new ID at the server component, and using the new ID to complete the
update.
The focus of this chapter is creating a test function that will test a component. This method of testing could
be used to test all of the above possibilities.
469
VB6 UML
To keep it simple, we'll write the debug information to the Immediate window, but in a real test it would
be better to write it to a file or database. Getting the hang of this is quite easy - so we won't test every
function, or any of the alternative flows.
This will avoid us having to write too much more code in this book… but I hope you will
recognize the benefit of more thorough testing in your own mission-critical projects.
This line allows us to create conditional code, denoted with # marks, which is only run when the above line
is in Project Properties. To turn off this debugging, set DebugclsCustomer = 0.
470
Testing and Quality Control
Now go back to the General tab and set the StartUp Object as Sub Main:
Now add the following code to basCMain. It creates an instance of our Customer object and calls a
conditional debugging routine that we'll create in a minute:
Sub Main()
objCustomers.DebugCheckCustomers
#End If
End Sub
Now go to the code window for the clsCustomers class and add the following:
This creates a function that is visible only when we have the DebugClsCutomers = -1 set in the
Project Properties.
471
VB6 UML
We will first test if the GetCustomerCollection has successfully retrieved the e_AllCustomer
collection:
With GetCustomerCollection(e_AllCustomers)
Debug.Print "Test if customer recordset has been retrieved"
We'll first of all try to retrieve the RecordCount. We can check this against the real Record Count in the
database. If the RecordCount is correct, the AllCustomers Collection was initialized properly:
We will next try to edit a Customer record. We want to simulate a bound control changing the values of the
fields. We'll do this by getting the recordset through the ItemsRecordset property, and then change the
fields.
We start by checking the Edit Mode property in both the Customer Middle and Top Classes:
With GetCustomerCollection(e_AllCustomers)
Debug.Print "Customer EditMode:" & .Item.EditMode
Debug.Print "Customer Managing EditMode:" & .EditMode
.Edit
We now need to check if the EditMode property has been properly changed:
First, let's print out the current values by using the properties in the Bottom object:
With .Item
472
Testing and Quality Control
Now we'll check the values from the recordset. These are the values a Data Consumer control bound to the
Bottom object would have. These values should be the same as those above:
With .ItemsRecordset
Debug.Print "Customers Object Values: " & vbCrLf & _
"CompanyName: " & .Fields("CompanyName: ") & _
vbCrLf & "ContactName: " & .Fields("ContactName") & _
vbCrLf & "ContactTitle: " & .Fields("ContactTitle:") & _
vbCrLf & "Country: " & .Fields("Country") & vbCrLf & _
"CustomerID: " & .Fields("CustomerID:") & vbCrLf & _
"Fax: " & .Fields("Fax") & vbCrLf & _
"Phone: " & .Fields("Phone") & vbCrLf & _
"PostalCode: " & .Fields("PostalCode") & vbCrLf & _
"Region: " & .Fields("Region") & vbCrLf
Now we'll try to directly change the records in the recordset (an alternative flow would be to try to change
the Primary Key):
.Fields("CompanyName") = "TestCompanyName"
.Fields("ContactName") = "TestContactName"
.Fields("ContactTitle") = "TestContactTitle"
.Fields("Country") = "TestCountry"
.Fields("Fax") = "TestFax"
.Fields("Phone") = "TestPhone"
.Fields("PostalCode") = "TestP"
.Fields("Region") = "TestRegion"
Debug.Print "CUSTOMERS editmode: " & .EditMode
End With
.Update
Next, we'll see if both the properties and the Fields collection return the new values. First check the
properties:
With .Item
Debug.Print "Item Object Values: " & vbCrLf & _
"CompanyName: " & .CompanyName & vbCrLf & _
473
VB6 UML
With .ItemsRecordset
Debug.Print "Customers Object Values: " & vbCrLf & _
"CompanyName: " & .Fields("CompanyName: ") & _
vbCrLf & "ContactName: " & .Fields("ContactName") & _
vbCrLf & "ContactTitle: " & .Fields("ContactTitle:") & _
vbCrLf & "Country: " & .Fields("Country") & vbCrLf & _
"CustomerID: " & .Fields("CustomerID:") & vbCrLf & _
"Fax: " & .Fields("Fax") & vbCrLf & _
"Phone: " & .Fields("Phone") & vbCrLf & _
"PostalCode: " & .Fields("PostalCode") & vbCrLf & _
"Region: " & .Fields("region") & vbCrLf
End With
Now we're going to try to Edit the same record by changing the properties in the Bottom object. We know
the current values (they were just printed) so we can immediately try to change the values:
.Edit
With .Item
.CompanyName = "TestCompanyName3"
.ContactName = "TestContactName3"
.ContactTitle = "TestContactTitle3"
.Country = "TestCountry3"
.Fax = "TestFax3"
.Phone = "TestPhone3"
.PostalCode = "Test3"
.Region = "Test3"
End With
Let us also make sure the EditMode is being properly set back when we do the update:
With .Item
Debug.Print "Item Object Values: " & vbCrLf & _
"CompanyName: " & .CompanyName & vbCrLf & _
474
Testing and Quality Control
With .ItemsRecordset
Debug.Print "Customers Object Values: " & vbCrLf & _
"CompanyName: " & .Fields("CompanyName: ") & _
vbCrLf & "ContactName: " & .Fields("ContactName") & _
vbCrLf & "ContactTitle: " & .Fields("ContactTitle:")& _
vbCrLf & "Country: " & .Fields("Country") & vbCrLf & _
"CustomerID: " & .Fields("CustomerID:") & vbCrLf & _
"Fax: " & .Fields("Fax") & vbCrLf & _
"Phone: " & .Fields("Phone") & vbCrLf & _
"PostalCode: " & .Fields("PostalCode") & vbCrLf & _
"Region: " & .Fields("Region") & vbCrLf
End With
.AddNew
Print out the values to make sure a new record has been returned:
With .Item
Debug.Print "Item Object Values: " & vbCrLf & _
"CompanyName: " & .CompanyName & vbCrLf & _
"ContactName: " & .ContactName & vbCrLf & _
"ContactTitle: " & .ContactTitle & vbCrLf & _
"Country: " & .Country & vbCrLf & _
"CustomerID: " & .CustomerID & vbCrLf & _
"Fax: " & .Fax & vbCrLf & _
"Phone: " & .Phone & vbCrLf & _
"PostalCode: " & .PostalCode & vbCrLf & _
"Region: " & .Region & vbCrLf
End With
With .ItemsRecordset
Debug.Print "Customers Object Values: " & vbCrLf & _
"CompanyName: " & .Fields("CompanyName: ") & _
vbCrLf & "ContactName: " & .Fields("ContactName") & _
vbCrLf & "ContactTitle: " & .Fields("ContactTitle:") & _
vbCrLf & "Country: " & .Fields("Country") & vbCrLf & _
"CustomerID: " & .Fields("CustomerID:") & vbCrLf & _
475
VB6 UML
.Fields("CompanyName") = "TestCompanyName4"
.Fields("ContactName") = "TestContactName4"
.Fields("ContactTitle") = "TestContactTitle4"
.Fields("Country") = "TestCountry4"
.Fields("CustomerID") = "TsID4"
.Fields("Fax") = "TestFax4"
.Fields("Phone") = "TestPhone4"
.Fields("PostalCode") = "Test4"
.Fields("region") = "TestR"
Debug.Print "Customers EditMode" & .EditMode
End With
.Update
Debug.Print "AddNew complete"
With .Item
Debug.Print "Item Object Values: " & vbCrLf & _
"CompanyName: " & .CompanyName & vbCrLf & _
"ContactName: " & .ContactName & vbCrLf & _
"ContactTitle: " & .ContactTitle & vbCrLf & _
"Country: " & .Country & vbCrLf & _
"CustomerID: " & .CustomerID & vbCrLf & _
"Fax: " & .Fax & vbCrLf & _
"Phone: " & .Phone & vbCrLf & _
"PostalCode: " & .PostalCode & vbCrLf & _
"Region: " & .Region & vbCrLf
End With
With .ItemsRecordset
Debug.Print "Customers Object Values: " & vbCrLf & _
"CompanyName: " & .Fields("CompanyName: ") & _
vbCrLf & "ContactName: " & .Fields("ContactName") & _
vbCrLf & "ContactTitle: " & .Fields("ContactTitle:") & _
vbCrLf & "Country: " & .Fields("Country") & vbCrLf & _
"CustomerID: " & .Fields("CustomerID:") & vbCrLf & _
"Fax: " & .Fields("Fax") & vbCrLf & _
"Phone: " & .Fields("Phone") & vbCrLf & _
"PostalCode: " & .Fields("PostalCode") & vbCrLf & _
"Region: " & .Fields("region") & vbCrLf
End With
End With
#End If
We could have improved this code by creating a function to print out the current values
using the properties and using the recordset, but I wanted to keep the code as simple and
clear as possible.
476
Testing and Quality Control
This is all the code we're going to include for this test, as I'm sure you've got the hang of it by now. We
could have gone on to test the following:
Placing values into the recordset through from the Bottom to simulate a bound Data Consumer
Deleting records
All of the alternative flows in our use cases
Business rules
And more...
You can now run your project and debug the Customer component.
This would give us a reference to a valid recordset that we could use for testing clsCustomer.
Summary
This small chapter should give you a few good ideas on how to debug your classes.
Every time I made a change to the project, I would run these test functions. I could detect an error in a class
immediately. It was then just a matter of putting break points in, tracing the code and finding the source.
While it's true a few errors took me some time to find, most of them were tracked down in a few minutes.
These little test functions were a lifesaver. Without them, I would never have made my deadline, and you
would not be reading this chapter.
It's not so much about the method we actually use to test our classes; what is important is that we do test
them in a thorough, consistent manner. Making charts for our properties and methods is a simple, thorough
way to test our components. We can make the charts, then go through our Business Rules and use cases to
fill in the appropriate sections. From this we can make a list of what needs to be tested.
In this way, we can be sure that every rule, alternative flow and use case will be tested. Finally, we can
make a test function, just as we did above. The end result should be a component that is completely bug-
free and ready to be plugged into our project.
477
What Comes Next?
Overview
We have finally reached the end of the road. In reality, this is only the beginning of course. These chapters
have only laid out the basic framework that you can use to make powerful Visual Basic Enterprise
applications.
When I was given the task of writing this book, I not only wanted to write it to teach you about UML, but
also to give you plenty of useful information about design and development in Visual Basic with UML…
information that you might not be able to find elsewhere. As I did my research for this book, I found some
rather interesting things. To begin with, there were very few books on UML and Visual Basic. I suppose
this is because most Visual Basic programmers still have not realized the value of UML when they're
creating a Visual Basic project. I would consider proper design as the most critical element in making all
projects. You simply cannot produce good Visual Basic code without good design, and one of the best tools
for object-oriented design is UML.
Since UML is for modeling object-oriented projects, and Visual Basic is still not fully accepted as an OOP
language, it is likely that this attitude has also played a role in the lack of Visual Basic UML books. I hope,
once and for all, that this book will lay to rest the question of Visual Basic's ability to build object-oriented
programs. We have not only built VB objects and components in this book, but we did it with UML patterns
and Visual Basic code templates.
VB6 UML
Some UML books are very intense and theoretical. These are great books for universities, hardcore C++
programmers, and those who like deep theory. For those who were expecting an intense theoretical
discussion of UML and the models, I apologize if I disappointed you. There are many great books that can
give you the entire history of object-oriented modeling. In this book, I was more interested in teaching you
how to make the diagrams than who invented a particular diagram, its entire lineage through the history of
time, and the impact each diagram has had on modern civilization.
By far the most difficult problem I found, while researching UML, was finding a book that really told me
how to apply UML to making my applications. Sure, there were books with samples, but these samples
were simplistic and had absolutely nothing to do with the Visual Basic programs that you and I are going to
be making for the Enterprise applications. Most books do not even explain why we should bother going to
all of the trouble of making these diagrams.
It is my hope that this book has provided you with an insight into how and why Visual Basic programmers
should make UML diagrams, and how we can use these diagrams to build real-world Visual Basic projects.
Using UML
UML diagrams make our lives easier. They shorten the time it takes to make our project, they clearly define
how we're going to build our project, and they provide us with a tool to experiment with the design of our
project in an easy, clear way. I could not imagine programming without them.
There is a small learning curve when you first explore how to make UML diagrams, but once you figure it
all out, they become easy. You can use sophisticated tools like HOW, or scribble little diagrams on the back
of napkins. It doesn't matter. UML works everywhere, even on paper scraps.
It was my hope that those of you who had some basic understanding of all of these concepts would gain a
deeper understanding of them, and how they fit together, by working out a real-world example. I have tried
at every turn to avoid complicated code. I realize that the end result was still some pretty heavy code
samples, but I believe they are worth your time to understand.
480
What Comes Next
This book was not only about UML: it was also about Microsoft's DNA framework. DNA can be explained
in many ways, but in its simplest form DNA boils down to this:
Build reusable components that can be used in many different applications. These
applications will do different things and may work under different conditions.
Thus, we can build a Product component that works in:
A Visual Basic Order Entry application, providing a list of products for creating an order
An ASP page providing data for HTML pages for customers to order products through the Internet
A Visual Basic ActiveX document that generates a sales report for each product for management
running over an intranet
This is what DNA is all about. Building components like this should reduce production time, and enable
developers to make a multitude of projects with little effort. Yet, for this to work, there is one hidden
requirement: the components must not require a great deal of code to work.
Consider this: if I need one hundred pages of code to bind a component to my form's objects, then what
have I gained? If this is required, I will end up writing nearly as much code to reuse my components than I
would have if I had just built several applications without them.
The key to DNA is not just building reusable components, but to build reusable components that can be
used with a minimum amount of work and code. What we've built in this book is just this type of
component. In the end, we found ourselves making Client components in a cookie cutter fashion. Based on
the Customer component template, we were able to quickly build the rest of our components. Once the
components were done, it took less than six pages of code to make our order entry application functional.
By adding a few more features onto our components, we could have probably reduced the code to two or
three pages. This is what DNA is about, and this is what good design, using UML and project management,
can achieve.
Components like these can only come out of a thorough design phase. Once they are completed, they
become a powerful tool that can reshape the way you wish to develop your programs.
I would like to finish this book by discussing where we would take our project from the point we have
currently reached.
It's likely that if Northwind really existed, and we were actually making this project, the next
step would be completing the Client components.
Using the pattern that we made for the Customer component, we could now create a complete set of UML
diagrams for all of our components. Our Order component would be the most complex, and I suggest you
sit down and try to draw the sequence diagrams for this component.
481
VB6 UML
If we had tried to jump into the code for the Order component, without a thorough design phase, we would
probably have found ourselves one month into coding before we suddenly realizing that we were not
making this component in the best way possible. If this had happened, we would have been left with a
choice of either continuing down a bad path, reworking the component, or throwing away a month of work.
Even in the best situation, where we chose the right technique, we would be coding a component without
any guidance or direction. Why would we want to do something so frustrating when we can map everything
out with UML?
The second portion of the application could include essential reports and any additional key functionality
such as deleting Employees. When this part of the project is complete, the final stage may be additional
non-essential reports and perhaps some additional features on the user interface. The last phase of the
project may be to use these components to build an Internet application. The various components could be
used to pass information to an ASP page that will build HTML code for an Internet e-commerce site
running under Microsoft Commerce server.
How a project is divided can depend on many factors such as the client’s needs, the project
deadlines, critical features, etc. It is not important how the project is divided, only that the
project is built with components and it uses a staged delivery of functioning components.
Visual Basic is a powerful language that is capable of building powerful Enterprise applications. Hopefully,
this book has given you some tools to build your own Enterprise Visual Basic solutions. Thank you for
taking the time to read 'Visual Basic 6 UML Design and Development'. I hope it has provided you with
useful information.
482
UML to VB Mapping
As of writing, version 1.3 was in draft, although there are no major changes to this version that VB
developers need to be aware to.
It is recommended that VB Developers visit this site and review the official documents before trying to
understand the mapping in detail.
Therefore, the mapping that follows concentrates on the concrete elements from the UML that have notation
and appear on diagrams. These are also the artifacts that the VB Developer will have the most contact with.
These include class, attribute, operation, interface, etc. Each of these elements will be discussed in turn
and references will be made to the more abstract elements in the UML semantics document that they derive
from, so that the developer can choose to cross-reference with the official specification.
So to reiterate, the mapping focuses essentially on how the diagrams you draw and communicate with relate
to the code you write.
Please refer to the Mapping Guide at the beginning of the mapping itself for a breakdown of
the sections therein.
Statechart diagrams, on the other hand, are not so clear. This is because the standard VB language does not
come with a 'standard' statechart implementation (often called a 'finite state machine'). This is also partly
because of the many different ways we can write them. Nonetheless, some guidelines are provided to help
get you started.
Fortunately for us, we can come up with conventions that developers can follow to help us define
components. A further discussion of this can be found in the mapping.
Concrete Or By Convention?
Some mappings from the UML to VB are plain and concrete - there is no debate as to its correctness. Some
mappings, on the other hand, are purely by convention because VB simply doesn't support the construct.
When convention is used, developers should use their judgment as to whether they follow the convention or
invent their own. Remember, though, that the suggestions given are tried and tested.
486
UML to VB Mapping
While sections of VB code that are provided as context, but are not directly relevant to the topic under
discussion are presented like this:
As you become more familiar with the UML, you will soon only need to refer to this Appendix periodically,
as the core mappings are very intuitive and easy to remember.
There are many choices here from general symbol tools like Visio (UML templates are available for this on
the web) to a full-blown repository-based CASE tool that will also ensure your diagrams are in-sync' with
each other. Of course never underestimate the power of a whiteboard and marker!
So, without further ado, let's start to look at the VB to UML mapping.
487
VB6 UML
Mapping Guide
These are the sections that are included in this mapping.
488
UML to VB Mapping
4.6.2 Qualifiers
4.6.3 N-ary Associations
4.6.4 Aggregation and Composition
4.6.5 Implementing Associations
4.6.6 Basic 1-to-1 Unidirectional Associations
4.6.7 Basic 1-to-Many Unidirectional Associations
4.6.8 Implementing Qualifiers
4.6.9 Bi-directional Associations and Referential Integrity
4.6.10 Problems with Simple Associations
4.6.11 Link Management Routines
4.6.12 Association Classes and Link Management
4.6.13 Bi-directional 1-to-Many Association with Link Management and Qualifier
4.6.14 Miscellaneous Advice
5.0 Use Case Diagrams
5.1 A Word of Warning
5.2 Use Cases and Transactions
5.3 Use Cases and Controllers
6.0 Behavioral Diagrams
7.0 Sequence & Collaboration Diagrams
7.1 Object Instances and Context
7.2 Interactions
7.3 Pseudo-Code
7.4 Collaboration Diagrams
8.0 Statechart Diagrams
8.1 Mapping States and Transitions
8.2 Implementing State
8.3 An Example
8.4 Implicit Implementation
8.5 Explicit Implementation: A Finite State Machine (FSM)
9.0 Activity Diagrams
10.0 Implementation Diagrams: Component & Deployment Diagrams
10.1 UML Components and Visual Basic
10.2 Logical components
10.3 Physical Components
10.4 Stereotypes
489
VB6 UML
In VB, this would translate into validation code inside the procedures of the VB class, such as an Order
class:
With this example, an order can only be associated with one customer. It is a constraint that only one
customer can be associated at a time. (Assume a separate routine is available to disassociate the customer.)
One useful feature of VB is to make copious use of Debug.Assert. This build-in library feature allows
us to make assertions in our code that must always be satisfied. If you are running in design mode and an
assertion fails, then VB will take you straight to the place where the assertion failed. This is very effective
way of catching logic bugs in your code, early.
Sometimes it is not always possible to translate a constraint to code. In these situations, the best you can do
is place an appropriate comment into the code to remind yourself and your colleagues that the constraint
exists.
490
UML to VB Mapping
1.3 Stereotypes
Most UML elements come with a set of standard stereotypes. Some CASE Tools also extend the range of
stereotypes to match the languages targeted. Here are examples of two class stereotypes:
«type» «interface»
::Document ::IPersistent
Stereotypes have a big impact on how we map UML to VB. In some cases, they affect only the properties of
the target VB item and sometimes it will completely change the mapping.
When we discuss each UML construct, the standard stereotypes of that item will also be covered.
::Accountancy
None A file system mapping by convention only.
491
VB6 UML
2.1 Packages
Packages in the UML are a logical grouping of related items such as classes. A Package has no direct
mapping to VB so conventions are used instead.
A convention typically used is to include the package name in the name of contained elements. For
instance, an Accountancy Package may contain a class called Account. Therefore, the class will have a
name in VB of Accountancy_Account. This will ensure that classes with similar names in different
packages will not collide. This activity is usually called Name Mangling .
Account Account
Balance: Currency Access Permissions
Withdraw (in Funds)
::RDO
Recordset
Database
492
UML to VB Mapping
::Order Processing
Orders Customers
Invoicing
This technique will not only keep the source code files manageable, but and also help partition the work up
between developers. For instance, one developer may be working on an Accountancy package, whilst
another is working on a User interface package.
A <<façade>> package indicates that the package is present to provide a façade on to other packages in
the system. In VB terms, this could represent a set of related classes that are providing a simple, more high-
level interface onto another set of more detailed classes. Again, because package is a logical grouping or
items, there is no explicit language feature in VB that you can use. It is all defined by convention.
A <<framework>> package indicates that the package contains a framework of items. In VB terms, we
might design a set of related classes that perform a certain framework service, e.g. generic database access.
The <<framework>> constraint simply implies that the package would be used as part of a complete
system, together with the extra classes that complement the framework and make it complete.
493
VB6 UML
::Order Processing
<<imports>>
::RDO
Association
Attribute
Variable
- Balance : Currency
494
UML to VB Mapping
Class
Classifier
Abstract class of Class,
Class
DataType and Interface
(no notation)
Constraint
Assert statement, comment
or validation code.
{ ordered }
Interface
IPersistent
A specialization of Classifier.
For an ActiveX component,
or – Class
Instancing =
PublicNotCreatable
«interface»
::IPersistent
Operation
Procedure without body Typically on a VB interface
Withdraw()
4.0 Classifiers
4.1 General
A UML Classifier is the abstract definition of a Class, DataType or Interface in the UML. All of these
elements map to a VB Class, Standard Module or Form.
( Classifiers are defined in the UML to simply capture the similarities between Classes, DataTypes and
Interfaces. They are abstract however and don't appear directly on diagrams. Consult the UML semantics
document for further details about them.)
495
VB6 UML
UML classes are the most used of all the Classifier types and these map straight to VB Classes or Forms as
shown:
Account
Balance: Currency
Withdraw (in Funds)
4.1.1 Name
The name of the UML classifier becomes the name of the VB item.
4.1.2 Features
All UML classifiers have features, which are further defined as attributes and operations and these map to
VB Member Variables and Procedures, respectively. These are discussed later.
If we consider these two forms of inheritance, it is important to realize that the first form is more important
for building components that exhibit good qualities of design like low coupling. (A component with low
coupling means that the component has few dependencies on other parts of a system. This means that the
component could be used easily in other systems without too much work). The details of this are beyond the
scope of this book and I suggest you read the 'VB Books On-line' for a background on interfaces and the
Implements construct.
In UML terminology, a class can realize many interfaces. This directly maps to the meaning of
Implements, in VB.
496
UML to VB Mapping
<<extends>>
::Customers
AddCustomer
GetCustomer
Code reuse via Inheritance Code reuse via composition (the VB way)
In this situation, any operations you want to reuse, involves writing delegation code to delegate to the
reused object. Here is an example 'customers' collection:
Of course this can be very tedious when there are many services involved. This is where CASE tools are
useful for doing the 'grunge work'. VB Developers may wish to develop their own VB 'add-in' that performs
this task.
So to recap, if you are turning an <<extends>> inheritance relationship into containment, do the following:
497
VB6 UML
If the stereotype is <<implements>> then this maps simply to the VB construct 'implements':
«interface» «interface»
::CompanyAsset ::PersistentObject
GetValue Load
Save
Implements Implements
::Employee
GetValue
Load
Save
The VB code for the above model could look like this: -
'Employee Class
Implements CompanyAsset
Implements PersistentObject
One convention we can use is to create a standard (.bas) module for each class that has static members. All
static operations and attributes can then be added as members of this module. When naming the module, try
to pick a name that relates the module to the VB class. For instance, if we have a class called Account, we
may choose to name the module AccountClass.
498
UML to VB Mapping
Account
Balance: Currency
OverdraftRate: double {static}
Withdraw (in Funds)
If you include parameterized classes in your design then this is OK but remember that you will have to
manually write the specific versions yourself. It may be better to avoid modeling with them to start with.
It is good practice to choose a filename for the class that is similar to the class name.
4.2 Interfaces
An interface, which is a specialized Classifier in the UML maps directly to a class in VB.
This mapping is not explicitly stated and checked in VB - it is only apparent that a VB class is an interface
by the way it is used by other classes and the fact that the VB class contains empty procedures.
499
VB6 UML
Interfaces are crucial when building components in VB. To expose an interface from a VB ActiveX
Component, the Instancing property of the VB class should be set to PublicNotCreatable .
In all other respects, an interface has a similar mapping to a UML class in that is also has features that can
be operations, attributes, associations, etc.
Note: Developers should not confuse interface with user-interface or user-interface class. Refer to the UML
documentation for a precise definition of interface.
4.3 Attributes
An attribute of a UML classifier maps to a VB variable.
Account
'Account class
- Balance: Currency '
+ Withdraw (in Funds) Private Balance As Currency
4.3.1 Visibility
The visibility of the attribute has a partial mapping to the access of the VB attribute as captured in this
table:
4.3.2 Name
The name of the attribute simply maps to the name of the VB variable.
4.3.3 Type
The type specified for the attribute will be the name of either a built-in VB type such as an Integer or a
Long, a user-defined type or a VB object type such as a Class or Form.
500
UML to VB Mapping
'Account class
Account '
Private m_theBalance As Currency
- Balance: Currency = 10
Private Sub Class_Initialize()
+ Withdraw (in Funds) 'this generous bank gives you a tenner to
'start!
m_theBalance = 10
End Sub
If the attribute is tagged as {frozen} then this implies that the attribute is constant. Constant attributes are
easily represented in VB as Const variables. For example:
Note: VB does not allow classes to declare public constants. This is more of a nuisance rather that a logical
restriction. It is recommended that constants that relate to a class be placed in module that is closely related
to either the class or the package containing the class. For instance, if you have a package called
Accountancy that contains the related classes, Account and Ledger , then you might want to place your
constants in a bas module that is related to the package called, say, Accountancy
( Accountancy.bas ).
4.4 Operations
An operation on a UML class maps to a VB Procedure.
4.4.1 Visibility
The visibility of the operation has a partial mapping to the access of the VB procedure as captured in this
table:
The actual meaning of public, protected and private really comes down to the language you are using to
implement your models in. In C++ for instance, public, protected and private all have quite distinct
meanings that are different to VB.
501
VB6 UML
In VB, the visibility options provided in the UML will map to different things in VB. When we are building
VB components, we are interested in three levels of access:
So when using the UML we should choose the mapping as shown in the table.
4.4.2 Name
The name of the operation simply maps to the name of the procedure in VB.
4.4.3 Parameters
(see below)
If a return type is present then we map the item to a VB Function, otherwise we map it to a Subroutine:
'Account class
Account Private m_theBalance As Currency
If we really want to make a distinction in VB, then we can say that the empty procedures defined in a VB
interface are the operations and the methods are the actual implementations in each of the VB classes that
implement the interface.
4.4.6 Parameters
Each parameter in UML has the following form:
502
UML to VB Mapping
Kind
The kind of the operation is either in , out or inout , and indicates how the parameter is passed to the
operation's body. VB has an approximate mapping for these:
Type
The type specified for the parameter will be the name of either a built-in VB type such as an Integer or a
Long, a user-defined type or a VB object type such as a Class or Form.
Default Value
If a parameter is given a default value then this can be implemented in VB too. For a VB parameter to have
a default value, the Optional keyword must be present:
::Document
4.4.8 Stereotypes
A <<get>> or <<set>> operation stereotype is an operation whose role is to provide access to a value on
the object. In VB terms, Property procedures are good examples of these types of operations.
4.5 Utilities
A UML utility class represents just a cohesive group of global procedures or variables. These are usually
mapped to standard (bas) modules in VB because they don't have multiple instances like a regular class
does. Examples of utility classes are the standard procedure libraries that come with VB. It is convenient to
show them in this way to make a model easier to understand. These are shown as a stereotype of class:
503
VB6 UML
«utility»
::WINAPI
SendMessage
PostMessage
GetDlgItem
CreateWindow
4.6 Associations
4.6.1 Multiplicity
The multiplicity of an association determines how we implement the association. If the multiplicity is 1 ,
then we may choose to implement the association using an object reference. If the multiplicity is a fixed
number then we might employ the use of VB arrays. If the multiplicity is unbounded, e.g. 1+ then a
collection may be used instead. See the implementation section for examples of this.
4.6.2 Qualifiers
A UML qualifier in is effectively a key for navigating to the object at the other end of an association. For
instance, we may initially model that a Person owns many Cars . However in later design revisions, we
may state that a Person owns a single Car when qualified by a particular registration plate. See the
following examples for more about this.
'Class Car
'My engine – this will be created when I get created
Dim m_theEngine As New Engine
504
UML to VB Mapping
Here is the code for a simple unidirectional association (note that the example actually holds a link from
Car to Person ).
505
VB6 UML
To test the association, we will place some code in the main routine of the application:
Sub Main()
'simple 1 to 1 unidirectional association
A basic implementation of this is to embed a collection of Cars into Persons and then allow client code
to add Car s to Person at will.
506
UML to VB Mapping
Note the embedded, hidden Cars collection in Person . This holds the references to the associated Car
objects. The code for Car follows. (This was generated by the class builder add-in.)
Option Explicit
End Function
507
VB6 UML
mCol.Remove vntIndexKey
End Sub
To test the collection, we can write some code for the main application routine
like this:
Sub Main()
'simple 1 to many unidirectional association
The example code assumes that the collection is not ordered in any way.
508
UML to VB Mapping
Building collection classes can be tedious so it is recommended that developers take a look at the Class
Builder add-in, which provides collection-building features.
1 Owns 1
::Person Registration ::Car
Owner Transport
As mentioned in the mapping, a qualifier becomes an index when traversing the association. If we are using
property procedures, then the qualifier will become a parameter of that procedure. So, to access a single
Car that the Person owns, we must provide a registration number. Clients wishing to access an individual
car would supply the registration number of the car of interest:
509
VB6 UML
Here is the code for Person also with a link back to Car:
Option Explicit
Sub Main()
'1 to 1 bidirectional association
'with *no* referential integrity
510
UML to VB Mapping
If we want to build more robust bi-directional associations, we must build referential integrity into our
models. We can do this in a number of ways.
So let's revisit our original 1-to-1 bi-directional association between Person and Car but this time, we
will add some association link management.
The code for Person and Car is almost the same as before but we have a new module to add which
contains the link management:
511
VB6 UML
Option Explicit
Sub Main()
'1 to 1 bidirectional association
'with referential integrity
512
UML to VB Mapping
Let's assume that association between Person and Car is supplemented with a 'Log Book' that records the
Car 's history:
::Person 1 Owns 1
Registration ::Car
Owner Transport
::Log Book
Milage
We will now want to ensure that when a Person is associated to a Car , a LogBook is also associated. We
will assume that the LogBook already exists. If we do this, we can end up with the following
implementation.
(Note that some properties in the code have been omitted from the model to make it easier to read. In fact,
we can see that the LogBook in the code actually has a number of {derived} attributes that get their
information from the Car.)
Firstly, here is the LogBook association class, which also contains the link management code:
'attributes ------------------------------------
513
VB6 UML
'operations ---------------------------------
'Register the log book for use
Public Sub Register(ByVal aPerson As Person, ByVal aCar As Car)
'delegate to link
Link aPerson, aCar
End Sub
m_blnInitialized = True
End Sub
Here is the Car source code now (some extra attributes have been added to spice things up):
'Car Class
'our private implementation
Private m_Make As String 'make of car
Private m_myRoadMilage As Long 'My Milage
514
UML to VB Mapping
'attributes ---------------------------
'operations ---------------------------
'Bump up the car milage
Public Sub BumpMilageBy(ByVal someMiles As Long)
m_myRoadMilage = m_myRoadMilage + someMiles
End Sub
When we now want to associate Person and Car, we first create a LogBook and then get the LogBook
to associate all three objects together. In the example client code we first associate the objects and then
iterate over the assocation and list the information:
(Code in App.bas )
Option Explicit
Sub Main()
'1 to 1 bidirectional association with association class
515
VB6 UML
516
UML to VB Mapping
'attributes ------------------------------------
'operations ---------------------------------
'Register the log book for use
Public Sub Register(ByVal aPerson As Person, ByVal aCar As Car)
'delegate to link
Link aPerson, aCar
End Sub
m_blnInitialised = True
End Sub
517
VB6 UML
Here is the revised Person class. Note that the Transport property is now qualified with a registration
plate:
'attributes -------------------------------
518
UML to VB Mapping
'operations ---------------------------------
Private Sub Class_Initialize()
Set m_Transport = New Cars
End Sub
We also have a slightly revised implementation of the Car s collection. This is the same as the previous
version except that it has been changed to accept an existing object rather than creating it itself. Here is the
revised Add operation.
'add a car
Public Function Add(ByVal aCar As Car) As Car
mCol.Add aCar, aCar.Registration
Option Explicit
Sub Main()
'1 to many, bidirectional association with association class
'an owner
Dim aPerson As New Person
aPerson.Name = "Russell"
'the cars
Dim aFastCar As New Car
aFastCar.Make = "Ferrari"
aFastCar.Registration = "R999 ICH"
aFastCar.BumpMilageBy 50
519
VB6 UML
Points to note:
Car objects can now navigate to their associated LogBooks. This was because I wanted to report the
issue date of the log book via the car.
This is also a good example of how the UML can help you understand quite a complex code structure.
Consider redrawing the model with the extra operations and attributes added and then review the design
visually.
I have used the Registration Plate of the Car as the qualifier from Person to Car. This makes sense
because the Registration Plate will always identify a single Car.
I have chosen in this example to continue to give the LogBook class the link management
responsibilities. In practice this responsibility will vary depending on your model. The objective is
usually to produce a simpler model that other developers can pick up easier later.
It allows you to vary the way you store the reference to the associated object. This hidden reference may
change over the lifetime of the system.
It allows you to provide an association that is created "on-demand", to improve performance. For
instance, you might have an association to a large collection of objects. By hiding the association behind
a property procedure, you could build the collection when the property is first called (which of course
may never happen for the object's lifetime).
It allows you to provide a "Derived" association. Because your implementation of the association is
hidden, it may never actually take up any fixed storage at all. In other words, you have an algorithm
behind the property procedure that calculates the associated object by traversing many other objects.
520
UML to VB Mapping
For example, given two classes, Person and Car , Person has a reference to Car, and Car has a
reference to Person . If you introduce such a cycle, the VB run-times are often unable to clean up the
objects, even after you released the objects:
How should you avoid this? There are a number of solutions to this. The first suggestion is to allow cyclic
references but make sure you explicitly break the cycles when releasing a model. The technique is to add
explicit Destroy() routines to classes which are called when the object is ready to be released.
m_Car
Russell: Ferrari:Car
Person m_Pers
'Car Class
Private m_Engine As Engine
Private m_Alarm As Alarm
'other operations.......
So, given the Person-Owns-Car example, we might choose to hold a reference from the Person to the
Car, but on the Car we place an ID that uniquely identifies the Person. To support this implementation,
we will need to introduce a 'registry object' that keeps a track of Persons and their associated ID. When
the Car wants to traverse to its owner, it now consults the registry.
Both techniques have their merits and different performance tradeoffs. It is a judgment call as to which
technique is better for a particular situation.
521
VB6 UML
'add a car
Public Function Add(ByVal aCar As Car) As Car
mCol.Add aCar, aCar.Registration
mCol.Remove vntIndexKey
End Sub
522
UML to VB Mapping
Actor
<name>
By Convention only.
Class
(See below.)
Use Case
Use case diagrams are basically an analysis tool - they are created to help all parties involved in a
development understand what is being delivered rather than how it is to be built. Therefore it is not usual to
map the artifacts from these diagrams to actual source code.
523
VB6 UML
This is wrong because the use cases are there to help the potential users of the system understand what the
system will do for them – not how the system should be written.
The useful thing about use cases is that they clearly define the boundaries of candidate transactions, (where
a transaction is defined as a distinct, logical unit of work that our system might perform). 'Create Order' is a
good example of a transaction. In other words, the use case 'Create Order' must run to completion or not at
all. If we 'half' created an Order, that simply wouldn't do and could leave our system is a nasty state.
Therefore it would be useful if we could help ensure that transactions are cleanly defined and applied in our
system.
(A <<controller>> class stereotype is a class that is mainly responsible for controlling and coordinating
the implementation of a use case but doesn't actually perform any of the use case-specific detail, itself.)
Let's start by looking at a use case model of the example use case:
Transfer
Funds
Bank
Customer
The use case will be performed by the following classes. Note the presence of the use case controller:
«controller» 1 2
TransferFunds ::Account
524
UML to VB Mapping
This is a high-level model. The two accounts will be the source and target accounts.
As previously stated, the controller is just acting as a coordinator but also knows about the whole
transaction. Therefore the lifetime of the controller is synonymous to the lifetime of the transaction and use
case. When we discuss collaboration diagrams, we will show how the controller performs the use case.
The UML offers four different diagrams for capturing your design dynamics and these are detailed in two
parts: The first part looks at how we map sequence and collaboration diagrams. They are focused on object
instances and the messages and events that travel between them.
In the second part we look at statechart and activity diagrams . These are closely tied in with Collaboration
Diagrams but are focused more on states that an item such as an object can be in and how external and
internal events change that state .
525
VB6 UML
The first thing to note about sequence diagrams (and collaboration diagrams, for that matter) is that they are
usually drawn with a context in mind. The first time we use these diagrams is when we are designing how
our use cases will be handled. In this case the context is just the system as a whole. Later in the design we
will use sequence diagrams to understand how a particular object method behaves. In this case the context
is the method.
Here is an example of a sequence diagram scoped to a method together with the resultant code:
'Withdraw funds
theSource.WithdrawFunds theFunds
'Deposit Funds
theTarget.DepositFunds theFunds
'commit
theTransManager.Commit
Exit Sub
TransferError:
526
UML to VB Mapping
'Abort Transaction
theTransManager.Abort
End Sub
Notice that we have slightly changed the logic so that we can make use of VB's built in error system. This
is OK as long as we don't impact the core interaction design.
7.2 Interactions
The horizontal interactions connecting the object instances tell us what operations on the objects need to be
called (they also tell us what extra operations we need to add to our static model). The order of these
interactions is crucial– this is the main point behind ( message) sequence diagrams. Each horizontal line
represents a single message and this represents an actual operation call. So we need to map this call into an
actual VB operation call. The next question is 'which VB method makes the call?'
By studying the diagram it is usually easy to see which previous operation is making the call.
7.3 Pseudo-Code
The left-hand side of a sequence diagram usually contains pseudo-code that provides more clues about how
the operations used on the diagram are actually behaving. This code is usually stated in three basic control
constructs: sequence, selection and iteration. The Developer can use these constructs to write the skeleton
of the related method's logic. So the mapping here is quite straightforward. Once the Diagram has matured
somewhat, the Developer will use the pseudo-code as the basis for the body of the related VB method.
Collaboration diagrams do however allow us to show which associations between two objects are being
used to perform the interaction. This will be reflected in the names of the objects being manipulated in the
procedure, i.e. if the objects have a 'm_' naming prefix, then it implies that the objects are defined at class
scope – not the procedure scope.
527
VB6 UML
Depends on
state Abstract – see notes
implementation
Transition Depends on
Abstract – see notes
implementation
State modeling is one of the more specialized areas of the UML. Although all objects in your system have
state, many applications such as client-server applications don't contain objects with very much state so the
need to model state is less important.
If you are using VB in a real-time systems environment, then understanding how to map statecharts to VB
will be very useful to you.
As was suggested in the introduction, statecharts have no straight mapping to VB. The main use behind
statecharts is to understand clearly how objects with complex state behave. Looking at the source code of a
complex object isn't very intuitive. So, once you've created your statechart and you are happy with it, what
comes next?
528
UML to VB Mapping
Alternatively, if the object in question is going to have a particularly complex state, then it is sometimes
worthwhile implementing an explicit 'Finite State Machine' inside the object. What we do here is create a
machine that is driven from a table loaded with information about what to do for a particular combination
of state and event. It is also possible with this technique to change this data at run-time thus altering the
dynamic behavior of the object. (This technique is akin to data-driven programming, where the data is
partially controlling the program flow.)
8.3 An Example
As always, the best way to understand the difference in implementation is to see some examples.
Let's start by defining a simple statechart for a car's security system. Here is the Car's static class model,
showing the objects involved in the state machine activities:
::RemoteKey
1
PressButton
TuneToCar( in aCar : Car )
1 CarLock
::Car
1 1 ::CentralLocking
- Locked : Boolean
- AlarmOn : Boolean myCentralLocking ToggleLock
+ StartEngine
+ StopEngine
::Alarm
- SoundAlarm 1 1
+ ResetAlarm TurnOn
myAlarm
+ <<get>> CentralLocking TurnOff
Now, the part we are interested in is the state of the Car object as shown:
Initial
Create
LockSignal / LockCar
Unlocked Locked
UnlockSignal / UnlockCar
StartEngineSignal / SoundAlarm
529
VB6 UML
'initialise self
Private Sub Class_Initialize()
'create my composite objects
Set m_myAlarm = New Alarm
Set m_myCentralLocking = New CentralLocking
530
UML to VB Mapping
Next, let's look at the code for the Alarm (quite simple):
RaiseEvent Unlocked
Else
'unlocked - now lock
Debug.Print "Click"
m_Locked = True 'set unlocked state
RaiseEvent Locked
End If
End Sub
531
VB6 UML
Considering the complexity of the statechart (low!) the example is quite easy to follow. If the Car had
more states and events (which it could be if we considered the whole Car), then an explicit version might
be preferable.
532
UML to VB Mapping
'initialise self
Private Sub Class_Initialize()
'create my composite objects
Set m_myAlarm = New Alarm
Set m_myCentralLocking = New CentralLocking
Set m_MyFSM = New CarFSM
m_MyFSM.LinkCar Me
'actions -----------------------------------------------------------
533
VB6 UML
The CentralLocking, Alarm, and RemoteKey classes are the same but we have now introduced a
separate state machine that will drive the Car object:
m_myActionTable(Unlocked, EngineStarted).ActionsToPerform(0) = _
InternalStartEngine
m_myActionTable(Unlocked, EngineStarted).ActionsToPerform(1) = NoAction
m_myActionTable(Unlocked, EngineStarted).NextState = Unlocked
534
UML to VB Mapping
535
VB6 UML
The state/event/action information has been stored in a two-dimensional table. To find out what actions
need to be performed on a given event, the state machine just indexes into this table using the current state
and event just received.
Each entry in the table contains a list of actions to perform in sequence, terminated by the special action
"NoAction". Each entry also contains "Next State", which is the state that the Car to enter after performing
the actions.
Note that the finite state machine (FSM) is part of the Car 's implementation. The client code has remained
the same in both the examples. Clients shouldn't be aware of how you have implemented the state of the
object. (This is the power of object encapsulation.)
In the example it was decided that the state machine should be a separate abstraction. By doing this we have
made the Car class simpler to understand. However this decision has introduced some compromises. For
instance, the actions that Car can perform can no longer be private as the state machine ( CarFSM class)
needs to call these procedures. These procedures now have 'Friend' access to at least ensure that they are
not visible outside of the component - only inside.
In VB terms, a single activity diagram usually maps to a single VB method. You should use activity
diagrams only when dealing with complex operations that are easier to understand visually than in code.
The interesting items on activity diagrams are action and transitions . An action usually maps to either a
single VB statement or a cohesive group of statements. We can see how this relates to our previous
example. The procedure call to SoundAlarm can be treated as a single action in an activity diagram.
Transitions are simply the invisible transition from one VB statement to the next so they don't have any
concrete mapping to deal with.
The text associated with an action , can also be conveniently mapped to a comment near to the statements,
particularly when the statements themselves aren't self-explanatory.
536
UML to VB Mapping
For completeness, here is an example activity diagram. This is the PerformUseCase operation on the
<<controller>> class TransferFunds we looked at earlier:
Begin
Begin Transaction
Withdraw Funds
Deposit Funds
End
Note that the code mapped from this is identical to that produced from the sequence diagram for the same
operation, so we won't repeat it.
Interface
'PublicNotCreatable' Class
537
VB6 UML
Still, when developers talk about components, we are usually referring to either physical components, i.e.
Active X components, DLLs, etc. or logical components, i.e. Business Objects, such as Account, Sale,
etc., contained and exposed from these physical components .
Logical components expose services that applications can use, e.g. Account exposes the service
Withdraw Cash.
Physical components, on the other hand, are the packaging and deployment of these logical components
and their partitioning is influenced by the technical requirements of the target system.
IDocument
IPersistent
IPrintable Document
538
UML to VB Mapping
For mapping purposes, it is probably easier to look at an alternative but more familiar representation:
::Word Processing
«interface» Implements
IDocment
«interface» Implements
IPersistent
«interface» Implements
IPrintable
Document
Components also have a private implementation, which typically involves one or more private classes that
carry out the work for the component. The client code using the component via interfaces is completely
unaware of how the services requested are being handled internally. This is the key strength of component
development.
Any VB class in a Project that has an instancing property that is not private can be treated as a logical
component. The classes in the project that have an instancing property of 'Private' are usually there to
provide the implementation of the exposed components.
If we consider the Car example introduced in the Statechart mapping, the Car could be exposed as a
logical component and the CarFSM would be part of the private implementation.
::<<ActiveX EXE>>Word
IApplication
Application Indexer
IDocument
IParagraph
Paragraph
The UML can show a component offering many interfaces to its prospective users. Here is an example
539
VB6 UML
physical component together with exposed logical components and two hidden implementation classes:
However VB Developers should note that it isn't really the UML component that is offering interface -
rather the classes within the component (project) that are marked as public. This is something that the UML
is not clear about. It is likely that the distinction will be clarified in later drafts.
10.4 Stereotypes
The UML comes with a standard set of stereotypes for components:
An <<executable>> stereotype of component simply states that the item represents an executable file, such
as 'program.exe', and can be individually deployed onto a node on a network.
A <<file>> component stereotype is nothing more than a physical file. However, this notation gives us the
opportunity to model files and more importantly, show where files live on a network of nodes in the system
we are building.
Most CASE tools extend the range of stereotypes available for components to cover those supported by
target programming languages. Visual Basic, for instance, is capable of creating the following component
stereotypes:
Stereotype Meaning
<<ActiveX DLL>> An ActiveX server packaged as a dynamic link library.
<<ActiveX EXE>> An ActiveX server packaged as an executable program.
<<EXE>> A regular Win32 executable (without COM).
<<DLL> A regular Win32 dynamic link library.
540
Adding Keys to the Registry
We saw, in Chapter 10, that we could write some VBScript that allowed us to register objects. Let's take a
look at how to do this manually.
To add the key to the registry, we must first use Regsvr32, and then RegEdit. Go to the Windows Start
menu, select Run and type Regsvr32 path\nameofmy.dll as follows:
If you've built your Visual Basic DLL on the server and compiled it, the DLL will already be registered and
you can skip the step above.
I would recommend that you have a machine on which you do the bulk of your designing and
building that is separate from the clients and servers which your application will ultimately
be deployed on.
Next, you must manually add a registry entry using RegEdit.exe. On the Windows Start menu, click
Run. Type RegEdit into the Run box, and then click OK. Find the following key in the registry:
VB6 UML
HKEY_LOCAL_MACHINE
\SYSTEM
\CurrentControlSet
\Services
\W3SVC
\Parameters
\ADCLaunch
To find this key, click the HKEY_LOCAL_MACHINE folder, then SYSTEM, then CurrentControlSet,
then Services. This should look as follows:
Scroll down through the services until you get to the W3SVC folder and click on it; then click on the
Parameters folder, and finally click on ADCLaunch.
If ADCLaunch is missing from your system, you probably haven't installed Remote
Data Services (RDS) on to your computer.
When you add a new key, name it with the programmatic ID of the server object you created. The format of
the key will be ProjectName.ClassName. For example, if you named your project prjServerTest
and the class clsServerTest the programmatic ID would look as follows:
prjServerTest.clsServerTest
544
Adding Keys to the Registry
Now right mouse click on ADCLaunch, select Add New Key from the drop-down list and type in the
programmatic ID:
Once you've done this, you can now access this object with RDS through HTTP. As only the name of the
project and class are used, a change to the GUID will not affect this registry entry.
545
Using MTS
We covered the basic theory of MTS in Chapter 11, but I didn't have time there to explain how you use it.
In this Appendix, I will give you a brief run through of the steps involved in placing our DLLs in MTS.
A package is one or more components that have been grouped together so that they
can be easily set up and managed as a collective group.
With packages, it's a lot easier to distribute, administer, and use multiple components. We may have a
project with multiple classes that will need MTS services. Instead of working with each one on an
individual basis, we can create packages that can be distributed to other MTS servers or clients through a
single install process.
A package can be configured to host objects in their own process space when set and run as a server
package, or to create components in the process space of the calling base client when configured as a
library package.
VB6 UML
In this section, we'll see how to get our objects running under MTS on a local machine. Once we've done
this, we'll see in the next section how we can export our objects so they can be used from a remote client.
Open the Transaction Server Explorer and browse to the Packages Installed node:
We need to add a new package, so choose the Action | New | Package option from the toolbar.
MTS will bring up a wizard to guide us through the process of adding a package. The first page asks
whether we're creating a new empty package or importing an existing one. Since we haven't placed our DLL
into MTS we need to Create an empty package:
548
Using MTS
Next, we need to supply a name for our new package. Let's call our package prjServer, jut like the DLL:
549
VB6 UML
Finally, we need to provide MTS with information about the user under which the component will be
running (this page doesn't appear under the 9x version). We can either supply a specific user account or use
the currently logged-on user. Typically, you'd want to use a specific account but for simplicity we'll just use
the Administrator account:
Now when we click Finish, MTS will add our new packages to the list of installed packages:
550
Using MTS
So far, we've only created an empty package. We also need to get our objects from our DLL into the
package. This is simpler than even creating the package. It's merely a matter of dragging and dropping.
Expand our package until you can see the empty Components folder:
Then, using Windows NT Explorer, simply drag and drop the prjServer.dll file into the empty
Components folder. The result will be a listing of all the classes contained in the DLL shown as
components of this package:
551
VB6 UML
If you then right-click on a component and select Properties you will bring up the properties windows for
that component. If you then go to the Transaction tab you will see the setting that we specified in the class
Properties window:
The actual terminology doesn't exactly match those in the Properties window but the
mapping is clear enough.
Exporting a Package
Running a component in MTS on a local machine is all very well, but what MTS is really designed for is
scaling components to be used by multiple clients. We therefore need an easy method of exporting a
package to its clients. Using the MTS Explorer this is easily achieved.
552
Using MTS
Select the package that you want to create a client set up for (in our case this is the prjServer package) in
the MTS Explorer:
Use the Action | Export option on the tool bar. This will bring up a dialog asking where we want to create
the .PAK file. The PAK file can be used to export out a package to other installations of MTS:
553
VB6 UML
Click the Export button and MTS will create the PAK file at the location we specified. MTS will also
create a Clients subdirectory beneath this, where you will find the client install program:
The Windows 9x version cannot generate executables and so you will only be able to create
the PAK file.
The client install program has everything it needs to install and register your components on a client. All
you need to do is run the executable on the client.
Service Pack 4 for Windows NT 4.0 on both the server and the client machine. You can get Service Pack
4 from the Microsoft site. It may take a few hours to download unless you have a T1 connection, but it is
the only way to make it work.
Debugging only works for a single-client accessing one copy of the MTS server component at a time.
Debugging requires that the DLL is hosted in a Library Package:
554
Using MTS
Now if you run the DLL project, you may see the following exciting message box:
You will get this even with NT Service Pack 4 and the correct version of MTS. This is a bug (documented
in the Microsoft Knowledge Base). If you get this message, you need to install Service Pack 1 or later for
Visual Studio. This will get rid of the message box, but you still will not get the object context when
debugging.
Setting Up a Database
Because debugging any component is important, I would make the following recommendation.
In order to keep track of the errors, make a database called Debug with one table called DebugTable. Do
not allow Access to make a primary key for the table. Make one table with an auto-incrementing field and a
field called DebugValue:
555
VB6 UML
Option Explicit
The # creates a conditional statement. I will show you how to turn it on in a moment.
Add the following method that will open our debugging database and write string information to it:
With m_objDebugADOConnection
.CursorLocation = adUseClient
.ConnectionString=" "Provider=Microsoft.Jet.OLEDB.3.51;" _
"Persist Security Info=False;Data Source=C:\Wrox\UML\Debug.mdb"
.Open
Set recRecordset = New ADODB.Recordset
recRecordset.CursorLocation = adUseClient
recRecordset.Open "Select * From DebugTable", _
556
Using MTS
CloseADOConnection
Set recRecordset = Nothing
End Sub
#End If
You will need to set the ConnectionString to wherever you saved your Debug database.
SetComplete
CloseADOConnection
Set recCustomers = Nothing
Exit Function
ReturnCustomerRecordSetError:
CloseADOConnection
SetAbort
557
VB6 UML
End Function
Switching Debugging On
However, in order for the conditional statements to work we need to set the debugging function to –1,
otherwise Visual Basic will simply ignore everything enclosed in the conditional compile # statements.
To set the debugging function, go to Project Properties dialog and then select the Make tab. Under the
Conditional Compilation Arguments section add debugging = -1:
This now turns on all of the sections inside the conditional arguments. To turn off the conditional
components, we could set debugging = 0.
Is this an ugly hack? Yes, but it does allow you to get information from your DLL while it is running under
MTS. The other solution is to debug using the C++ debugger, but I will leave it up to the hardcore VB
programmers to explore that route. Using the C++ debugger will provide the same information found in the
Visual Basic debugger, plus additional low-level information Visual Basic Programmers do not usually see.
558
Fine Tuning Visual Basic
Components Running Under MTS
We learned, in this book, that there are some special rules that have to be taken into consideration when
we're working with MTS. Our MTS Visual Basic components will need to call either SetComplete or
SetAbort, depending on whether the component has successfully completed its task, of if the component
has failed its task. The last SetComplete or SetAbort called will determine the outcome. Are there any
other special considerations? Yes, there are – and we will look at these in this Appendix.
In this case, we must keep a reference to the Connection object from the beginning to the end, as our
transaction is based on this particular Connection object. Thus, programming with continuous connections
requires us to hold on to our Connection object. Every time we make a connection to the database, the ADO
must initialize a new Connection object and communicate to the database. This takes time, so it is not
uncommon in this type of programming to have a global Connection object that is connected to the database
for long periods of time.
By programming under MTS, we now have a completely different situation, because our Connection objects
are pooled, and our transaction is running under MTS. When our components are built to run under
transactions, MTS will manage the transaction. Therefore, we will not use the ADO Connection object for
managing transactions for components set to use transactions running under MTS. This means that we can
perform all of the following steps within a single transaction, even though we're closing the connection
object between steps in this transaction:
Even though we haven't kept the Connection object open throughout all of these steps, MTS will still
manage this as one complete transaction. If we call Get all the way to the Product update, and then call
SetAbort before leaving the component, all the changes made to Order and Order Details will still be
rolled back. This will happen even though you do not still have a connection to the database with these
recordsets.
Some of you may be wondering: Why we would go through all of this work opening and closing
connections? After all, database connections are pooled when running under MTS. Well, there are only so
many connections lying around. If our application is required to scale to be able to service tens of thousand
of possible clients, we will have the potential to use up all of our Connection objects. If we only use a
Connection object for the split second that we need it, and then get rid of it while we're doing other work,
we will allow other components to use this Connection object.
As for that other question some of you may be thinking: no, it does not take time to keep reopening the
Connection object. The Connection objects are not only pooled, but they are kept hanging around for a
while in case they are needed again. As long as we are using the same data provider and the same data
source, MTS will not need to recreate the Connection object. Once it is created, as long as you keep using
it, it will be there waiting for us when we need this. Therefore, there is no extra cost for opening an closing
the Connection object. The only cost is when we first create the Connection object, and from that point on
we will have a connection to the database waiting for us.
To make our code work both ways (with and without MTS) we would probably write a routine as follows…
562
Fine Tuning Visual Basic Components Running Under MTS
OpenADOUnderMTS
‘Do some work with the database
CloseADOUnderMTS
OpenADOUnderMTS
‘Do some work with the database
CloseADOUnderMTS
m_objContext.SetComplete
Because we only want to open the object connection right before we use it, we will not open the connection
when the Activate event is raised. Instead, we will wait until we need the connection.
563
VB6 UML
The transaction statistics will show how many transactions are currently running, how many have been
committed, aborted, etc. If we stop our server component in the middle of a transaction, we can actually see
the transaction in the statistics. I have stopped our server component in the middle of a transaction, and my
statistics now looks as follows:
564
Fine Tuning Visual Basic Components Running Under MTS
This shows that there is one active transaction, as I have stopped the server component in the middle of
SetADOConnection.
A few seconds later, the transaction timed out, so it aborted; my statistics now look as follows:
If we right mouse click on My Computer, select Properties, we can set the time out value:
565
VB6 UML
We can increase this value for debugging, but we should remember to put it back when we're done.
Using these statistics, we can watch how our components are performing, and see if everything is working
as we'd expected.
566
Index
570
Index
571
VB 6 UML
572
Index
573
VB 6 UML
574
Index
UsesTransactions, 285
575
VB 6 UML
576
Index
adding an order
S products update, 324 Just In Time (JIT)
security, 283 ADO Connection Object activation, 281
RDS, 194 closing, 294 object life times, 281
sequence diagrams, 93 retrieving, 294 object manager, as, 298
see also Appendix A creating objects, non-
actor, 99 improved coding of, 292 transactional, 283
alternative flow, 108 test project, improved, objects, transaction
benefits of, 118 292 handling, 285
branches, 104, 108 test project, revisited, 292 resource pooling, 281
building, 96, 98 building security, 283
compared to collaboration ADO Connection Object, stateless programming
diagrams, 94, 125 working with, 292 model, 283
decisions, making, 104 declarations, adding, 287 transaction ending,
defined, 16 ObjectControl Interface, importance of, 282
detail, amount of, 121 coding, 290 transaction management,
ending point, 109 project, starting, 286 281
flow of events, 100 starting the project, 286 transaction, defined, 280
alternative flow, 108 database, updating, 306 Visual Basic, and, 284
focus of control, 97, 100 update method, generic, object creation
interface, 99 307 running without MTS, 289
introduction, 95 UpdateRecordset, ObjectControl Interface,
lifetime, 96 parameters, 308 coding, 290
messages declarations, adding, 287 objects, transaction
adding, 100-106 constants, 288 handling, 285
reflexive, 97 variables, 288 Password, validating, 295-
to itself, 107 Get Recordset functions, 297
Modify Customer diagram, other, 305 placing, 200
109 GetRecordset, 300 considerations, 200
Modify Product diagram, GetRecordset, generic disadvantages, 200
113, 115, 116 function, 300 product recordset,
objects GetRecordset, methods, 300 retrieving, 302
adding, 98 logging in, 295, 297 ADO Connection Object,
context, 526 caution, 296 303
instances, 526 MTS error handler, 303
interactions, 527 As Soon As Possible error trap, 304
patterns, 118 (ASAP) deactivation, parameters, 302
pseudo-code, 527 281 project, starting, 286
reflexive messages, 97 components, scaling, 281 recordset retrieving, 299
starting point, 99 context object, 281 running without MTS, 289
symbols, 96, 97 context object, methods, starting the project, 286
timed events, and, 98 291 stateless programming
timeline, 99 Context Wrapper, 281 model, 283
uses of, 118 database transactions, test project
server component, final 282 see server component test
project Distributed Transaction project, 220
see also server component, Coordinator (DTC), transaction ending
test project 282 importance of, 282
introduction, 280 Visual Basic
577
VB 6 UML
server component design, 219 project creation, 236 counting second intervals
server component test recordset retrieval, 241, 244 for client component test
project registering, 257 project, 269
see also server component, server object
final project compiling, 256
compiling, 256 registering, 257
T
connection object server time function, 255 tasks, 166
creating, 238 tasks to be tested, 220 technology models, 36, 37
connection object, creating testing disconnected templates, 499
ADO Connection Cursor recordsets, 231 test project, 220
Location, 239 server object, referencing client component
ADO Connection Object, for client component test preparing to run the test,
239 project, 265 268
ADO Connection.String, server records, requesting project creation, 260
239 for client component test project, running, 274
error collection, 239 project, 271 RDS, benefits over
error handler, 239 server time function DCOM, 267
connection to database, for server component test RDS, determining
creating, 236 project, 255 efficiency of, 274
creating the project, 236 single user interface RDS, setting up, 265
database recordset, updating benefits for RDS, testing, 267
ADO connection, 248 Internet/intranet, 173 server object, referencing,
conflicting records, filter, Internet/intranet benefits, 265
249 173 server records,
error handler, 248 specifications, documentation requesting, 271
error raising, 250 benefits of, 39 start at exact time, 270
pending records, filter, project direction, 39 starting test, 267
248 review process, 39 synchronizing client time,
product, 248 time and money, 39 268, 269
products recordset standards, 26, 27 test form, adding code,
connection, 248 coding standards, 27 264
recordset, return, 250 documentation standards, 27 test form, creating, 263
database recordsets, repository, 27 test form, remaining code,
updating, 246 UML tools, 27 273
determining methods, 220 statechart diagrams test, stopping, 273
product inconsistencies, see Appendix A user verification, 261
reconciling, 251 stateless transactions importance of, 35, 237
conflicting records, check RDS, 195 project design
for, 254 static members, 498 server component design,
error trap, 254 static structure 219
first record, moving, 252 see Appendix A server component, 220
original value, 253 stereotypes, 491, 540 compiling, 256
products recordset, synchronizing client time connection object,
filtering conflicting count off minute intervals creating, 238
records, 254 for client component test database recordset,
products recordset, project, 268, 269 updating, 246
update batch, 254 counting minute intervals database, connecting to,
resynchronizing, 251 for client component test 236
578
Index
579
VB 6 UML
580
Index
581