Copyright (c) 1999 - 2000 by
John Kaster
First published February 11, 1999. Revised December 11, 2000 for Delphi 5 (instead of Delphi 4)
Source Code
Introduction
One of the features that distinguishes MIDAS (Multi-Tier Distributed Application
Services) from other distributed database solutions is its support for
dynamic data constraints. These dynamic constraints control the visibility,
appearance, and validation rules of the data distributed to the thin-client
application from the middle tier server. I often mention this feature as
an important part of distributed database functionality, but the concept
is often difficult to conveniently demonstrate in the 15 or so minutes
I usually have to show the features of MIDAS to developers. (There's a
lot to show!)
An Overview of MIDAS Data Packets
Before I talk about a specific feature of the MIDAS data packet format,
an overview would be a good idea. The MIDAS data packet is a streamable
binary format that is language-independent and transport-neutral. It contains
metadata, constraints, data, and deltas.
The metadata is the description of the datasets being distributed by
the middle-tier server. MIDAS 2 supports nested datasets, so a single data
packet may contain information about more than one dataset. The constraints
are the validation and display rules for specific fields in the dataset.
The data is the actual rows and columns of the dataset. The deltas record
the changes to the data on the thin-client application after the data has
been retrieved from the server. Deltas are automatically maintained by
the thin-client and are transmitted to the middle-tier server for resolution
with the database server.
I'll be writing more about other aspects of the MIDAS data packet in
other articles. For now, we'll just drill down a bit into the data constraints.
MIDAS Data Field Constraints
Data field constraint propagation from the middle-tier server to the thin-client
application increases the lifetime of the distributed application, speeds
changes to business logic, reduces the need for deploying a new client,
and increases maintainability by providing server-based business rules
that will be automatically enforced on the client. If this wasn't enough,
enforcing the server-based data constraints on the client side increases
the reliability and data entry accuracy for the application because the
invalid data is detected immediately by the client application, educating
the user about proper data values and saving network roundtrips for bad
data. In short, data constraints make your distributed application faster,
responsive to changes, and more reliable.
MIDAS 2 Data packets can automatically propagate the following data
field properties from the middle-tier server to the client:
| Constraint Property | Description |
| ConstraintErrorMessage | Message to display if data for the field if not valid.
Example: CustNo cannot be blank |
| CustomConstraint | Constraints to place on the validation of the
field. Expressions can be used for this constraint.
Example: CustNo IS NOT NULL |
| DisplayFormat | Controls formatting of the field for display |
| DisplayLabel | Used as the data field's default column heading.
Example: Customer # |
| EditMask | Used for customizing data entry for the field.
Example: 0000 |
| MaxValue | The maximum value of a numeric field |
| MinValue | The minimum value of a numeric field |
| Visible | A boolean value indicating whether the field
should be visible in a grid by default |
Good Help is Good to Find
At BorCon 98 in Denver, Colorado, it occurred to me that a run-time constraint
editor was the best way to demonstrate the power of MIDAS' dynamic constraints.
I caught Louis Kleinman and Dan Miser in the exhibit hall and explained
the idea to them. A MIDAS application server and thin client that would
visually demonstrate how the MIDAS data packet implements and propagates
data constraints was just the ticket.
We were working at one of the dining tables next to the exhibit hall.
I had to go attend a session, so I left them my notebook to work on. When
I returned, they had finished most of what will be presented in this article,
probably much faster than I would have.
(This brings up a point for you to consider if you're waffling on attending
this year's BorCon in Long Beach, California, USA:
BorCon is loaded with attendees who like to get things done! You could well find
the solution to a nagging technical problem just by mentioning it
to a stranger at lunch while attending the conference.)
Implementing the Constraint Editor Server
Whenever a client connects to the MIDAS server, a remote data module is
automatically created to provide access for that client to the server.
The constraint editor server (ConstraintSvr.exe) uses the first of these
remote data modules to retrieve the list of providers and field constraints
contained in these providers. ConstraintSvr also provides a user interface
for editing the constraints for a specific field in a specific provider,
and saving those changes for that instance of the application.
After the changes are saved (at run-time), the client application simply
needs to re-connect to the server, which will send a new data packet to
the client. This new data packet will contain the new constraints, along
with the other information it sends.
First, let's implement the server and discuss some of the code you probably
just downloaded from the link at the top of this article. I'm not going to discuss
how to create the remote data
module, because there's nothing different there. If you want to see step-by-step
instructions on creating a MIDAS server, see my white paper "Fun
With Delphi 4"
A Cast of Variants
To build an application that can provide run-time editing of data constraints,
a little bit of custom code is required. The Screen object has a property
that lists all data modules instantiated for an application, so we can
examine this property to build the list of providers contained in the data
modules. This list of provider names is a variant type exported by the
remote data module, so we just need to cast it back to the appropriate
Delphi type for retrieving the named providers of the data module. Here's
the code:
{ Copyright (c) 1999 - 2000 by John Kaster, Dan Miser and Louis Kleiman }
procedure GetProviderList( ProviderList : TStrings );
var
ProviderNames : OLEVariant;
I : Integer;
DataSet : TDataSet;
GenObject : TComponent;
begin
if (Screen.DataModuleCount = 0) then
Raise Exception.Create( 'No data modules are active.' );
ProviderList.Clear;
{ Get the list of all providers for the first data module.
All instances of the data module are probably the same. }
ProviderNames := IConstraints(TConstraints(Screen.DataModules[0])).
AS_GetProviderNames;
for I := VarArrayLowBound(ProviderNames, 1) to
VarArrayHighBound(ProviderNames, 1) do
begin
{ Retrieve the component matching the provider name }
GenObject := Screen.DataModules[0].FindComponent(ProviderNames[I]);
if (GenObject is TDataSetProvider) then
DataSet := TDataSetProvider(Screen.DataModules[0].
FindComponent(ProviderNames[I])).DataSet
else if (GenObject is TDBDataSet) then
DataSet := TDataSet(Screen.DataModules[0].
FindComponent(ProviderNames[I]))
else
DataSet := nil;
{ Add it to the list of providers, attaching the Dataset if assigned }
ProviderList.AddObject(ProviderNames[I], DataSet);
end; { for }
end; { GetProviderList() }
Retrieving Constraint Values
Now that we can get the provider list from an instantiated remote data
module, the rest is mainly conventional Delphi code. We need a list box
for the list of providers, and a list box for the field constraints contained
in each provider. The list box for the providers is called lbProviders.
As the user selects a provider by clicking on one with the mouse, we want
to update the list of fields for that provider. Here's the code for that
event:
procedure TFormConstraintsEditor.lbProvidersClick(Sender: TObject);
var
DataSet : TDataSet;
I : Integer;
SaveActive : Boolean;
begin
lbFields.Clear;
DataSet := TDataSet(lbProviders.Items.Objects[lbProviders.ItemIndex]);
if Assigned(DataSet) then
begin
SaveActive := DataSet.Active;
DataSet.Open;
try
for I := 0 to DataSet.FieldCount - 1 do
lbFields.Items.AddObject(DataSet.Fields[I].FieldName,
DataSet.Fields[I]);
finally
DataSet.Active := SaveActive;
end; { try...finally }
end; { if }
end;
Assigning Editable Constraint Values
Then, we need edit controls that can be assigned the values of a chosen
field. After creating these controls on the form, the following click event
is used to assign the values for the edit controls from the field the user
selects by clicking the mouse. The list box for the fields is called lbFields.
Although this code is somewhat lengthy, it's primarily testing for different
field types and setting the editable values accordingly.
procedure TFormConstraintsEditor.lbFieldsClick(Sender: TObject);
var
Field : TField;
begin
Field := TField(lbFields.Items.Objects[lbFields.ItemIndex]);
if Assigned(Field) then
begin
edErrorMessage.Text := Field.ConstraintErrorMessage;
edCustomConstraint.Text := Field.CustomConstraint;
edDisplayLabel.Text := Field.DisplayLabel;
edEditMask.Text := Field.EditMask;
edDisplayFormat.Text := '';
edMinValue.Text := '';
edMaxValue.Text := '';
cbVisible.Checked := Field.Visible;
if Field is TNumericField then
begin
with Field as TNumericField do
edDisplayFormat.Text := DisplayFormat;
if Field is TFloatField then
with Field as TFloatField do
begin
edMinValue.Text := FloatToStr( MinValue );
edMaxValue.Text := FloatToStr( MaxValue );
end
else if Field is TBCDField then
with Field as TBCDField do
begin
edMinValue.Text := FloatToStr( MinValue );
edMaxValue.Text := FloatToStr( MaxValue );
end
else if Field is TIntegerField then
with Field as TIntegerField do
begin
edMinValue.Text := IntToStr( MinValue );
edMaxValue.Text := IntToStr( MaxValue );
end
else if Field is TLargeIntField then
with Field as TLargeIntField do
begin
edMinValue.Text := IntToStr( MinValue );
edMaxValue.Text := IntToStr( MaxValue );
end;
end
else if Field is TDateTimeField then
with Field as TDateTimeField do
edDisplayFormat.Text := DisplayFormat;
end; { if }
end;
Applying Edited Constraints
After the appropriate values are assigned for the data field constraints,
the apply action will assign the textual representations of the constraints
to their appropriate field values, depending on the type of field. It's
basically the reverse of the process that assigns the editable values to
the edit fields.
procedure TFormConstraintsEditor.actApplyExecute(Sender: TObject);
var
Field : TField;
begin
Field := TField(lbFields.Items.Objects[lbFields.ItemIndex]);
if Assigned(Field) then
begin
Field.ConstraintErrorMessage := edErrorMessage.Text;
Field.CustomConstraint := edCustomConstraint.Text;
Field.DisplayLabel := edDisplayLabel.Text;
Field.EditMask := edEditMask.Text;
Field.Visible := cbVisible.Checked;
if Field is TNumericField then
begin
with Field as TNumericField do
DisplayFormat := edDisplayFormat.Text;
if Field is TFloatField then
with Field as TFloatField do
begin
MinValue := StrToInt( edMinValue.Text );
MaxValue := StrToInt( edMaxValue.Text );
end
else if Field is TBCDField then
with Field as TBCDField do
begin
MinValue := StrToInt( edMinValue.Text );
MaxValue := StrToInt( edMaxValue.Text );
end
else if Field is TIntegerField then
with Field as TIntegerField do
begin
MinValue := StrToInt( edMinValue.Text );
MaxValue := StrToInt( edMaxValue.Text );
end
else if Field is TLargeIntField then
with Field as TLargeIntField do
begin
MinValue := StrToInt( edMinValue.Text );
MaxValue := StrToInt( edMaxValue.Text );
end;
end
else if Field is TDateTimeField then
with Field as TDateTimeField do
DisplayFormat := edDisplayFormat.Text;
end; { if }
end;
We are done with building the ConstraintSvr (some minor implementation
details were skipped, but they're in the code download). All we need to
do is compile and run it so that it registers itself, and we're ready to
implement the client.
Building the Constraint Client
The client is much easier to build than the server, because it simply retrieves
the modified data packet, and has no knowledge of what changes were made
to the server. This is a perfect illustration of the power of dynamic constraints:
we can use the server to make changes to the way the client application
behaves and looks to the user, without having to deploy a new client.
For the client, here's all that we need to do:
- Drop a DCOMConnection component
- Set: RemoteServer=ConstraintSvr.Constraints
- Drop a ClientDataset component
- Set: RemoteServer=DCOMConnection1
- Set: ProviderName=prvCustomer
- Drop a DataSource
- Set: DataSet=ClientDataset1
- Drop a DBNavigator
- Set: DataSource=DataSource1
- Drop a Button
- Set: Name=btnOpen
- Double click on the button's click event in the object inspector
- Make the btnOpenClick event code look like this:
procedure TFormConstraintClient.BtnOpenClick(Sender: TObject);
begin
if ClientDataSet1.Active then
ClientDataSet1.Close;
ClientDataSet1.Open; { Refresh the dataset }
BtnOpen.Caption := '&Reopen';
end;
Demonstrating Dynamic Constraints
We're all done with the client. We can run the client and click Open
to start and connect to the server, and retrieve the original data packet.
The client screen will look something like this after Open is pressed.
Note that the button caption has changed to "Reopen", as the code above
indicates:
Notice that every CustNo value has "CN " in front of it. This
is not actually part of the data, and takes up screen real estate that
could be better used to display more unique data. We need to fix that.
Also, I don't think the Addr2 column really needs to be displayed
in this grid, so we don't want it to be visible either.
If you scroll to the right in the grid until the TaxRate column
is visible, go ahead and put in some really high value like 80 for one
of the rows. You are allowed to enter this value because the default constraint
for that field sets the maximum at 100 and the minimum at 0. Since no country
in the world has 100% tax rate, allowing a value that high is not desirable.
We'll use the run time constraint editor to fix that and a few other things
about how the distributed data is presented on the client.
Editing Constraints on the Server
Once the client is running, the server is also active. If we press the
Refresh button, the list of available providers will be loaded. Selecting
"prvCustomer" from the providers column displays the fields for that provider.
The following screen shot also has the CustNo field selected. Note
that the display format is "CN 0000", which means that every customer number
will have "CN " in front of it, as you saw in the screen shot of the client
above.
We'll delete the "CN " from the display format for CustNo, make
the label "Cust #" and click Apply. This saves the changes for the
data constraints. Next, let's select Addr from the field list, uncheck
Visible, and click Apply. Finally, let's select TaxRate
and set its Max Value to something more reasonable, like 60,
and click Apply.
Refreshing the Client
Let's go back to the client and click Reopen. You should see something
similar to this following screen. Note that the column heading for CustNo
and the display format for each customer number has changed. Also note
that Addr2 is no longer visible on the client.
Finally, we just need to verify that our validation rules for the tax
rate are enforced. Go ahead scroll back over to the TaxRate column
again, and try to enter a value above 60. You will see a message similar
to the following:

This sure makes delivering a maintainable distributed database application
much easier, doesn't it?
Constraint Constraints
In Delphi and C++ Builder 4, the validation constraints for specific fields
are converted by the MIDAS server from a string into a binary format for
transmission in the data packet, which makes it difficult to edit them
on the client side. This may change for Delphi 5, but you could also create
a client application to allow you to edit the server-based constraints
by taking the techniques I've shown here and writing custom exported methods
for the server to accept new constraints from a client.
Summary
MIDAS is typical of our development tools. They are very easy to use and
deceptively simple on the surface, but extremely flexible, powerful, and
customizable. If you have technical questions about MIDAS, or would like
to see other MIDAS-related topics covered, leave a message in the borland.public.midas newsgroup. It's a very active newsgroup, with many skilled MIDAS developers
present.
Enjoy and code well.