Using DatasetController for developing static (client side) code accessing and manipulating ProDatasets

Introduction

With Change No. 13342 (6th of May, 2010) Consultingwerk has branched some exciting new features to the customer accessible / stable branch of the SmartComponent Library. One of the major new features is the ability to register a DatasetController class that contains a static ProDataset definition with SmartBusinessEntityAdapter instances.  

The DatasetController defines a static ProDataset with the same structure (ideally using the same include files that define the schema) than the business entity (procedural, object-oriented or from a partner framework like ticEnterprise). Because of the static schema definition and a centrally accessible location the DatasetController simplifies writing (client side) logic working on the data managed by the SmartBusinessEntityAdapter (or derived types).  

This development strategy is closely related to common MVC / MVP design patterns. It simplifies the separation of the actual UI logic from client side business logic and may also be used in Business Logic on the AppServer, referenced by the actual Business Entity (as long as developers does not access functionality like .NET classes that are not available on the AppServer).

Developing a DatasetController class

The basic requirement for a DatasetController class is that it has to implement the IDatasetController (Consultingwerk.SmartComponents.Interfaces.IDatasetController) interface. This Interface enforces the availability of the following members:  

PUBLIC READ-ONLY Property DatasetHandle .
PUBLIC VOID Method DeregisterConsumer (Progress.Lang.Object) .
PUBLIC VOID Method RegisterConsumer (Progress.Lang.Object) .

To implement a well behaving DatasetController it is sufficient to define a ProDataset (best practice would be using an include file) and to assign the handle of the ProDataset to the DatasetHandle property.  

{OERA\schema\dsOrder.i &ACCESS="PRIVATE"}

DEFINE PUBLIC PROPERTY DatasetHandle AS HANDLE NO-UNDO 
GET.
PRIVATE SET.

CONSTRUCTOR PUBLIC OrderDatasetController ():
    SUPER ().
        
    THIS-OBJECT:DatasetHandle = DATASET dsOrder:HANDLE . 
        
END CONSTRUCTOR.

There is no foundation class for DatasetControler inplementations. However we are aware that in individual customer implementations it might be advised to implement certain common functionality (like keeping a reference to the DataAdapter or subscribing to events) in a common base class.

 Using a DatasetController with SmartBusinessEntityAdapter and SmartDatasetChildAdapter components

There are two ways to associate a DatasetController with a SmartBusinessEntityAdapter:  

The standard way to associate a DatasetController with a SmartBusinessEntityAdapter will be using the property sheet of the SmartBusinessEntityAdapter instance on a Form. The SmartBusinessEntityAdapter has a property called DatasetControllerType where a developer should enter the class name of the DatasetController for the used Business Entity. The class name entered in the property sheet will be validated immediately.  

An alternative way of associating a DatasetController with a SmartBusinessEntityAdapter would be to assign a reference of an instance of a DatasetController to the property named DatasetController of the SmartBusinessEntityAdapter instance. This needs to be done before the first time RetrieveData() or similar methods have been called that might require that the SmartBusinessEntityAdapter is required to get a dataset instance.  

The property DatasetController can in any case be used to get a reference to the current DatasetController instance associated with a SmartBusinessEntityAdapter. When using the DatasetControllerType property in the property grid the SmartBusinessEntityAdapter will dynamically create an instance of that type and assign the reference to the DatasetController property.  

In any case the SmartBusinessEntityAdapter will “introduce” himself to the DatasetController using the RegisterConsumer method of the IDatasetController interface. This method needs to be implemented in the DatasetController and might be used to register to events of the SmartBusinessEntityAdapter class.  

A SmartDatasetChildAdapter will - like a SmartBusinessEntityAdapter - register with the DatasetController when it’s direct or indirect SmartDataSource is a SmartBusinessEntityAdapter that is working together with a DatasetController. The SmartDatasetChildAdapter will work on buffers on the static defined ProDataset provided by the DatasetController. A SmartDatasetChildAdapter is not able to independently create an instance of a DatasetController. From an architectural point of view there is no need for that. The SmartDatasetChildAdapter only creates another view on the data managed by the SmartBusinessEntityAdapter. The SmartDatasetChildAdapter is navigating on the same data as the SmartBusinessEntityAdapter and will always use the same ProDataset instance as its direct or indirect SmartDataSource that is a SmartBusinessEntityAdapter.  

The method DeregisterConsumer of the interface IDatasetController will be called by both types of the SmartDataAdapters when they are being destroyed.

Registering to events from the data adapter components  

To allow generic uses of the DatasetController classes the RegisterConsumer method expects the new consumer (usually SmartBusinessEntityAdapter or SmartDatasetChildAdapter) as a parameter of type Progress.Lang.Object. Before subscribing to relevant events from the consumer it is required to cast to the actual type.  

METHOD PUBLIC VOID RegisterConsumer (poConsumer AS Progress.Lang.Object):
        
    DEFINE VARIABLE oAdapter AS SmartBusinessEntityAdapter NO-UNDO .

    IF TYPE-OF (poConsumer, SmartBusinessEntityAdapter) THEN DO:
        oAdapter = CAST (poConsumer, SmartBusinessEntityAdapter) .
                
        oAdapter:AfterRetrieveData:Subscribe (AfterRetrieveDataHandler) . 
    END.
        
END METHOD.

Accessing the DatasetController from other classes (like the Form or a SmartViewerControl)  

The reference to the DatasetController is accessible from the DatasetController property of the SmartBusinessEntityAdapter. From a Form the DatasetController can directly be accessed using the instance variable of the SmartBusinessEntityAdapter, however since the purpose of the DatasetController is usually to contain custom code it will be required to CAST the reference to the actual type (the DatasetController property is always returning a reference of the type IDatasetController.).  

Something like this:  

DEFINE VARIABLE oDatasetController AS OrderDatasetController NO-UNDO .

ASSIGN oDatasetController = CAST (smartBusinessEntityAdapter1:DatasetController,
                                  OrderDatasetController).

In the case of a SmartViewerControl it is require to CAST the own SmartDataSource to the SmartBusinessEntityAdapter or SmartDataChildAdapter type before accessing the DatasetController property.

Example about using the DatasetController on the UI to manipulate Data

The following is sample for a method in a DatasetController that updates the SalesRep field of the eCustomer record.  

Please note: For Tables navigated by the SmartBusinessEntityAdapter (EntityTable and EntityView Tables) the implicit buffers for these tables (simply the buffer named eCustomer by default) will by positioned already on the same record as the UI. When it’s required to work on different records it’s advised to create an explicit buffer to avoid conflicts.  

For the tables navigated by the SmartDatasetChildAdapter there is no implicit positioning of a buffer available in the DatasetController class as the SmartDatasetChildAdapter will create separate buffers for the temp-tables in its query.  

The method RegisterConsumer is used the obtain the reference to the SmartBusinessEntityAdapter. This reference will be required to submit the modified rows. The DatasetController is by default not able to submit changes on it’s own.  

METHOD PUBLIC VOID RegisterConsumer (INPUT poConsumer AS Progress.Lang.Object):
        
    IF TYPE-OF (poConsumer, SmartBusinessEntityAdapter) AND 
       NOT VALID-OBJECT (oPrimaryDataAdapter) THEN 
           
        oPrimaryDataAdapter = CAST (poConsumer, SmartBusinessEntityAdapter) . 

END METHOD.

The first portion of this method validates the availability of the primary Data Adapter and makes sure that no SmartDataTarget of this Data Adapter is in update mode. If that would be the case it would be advised to perform the required modifications from within that SmartDataTarget (e.g. a SmartViewerControl).  

Further down the change tracking of the Temp-Table which contains the fields to be modified is turned on and the modifications to the record are performed.  

METHOD PUBLIC VOID AssignSalesRep (pcSalesRep AS CHARACTER):
        
    DEFINE VARIABLE rRowid AS ROWID NO-UNDO . 
        
    IF NOT VALID-OBJECT (oPrimaryDataAdapter) THEN 
        UNDO, THROW NEW AppError ("A SmartBusinessEntityAdapter needs to be associated with this DatasetController first.", 0) .
            
    IF VALID-OBJECT (oPrimaryDataAdapter:UpdatingSmartDataTarget) THEN 
        UNDO, THROW NEW AppError ("The SmartBusinessEntityAdapter is currently in update mode.", 0) .
        
    IF eCustomer.SalesRep = pcSalesRep THEN 
        RETURN . /* no action required */            
        
    TEMP-TABLE eCustomer:TRACKING-CHANGES = TRUE .
    
    ASSIGN 
        eCustomer.SalesRep = pcSalesRep 
        rRowid             = ROWID (eCustomer).

    oPrimaryDataAdapter:SubmitChanges () .
    
    /* Refresh a single row */
    /* oPrimaryDataAdapter:BindingSource:Refresh () .*/
 
    /* Reopen query in case the join between the tables might 
       have changed */
    oPrimaryDataAdapter:QueryHandle:QUERY-OPEN () .
    oPrimaryDataAdapter:BindingSource:RefreshAll() .
    oPrimaryDataAdapter:QueryHandle:REPOSITION-TO-ROWID (rRowid) .   
 
    CATCH err AS Progress.Lang.Error :
        Consultingwerk.Util.ErrorHelper:ShowErrorMessage (err) .        
    END CATCH.
 
END METHOD.

The SubmitChanges method of the Data Adapter writes back the dataset with the modified records (GET-CHANGES) to the Business Entity. In case of (validation) errors an error object is thrown that should be catched and presented to the user.  

When it’s required to refresh the UI after these modifications two cases may occur. When it’s enough to refresh the current iteration of the query the ProBindingSources Refresh () method is sufficient (like shown in this example in the comment). When it’s required to re-evaluate the join criteria it’s required to re-open the query, call the RefreshAll() method of the ProBindingSource and reposition the query to the previous ROWID as shown in the sample code.

Update: Simplified method to evaluate the reference to the SmartBusinessEntityAdapter

An update to the SmartBusinessEntityAdapter has been released today.  

The static method FromDatasetController (poDatasetController AS !IDatasetController) has been added to the SmartBusinessEntityAdapter implementation. This method should simplify the tasks of locating the SmartBusinessEntityAdapter that works on the ProDataset defined in the given DatasetController instance.  

To use this method in a DatasetController class do something like this:  

DEFINE VARIABLE oAdapter AS SmartBusinessEntityAdapter NO-UNDO .
                
ASSIGN oAdapter = SmartBusinessEntityAdapter:FromDatasetController (THIS-OBJECT) .

This reference will allow you to invoke methods of the SmartBusinessEntityAdapter (like SubmitChanges) directly. It is no longer required to use the method RegisterConsumer to get the reference of the primary SmartBusinessEntityAdapter instance. However if you need to subscribe to events of the SmartBusinessEntityAdapter you will still have to implement this in the RegisterConsumer method.