Dynamic Constraints in MIDAS

By: John Kaster

Abstract: John K shows source code and sample projects for a run-time dynamic constraint editor for MIDAS servers, allowing you to change data constraints on the fly for your clients without recompiling

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 PropertyDescription
ConstraintErrorMessageMessage to display if data for the field if not valid.
Example: CustNo cannot be blank
CustomConstraintConstraints to place on the validation of the field. Expressions can be used for this constraint.
Example: CustNo IS NOT NULL
DisplayFormatControls formatting of the field for display
DisplayLabelUsed as the data field's default column heading.
Example: Customer #
EditMaskUsed for customizing data entry for the field.
Example: 0000
MaxValueThe maximum value of a numeric field
MinValueThe minimum value of a numeric field
VisibleA 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:
  1. Drop a DCOMConnection component
  2. Set: RemoteServer=ConstraintSvr.Constraints
  3. Drop a ClientDataset component
  4. Set: RemoteServer=DCOMConnection1
  5. Set: ProviderName=prvCustomer
  6. Drop a DataSource
  7. Set: DataSet=ClientDataset1
  8. Drop a DBNavigator
  9. Set: DataSource=DataSource1
  10. Drop a Button
  11. Set: Name=btnOpen
  12. Double click on the button's click event in the object inspector
  13. 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.


Server Response from: ETNASC02