Try OpenEdge Now
skip to main content
ProDataSets
ProDataSet Attributes and Methods : Building a dynamic user interface from a ProDataSet
 

Building a dynamic user interface from a ProDataSet

In most cases, you expect that server-side ProDataSet definitions will be static in most procedures of your application. This is because each ProDataSet is likely to have a unique combination of tables and relations. Also, it is much easier to write business logic using static ABL statements that can reference table and field names directly rather than using indirect references such as BUFFER-FIELD(x):BUFFER-VALUE. Because business logic tends to be unique to the tables involved in most cases, a general purpose dynamic procedure that can handle many different ProDataSets is less likely to be useful on the server-side of an application, where you are loading ProDataSets from your database, doing validation, and saving back updates. There will, of course, be exceptions to this. The ability to mix and match DATASET and DATASET-HANDLE parameters makes it easy to pass a ProDataSet to another procedure, either locally or remotely, without being concerned about whether the other procedure wants to match the parameter up to a static ProDataSet definition or inspect it in a more general way through its handle.
In some cases, it is more likely that a user interface procedure on the client side of the application wants to accept a variety of different ProDataSets and display their data in a consistent way, by inspecting the ProDataSet structure and creating dynamic user interface objects such as fields and browses appropriate to the ProDataSet.
This section shows you an example of how you can build these kinds of procedures. You will run the same DynamicDataSet2.p procedure you built earlier, but from an AppBuilder-built window procedure that creates a series of dynamic browses for the tables in the ProDataSet.
Attaching a browse to a ProDataSet temp-table is very easy. If the table the browse displays is the child of a Data-Relation, then you can simply associate the browse with the dynamic query the ProDataSet generates for the relation. In this way the browse automatically shows the right records for the parent of the relation, because the dynamic query is filtered based on the parent key values and automatically reopens itself internally each time the record in the parent buffer changes. This can happen either programmatically by using a GET-NEXT() method or similar statement on the query, or by selecting a record in a browse for the parent table.
To create the new window procedure:
1. Create a new Window procedure in the AppBuilder.
2. Name its default window BrowseWin and its default frame BrowseFrame.
3. Make the window and frame 16 rows by 120 columns.
4. In the procedure's Main Block, add a statement to run an internal procedure called showDataSet, as shown:
MAIN-BLOCK:
DO ON ERROR UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK
ON END-KEY UNDO MAIN-BLOCK, LEAVE MAIN-BLOCK:
RUN enable_UI.
RUN showDataSet. IF NOT THIS-PROCEDURE:PERSISTENT THEN
WAIT-FOR CLOSE OF THIS-PROCEDURE.
END.
5. Define the internal procedure showDataSet. The procedure gets a dynamic ProDataSet back from DynamicDataSet2.p, and it creates three browses, each of which has a query and a buffer handle, so it needs these variables:
DEFINE VARIABLE hBuffer1 AS HANDLE NO-UNDO.
DEFINE VARIABLE hBuffer2 AS HANDLE NO-UNDO.
DEFINE VARIABLE hBuffer3 AS HANDLE NO-UNDO.
DEFINE VARIABLE hBrowse1 AS HANDLE NO-UNDO.
DEFINE VARIABLE hBrowse2 AS HANDLE NO-UNDO.
DEFINE VARIABLE hBrowse3 AS HANDLE NO-UNDO.
DEFINE VARIABLE hDataSet AS HANDLE NO-UNDO.
DEFINE VARIABLE hQuery1 AS HANDLE NO-UNDO.
DEFINE VARIABLE hQuery2 AS HANDLE NO-UNDO.
DEFINE VARIABLE hQuery3 AS HANDLE NO-UNDO.
Note that this example is somewhat simplified in that it is designed to accept a ProDataSet of exactly the kind that DynamicDataSet2.p creates, with three tables and a single Data-Relation between the first two of these tables. By extending the procedure somewhat, you can make it work properly for virtually any combination of tables and relations that might come from another procedure. You can also make a decision as to which data to display with dynamic fill-ins or other single field objects, and which to display in browses.
6. Add the same RUN statement you used in DynamicMain2.p, as shown:
RUN DynamicDataSet2.p
  (INPUT "Customer,Order,SalesRep",
   INPUT "CustNum,OrderNum,SalesRep",
   INPUT "CustNum,CustNum",
   INPUT "= 1",
   OUTPUT DATASET-HANDLE hDataSet).
The top-level buffer does not have a query created for it automatically because these Data-Relation queries are only for the child table of a relation, where the filtering occurs. So, retrieve the buffer handle for the ProDataSet's first buffer and create a dynamic query for it.
7. Prepare the query for each record in the top-level temp-table and open it:
hBuffer1 = hDataSet:GET-BUFFER-HANDLE(1).
CREATE QUERY hQuery1.
hQuery1:ADD-BUFFER(hBuffer1).
hQuery1:QUERY-PREPARE("FOR EACH " + hBuffer1:NAME).
hQuery1:QUERY-OPEN().
8. Create a dynamic browse for this query:
CREATE BROWSE hBrowse1 ASSIGN
  QUERY       = hQuery1
  FRAME       = FRAME BrowseFrame:HANDLE
  HIDDEN      = FALSE
  NO-VALIDATE = TRUE
  WIDTH       = 120
  HEIGHT      = 5
  SEPARATORS  = TRUE
  SENSITIVE   = TRUE.
The QUERY attribute connects the browse to the query on the top-level table.
The FRAME attribute parents it to the frame in your window.
The HIDDEN attribute makes sure it is displayed when the window is displayed.
The NO-VALIDATE attribute disables any field-level validation expressions that are built into the schema definitions for the database tables and inherited, by default, by the ProDataSet's temp-tables. For example, if there is a field validation expression for the ttOrder table that tries to check that the CustNum is in the Customer table, this might not work because the ProDataSet, and potentially your window as well, are not connected to the database.
The HEIGHT and WIDTH attributes set the browse's size.
The SEPARATORS attribute provides vertical and horizontal lines between columns and rows to improve readability.
The SENSITIVE attribute allows you to scroll the browse, even though its columns are not enabled.
9. Complete the browse definition by adding all the columns from the top-level table's buffer to it. For example:
hBrowse1:ADD-COLUMNS-FROM(hBuffer1:NAME).
10. You need to identify the query for the second table in the ProDataSet (the ttOrder table in this example). Because this is the child buffer of a Data-Relation, there is a query already available for you to use. You will recall that this query has a WHERE-STRING that filters ttOrder records for the currently selected ttCustomer, as shown:
The AVM inserts the correct value for ttCustomer.CustNum each time the ttCustomer record in the buffer changes.
To use this query, simply retrieve its handle from the Data-Relation and open it:
hQuery2 = hDataSet:GET-RELATION(1):QUERY.
hQuery2:QUERY-OPEN().
11. Create a second dynamic browse to display its records, positioning it below the first one:
CREATE BROWSE hBrowse2 ASSIGN
  QUERY       = hQuery2
  FRAME       = FRAME BrowseFrame:HANDLE
  HIDDEN      = FALSE
  NO-VALIDATE = TRUE
  ROW         = 6
  WIDTH       = 120
  HEIGHT      = 5
  SEPARATORS  = TRUE
  SENSITIVE   = TRUE.
12. Get the temp-table's buffer handle and add its columns to the second browse:
hBuffer2 = hDataSet:GET-RELATION(1):CHILD-BUFFER.
hBrowse2:ADD-COLUMNS-FROM(hBuffer2:NAME).
You could use either the expression hDataSet:GET-RELATION(1):CHILD-BUFFER to identify the right buffer, or hDataSet:GET-BUFFER-HANDLE(2). That is to say, the buffer you want is the child of the first (and only) relation, and also the second buffer overall in the ProDataSet.
13. Create a query for the third table. Because this is not in a relation, there is no Data-Relation query for it, so you need to create and prepare it yourself:
hBuffer3 = hDataSet:GET-BUFFER-HANDLE(3).
CREATE QUERY hQuery3.
hQuery3:ADD-BUFFER(hBuffer3).
hQuery3:QUERY-PREPARE("FOR EACH " + hBuffer3:NAME).
hQuery3:QUERY-OPEN().
14. Create a third browse to display the third buffer's records, positioning it below the second one, and add the buffer's columns to the browse:
CREATE BROWSE hBrowse3 ASSIGN
  QUERY       = hQuery3
  FRAME       = FRAME BrowseFrame:HANDLE
  HIDDEN      = FALSE
  NO-VALIDATE = TRUE
  ROW         = 11
  WIDTH       = 120
  HEIGHT      = 5
  SEPARATORS  = TRUE
  SENSITIVE   = TRUE.

hBrowse3:ADD-COLUMNS-FROM(hBuffer3:NAME).
15. Save this procedure as DynDataSetWin.w and run it:
Voilà! The top-level browse confirms that you selected just one Customer. The second one displays the Orders for that Customer. Because displaying the Customer browse automatically positions to that one record in the ttCustomer buffer, the Data-Relation's query for the ttOrder query finds the value it needs there. The third browse shows all SalesReps, unrelated to the other tables.
16. Just to confirm what the Data-Relation query does for you, and how this is especially useful when you attach it to a browse, modify the RUN statement to request all Customers less than 10, along with their Orders. For example:
RUN DynamicDataSet2.p
  (INPUT "Customer,Order,SalesRep",
   INPUT "CustNum,OrderNum,SalesRep",
   INPUT "CustNum,CustNum",
   INPUT "< 10",
   OUTPUT DATASET-HANDLE hDataSet).
17. Run the procedure again:
As you select different Customers, the Order browse reopens automatically with its Orders. Remember that the ttOrder table in the ProDataSet actually contains all the Orders of all the selected Customers, but its default query filters them for you when this is useful, as it is when you use a browse to display them.
18. Just to prove that the dynamic nature of the procedure really works, change the parameters of the RUN statement to use a different set of tables, as shown:
RUN DynamicDataSet2.p
  (INPUT "Order,OrderLine,Item",
   INPUT "OrderNum,LineNum,ItemNum",
   INPUT "OrderNum,OrderNum",
   INPUT "< 10",
   OUTPUT DATASET-HANDLE hDataSet).
There is one small problem here. Our simplified example expects a single key field for each table, and the key for OrderLine is, in fact, OrderNum and LineNum together. However, this does not affect the example in this case:
So what you have created here is a factory for a particular class of ProDataSets (the procedure DynamicDataSet2.p) and a general-purpose display mechanism for any ProDataSet of that type (the window procedure DynDataSetWin.w). This is a simple example of a very powerful capability.
Feel free to extend the window to prompt the user for the table and field names the parameters need, to make the example more fully dynamic.