One of the main goals of PowerBuilder Classic application refactoring is to divide the code into logical partitions. Although you will likely not gain significant performance increases in exchange for your efforts, you will achieve two other highly significant gains. First, your logic will gain interoperability; you will have the ability to share application business and data logic with other applications developed in other .NET languages. (I say ability because you still need to make your method interfaces Common Type System compliant.) Second, your code maintenance activities will gain a more predictable and less likely flawed result because of the code's newly attained clarity.
Figure 1 shows the conceptual structure of a typical Classic application before refactoring. Note that both business logic and data access logic are embedded with GUI elements.
Figure 2 illustrates a typical conceptual structure after refactoring. The important feature to note is that the GUI is decoupled from both the business logic and the data access logic. Any data that is needed by business logic is gotten by calls to classes in the data access partition. In this architecture, the GUI can be successfully disconnected from the database.
The DataWindow Challenge
If you look carefully at Figure 2, you'll notice that there is no flow between the Data Logic and the GUI. One minute!! What about DataWindows? How does data get displayed on the GUI? As we all know, DataWindows are the pure PowerBuilder mechanism of choice for displaying and interacting with data sets from the database whose use extends far beyond providing data to business logic. The figure doesn't show a connection between the DataLogic layer and the GUI. How come?
DataWindows present a challenge to partitioning because their internal structure defies partitioning. To clarify - a DataWindow Object is a self-contained intelligent object that consists of its own two logical layers: a presentation layer and a Data Source layer. The two layers are linked by a mapping between their elements that specifies which data source elements are linked to which presentation elements. Figure 3 shows the mapping as seen on the DataWindow Painter's Column Specifications View. If you doubt me, please note that the DataWindow tool is actually composed of two tools: one for defining the presentation and the other for defining the data source. In addition, the presentation layer has its own scripting language and an expression evaluation engine for defining GUI layer behaviors and dynamic characteristics. When you associate the DataWindow Object with a control, you gain the ability to code interacts with both the presentation layer and data layer APIs. Most important, you call
SetTransObject( ) to link the DataWindow's data source layer directly to the database via the control. From all this you can plainly see that placing a DataWindow on a window effectively breaks our partitioning scheme on multiple levels. The question begs an answer: How can we effectively partition DataWindows?
The answer is in the DataWindow API itself, which has a couple of mechanisms that support partitioning. Using these APIs you can place presentation ‘markup' and GUI logic in the GUI layer and data access logic in the Data layer. There are two possible approaches, which one you choose depends on your end goal. If your goal is to remain in a single application but have partitioned code, use the
ShareData( ) mechanism to accomplish your goal. If your goal is to bust out your DataWindows into separate assemblies for interoperability with foreign .NET code, the DataWindow synchronization API is your man (assuming you use DataWindow .NET in your Visual Studio applications). I'll now take a look at the single partitioned application approach; I'll examine interoperability in another article.
The ShareData API has been around forever. It allows you to have multiple DataWindow or DataStore presentations hooked up to a single buffer set. It's great because it is memory efficient and the effect is instantaneous. You designate one data control as the primary and the rest become secondaries. There is only one data set to drive all the presentations. DataWindow objects that are sharing can have different or the same presentation styles. The caveat is that they must all have a common data source definition. With ShareData turned on, any changes to the data in the shared buffer are reflected in all the presentations. You can leverage this mechanism to support logical portioning.
The basic technique is to wrap the primary DataWindow Objects inside a DataStore in DataAccess layer Custom Class User Objects. All presentation layer DataWindows become secondary controls. Disconnect the GUI layer from the database and share the data from DataAccess components to the GUI, then write (or move) your data access layer code inside the component, exposing it as necessary via method calls. This logic includes updates, retrieves and DBError event handling logic. You should keep GUI presentation layer particulars such as modify and describe calls, property expressions, data entry validation rules and navigation logic in the GUI layer. You can also leave data entry validation rules and visual property settings inside DataWindow object syntax.
Like everything in this world, not all is pure bliss. There are limitations. The good news is that for most of the limitations, code workarounds are possible. I'll describe them in the following section.
When sharing parent DataWindows, Child DataWindows are not shared. This limitation has two ramifications. First, DropDownDataWindows are not automatically populated in secondary controls. The workaround is that you'll need to retrieve the DropDowns in separate DataStores in the data components and share them with the DropDowns in the GUI. To avoid duplicate and unnecessary child data retrieves in the data partition, you should turn off the AutoRetrieve property in the primary DataWindow Object's definition. In the GUI layer, after coding the share on the parent DataWindow, populate the DropDowns using
GetChild( ) to make them secondary DataWindows to their primary partners in the Data Access layer.
Second, similar to the DropDown DataWindow issue, nested DataWindows inside a Composite DataWindow are not shared. If the GUI sports a Composite DataWindow presentation, there is no sense in sharing the Composite DataWindow directly. You'll waste time retrieving it in the Data Component since nothing is shared. Rather place the Composite and all its nested DataWindow objects in the GUI layer. Duplicate the nested DataWindows in the data access layer. Retrieve all the nested DataWindows into DataStores and then share them with their partners in the GUI layer.
Third, Crosstab DataWindow objects are not sharable, period. Designating a data control with a crosstab presentation as a secondary will fail. The workaround is to use the DataWindow synchronization API
GetFullState( ) and
SetFullState( ). In the data component, retrieve into a DataStore utilizing the crosstab presentation DataWindow Object. Then call
GetFullState to get the presentation and the data into a blob. Next, return the blob. On the GUI side, call
SetFullState to apply the blob to an empty DataWindow control. There are two downsides to this method. One, it will take double memory because there are actually two data sets here. Two, the data is not automatically synchronized between the controls, so changes to the ‘primary' are not automatically reflected in the GUI layer. Figure 4 shows the synchronization code.
Last, when you are sharing data, you cannot turn on query mode for a secondary DataWindow. If your DataWindows rely on QueryMode to allow the user to specify WHERE clause criteria, you'll have to redesign your query mechanism. Trying to set the QueryMode or QuerySort DataWindow object properties causes an error.
To conserve memory and improve performance when sharing from a primary control in a CCUO, you can delete the GUI layer from the Primary DataWindow Object in the Data Access layer. To do this, first create a matching duplicate of the DataWindow Object definition by doing a SaveAs to the DataAcess PBL. Add a suffix such as
_nv to the name so you can associate the pairs. Select All in the layout painter and delete. DataWindow object names will remain but the object will be gone from the GUI. Also set the height of all the bands to zero. Last, make sure you turn off the AutoSize height property for any bands that may have had it set. You are only interested in the buffer definition.
The purpose of this exercise has been to show you that with a bit of code refactoring it is possible to disconnect your GUI layer from the database and facilitate complete logical partitioning of your application. Having a well-partitioned application will open the way for future code migration to other GUI platforms.
We can now complete the conceptual diagram, as shown in Figure 5, to show the relationship between the GUI and the Data Logic Layer.