View RSS Feed

Development Team Blog

Conditional Constraints

Rate this Entry
In prior articles we’ve discussed the basics of constraints. Now it's time to put them all together. Before we get started let's review what we've covered so far.

Constraints and Local Variables discusses how constraints work. While the main purpose of the article was to talk about how local variables should be used within OnConstrain, it provides the basics for how constraints are built explaining the relationship between OnConstrain and Rebuild_Constraints.

Using Expressions and Properties in Constraints discusses how functions and properties can be used with constraints. It also explains how a DDO’s constraints are inherited from its parent DDO and what that means to the placement of the property and function definitions.

Constraints and Performance explains in detail how constraint expressions are built and how the runtime optimizes these expressions based on the index used.

Inherited Constraints talks more about inherited DDO constraints and explains how and why this inheritance can be disabled

We will now apply all of this information and use this to create conditional constraints. Creating conditional constraints can provide important functionality to your application’s views and dialogs. It allows you to change your constraint filter based on some external event. Usually this event will be some kind of user action. Creating conditional constraints can make you views and dialogs much more multi-purpose. If you understand how constraints work, it’s pretty easy to do. If you don’t understand how constraints work, it will appear to be pretty easy but you will just encounter one problem after another.

Basic Conditional Constraints

As always, we will use the sample Order Entry system as our test. We will start by adding a conditional constraint to our Customer Entry View. We want to be able to select all customers or only active customers (where an active customer has Customer.Status="Y"). We want to be able to toggle between these two constraint conditions. To make this simple the toggle event will be pressing a key (Ctrl+A).

We will start by altering the sample Customer view (Customer.vw). First we will create the constraint for the Customer_DD object.
Code:
Property Integer pbCustomerActiveOnly False
      
Object Customer_DD is a Customer_DataDictionary
     Procedure OnConstrain
        Boolean bCustomerActiveOnly
        Get pbCustomerActiveOnly to bCustomerActiveOnly
        If (bCustomerActiveOnly) Begin
            Constrain Customer.Status eq "Y"
        End
        Forward Send OnConstrain
    End_Procedure
End_Object
Remember that OnConstrain is only called when the constraints are rebuilt. Normally this is an automatic process, which occurs when a view or dialog is paged (i.e., activated from a non-active state). When this activation process occurs, the message Rebuild_Constraints is sent to every DDO. Often this is the only time that Rebuild_Constraints is sent to your DDOs. Rebuild_Constraints calls OnConstrain and the constraint filters are created.

In the above example, the value of pbCustomerActiveOnly is used to determine if the Constrain command should be executed, which determines if this constraint condition should be added the the DDO’s internal constraint expression. Again note that this occurs when the view/dialog is paged and not each time a record is found. This is discussed in
Constraints and Local Variables and it’s really important that you understand this.

Because we are not using Constrain As this constraint is completely static.


Notice that the property that controls this, pbCustomerActiveOnly is created in the DDO’s parent object and not within the DDO. The reason for this is explained in Using Expressions and Properties in Constraints.

Now that we’ve created the constraint in OnConstrain how do we make this constraint conditional? We need to create a user event that calls a method, which will 1) change the condition of the constraint and 2) manually rebuild the constraint. We can do this by adding the following to the dbView object:
Code:
On_Key Key_Ctrl+Key_A Send ToggleActive
      
Procedure ToggleActive
    Boolean bCustomerActiveOnly bOk
    Get pbCustomerActiveOnly to bCustomerActiveOnly
    Set pbCustomerActiveOnly to (not(bCustomerActiveOnly))
    Send Rebuild_Constraints of Customer_DD
    Send Find of Customer_DD FIRST_RECORD 1
End_Procedure
This code should be self-explanatory. We change the condition by changing the property and we rebuild the constraint by sending Rebuild_Constraints. Why do we send the Find message at the end? After the constraint is rebuilt the current record in the DD may no longer be valid. What happens if the record is no longer valid? Don’t find out - the behavior is undefined and you do not want to rely on it. Therefore, we make sure the record is valid by finding the first valid record.

If you add this code to the Customer Entry View, you can test this by finding customer 8, which happens to be an inactive customer. When the view is activated, you should be able to find the record because there are no filters. If you press Ctrl+A you will not be able to find customer 8. If you press Ctrl+A again, you will be able to find it.

We have now created a conditional constraint.

Here’s a minor technique you can apply which will make it a little easier to actually see this filtering. In the Order Entry sample, all of the lookup lists use their own DDO structures. If you invoke the Customer Lookup List, you will never see filtered records because the dbList’s DDO has no constraints. We can change that. If the dbList’s Auto_Server_State is set to True, the dbList will use the DDO structure of the invoking object's view. This will make the dbList use the constraints we’ve just created. Rather than changing this property via code in the dbList we can do this dynamically. We can set Auto_Server_State to True right before the Customer Lookup List is invoked by setting it inside of the invoking object’s Prompt_Callback event (the invoking object is the entry object that requested the prompt).
Code:
Object oCustomer_Number is a dbForm
    Entry_Item Customer.Customer_Number
    Set Label to "Customer Number:"
    Set Size to 13 42
    Set Location to 5 72
    Set peAnchors to anTopLeft
    Set Label_Col_Offset to 2
    Set Label_Justification_Mode to jMode_Right
   
    Procedure Prompt_Callback Integer hPrompt
        Set Auto_Server_State of hPrompt to True
        Forward Send Prompt_Callback hPrompt
    End_Procedure
          
End_Object
This setting will be temporary. This will set Auto_Server_State before the lookup list is invoked. When the list is closed, its original property value will be restored. Check out the documentation on Prompt_Callback, Store_Defaults and Restore_Defaults. This will make it a little bit easier to test our results.

In a real world example you’d probably want to add a radio button or a checkbox to control the filtering. I am not doing this here. I’ve tried to keep this sample as simple as possible by not adding any user-interface to this other than a new shortcut key.

Conditional Constraints in Parent DDOs

Now lets add this same filter to the Order Entry View. As we’ve learned in Inherited Constraints we can choose to apply this filter to just the customer DDO records or to the DDOs that are children of the customer DDO. This is controlled by the pbInheritConstraints property. In this case, let’s use the default behavior of inheriting parent constraints in the OrderHea DDO. This means that any OrderHea record with an inactve customer will be filtered. Here is what our code will look like in Order.vw:
Code:
Property Integer pbCustomerActiveOnly
   
Object Vendor_DD is a Vendor_DataDictionary
End_Object    // Vendor_DD
   
Object Invt_DD is a Invt_DataDictionary
    Set DDO_Server to Vendor_DD
End_Object    // Invt_DD
   
Object Customer_DD is a Customer_DataDictionary
    Procedure OnConstrain
        Boolean bCustomerActiveOnly

        Get pbCustomerActiveOnly to bCustomerActiveOnly
        If (bCustomerActiveOnly) Begin
            Constrain Customer.Status eq "Y"
        End
        Forward Send OnConstrain
    End_Procedure
End_Object    // Customer_DD
   
Object SalesP_DD is a Salesp_DataDictionary
End_Object    // SalesP_DD
   
Object OrderHea_DD is a OrderHea_DataDictionary
    Set DDO_Server to Customer_DD
    Set DDO_Server to SalesP_DD
    // This is not really needed because this is the default, but 
    // this is added to make the sample clear
    Set pbInheritConstraints to True
End_Object    // OrderHea_DD
   
Object OrderDtl_DD is a OrderDtl_DataDictionary
    Set DDO_Server to OrderHea_DD
    Set DDO_Server to Invt_DD
    Set Constrain_File to OrderHea.File_Number
    // Same as above
    Set pbInheritConstraints to True
End_Object    // OrderDtl_DD
   
On_Key Key_Ctrl+Key_A Send ToggleActive
   
Procedure ToggleActive
    Boolean bCustomerActiveOnly bOk
    Get pbCustomerActiveOnly to bCustomerActiveOnly
    Set pbCustomerActiveOnly to (not(bCustomerActiveOnly))
    Send Rebuild_Constraints of Customer_DD
    Send Rebuild_Constraints of OrderHea_DD
    Send Rebuild_Constraints of OrderDtl_DD
    Send Find of OrderHea_DD FIRST_RECORD 1
End_Procedure
Notice that the ToggleActive procedure is a little different. Instead of sending Rebuild_Constraints to just the Customer_DD we send it to all DDOs that are affected by the conditional change. Since constraints are inherited, that will be the child OrderHea_DD and grandchild OrderDtl_DD. If constraints were not inherited, we’d only have to send Rebuild_Constraints to Customer_DD. Once you understand this, we can simplify this rebuilding all DDO constraints by broadcasting Rebuild_Constraints. It doesn’t hurt to rebuild them all.
Code:
Procedure ToggleActive
    Boolean bCustomerActiveOnly bOk
    Get pbCustomerActiveOnly to bCustomerActiveOnly
    Set pbCustomerActiveOnly to (not(bCustomerActiveOnly))
    Broadcast Send Rebuild_Constraints
    Send Find of OrderHea_DD FIRST_RECORD 1
End_Procedure
Note that we are sending Find to OrderHea_DD and not to Customer_DD. When you change your filters you usually want to find records for the view's main DDO.

If you add the code and run this, it should work as expected. Order number 108 has an inactive customer. When filtered, that order will be skipped. If you want add the Prompt_Callback change to the Order Number DEO and you will see that the Order Lookup List will be filtered.

Optimization of Constraints

So far we’ve not talked about how conditional constraints impacts optimization. There’s no magic here. If you create a constraint condition that cannot be optimized and you have a very large table, your finding may slow down. This is discussed in Constraints and Performance and the same rules all apply – there is nothing special about conditional constraints. In the example we’ve created, the constraints are not optimized. This does not matter because the Customer file is small and most of the Customers are active. If your Customer table was large enough and you had enough inactive Customers, you might see a performance change. Is this an issue? That’s up to you to decide. If it is, we would need to add indexes that could be optimized (e.g., Customer.Status x Customer.Customer_Number) and restrict the DEO finds to those optimized indexes.

If you are inheriting constraints (as we are in our Order example) and there are performance issues there will be no way to optimize the parent constraint because parent constraints can never be optimized. If this creates a performance issue you could choose to not inherit constraints or make the view use a top down finding strategy where you must first find the customer and then find the orders for that customer.

The important point is you need to take these issues into consideration when you are designing your views and not after you discover that this is too slow. You can anticipate this.

Restricting Index Finds

In the prior section I mention that you can restrict your DEO finds to optimized indexes. There are two ways to do this: 1) Set Ordering and 2) Function Field_Index.

Normally a DDO’s Ordering property is set to –1, which means you can find using any index you want. If you change this you can restrict all DEO finds to a single index. You can use this to make sure that you are using an optimized index. Assume that we’ve created a new index (index 4) that is defined as Customer.Status x Customer.Customer_Number and that we want to use this any time the active constraint is in place. You could do something like this:
Code:
Object Customer_DD is a Customer_DataDictionary
    Procedure OnConstrain
        Boolean bCustomerActiveOnly 
        Get pbCustomerActiveOnly to bCustomerActiveOnly
        Set Ordering to (if(bCustomerActiveOnly,4,-1))
        If (bCustomerActiveOnly) Begin
            Constrain Customer.Status eq "Y"
        End
        Forward Send OnConstrain
    End_Procedure
End_Object
Alternately, you could augment Field_Index in your DDO to return the appropriate index for the passed field number. Normally, this function looks at the table’s definition and returns the best index for the passed field or –1 if there is no good index. You could augment that to return different indexes depending on what filtering is in place.
Code:
Function Field_Index Integer iField Returns Integer
    Integer iIndex
    Boolean bCustomerActiveOnly
    Get pbCustomerActiveOnly to bCustomerActiveOnly
    Forward Get Field_Index iField to iIndex
    // if there is a good index but we are filtering, force it to be
    // our one good index.
    If (iIndex<>-1 and bCustomerActiveOnly) Begin
        Move 4 to iIndex
    End         
    Function_Return iIndex
End_Function
While these two examples work the same, Field_Index could be extended to handle more complicated situations where multiple indexes might be available for a filtered find (e.g., Customer.Status x Customer.Customer_Number and Customer.Status x Customer.Name x Customer.Customer_Number) and you would return different indexes based on the filters and the field.

What to do after you’ve rebuilt the constraints

I mentioned earlier that after you’ve changed and rebuilt the constraints you want to make sure you are not leaving invalid records in your DDOs. In this sample, we took the simplest approach to handling this, which is to just find the first valid record. This will always work. Alternately, you can just clear the DDO (send Clear or Send Clear_All). This works well if your main entry controls are non grids. You usually don’t want to clear a grid.

You could refine this process so that you only change the DDO if the new constraint renders the current record invalid. The message Validate_Constraints could be used as follows:
Code:
Procedure ToggleActive
   Boolean bCustomerActiveOnly bOk
   Get pbCustomerActiveOnly to bCustomerActiveOnly
   Set pbCustomerActiveOnly to (not(bCustomerActiveOnly))
   Broadcast Send Rebuild_Constraints
    Get Validate_Constraints of OrderHea_DD to bOk
   If (not(bOk)) Begin
       Send Find of OrderHea_DD FIRST_RECORD 1
    End
End_Procedure
This technique will not work properly with grids. A grid contains additional rows of records and some of those may no longer be valid. With grids you could refind the valid record, which will update your other rows.
Code:
Procedure ToggleActive
    Boolean bCustomerActiveOnly bOk
    Integer iFile
    RowId riId
    Get pbCustomerActiveOnly to bCustomerActiveOnly
    Set pbCustomerActiveOnly to (not(bCustomerActiveOnly))
    Broadcast Send Rebuild_Constraints
    Get Validate_Constraints of OrderHea_DD to bOk
    If (not(bOk)) Begin
        Send Find of OrderHea_DD FIRST_RECORD 1
    End
    Else Begin
        Get Main_File of OrderHea_DD to iFile
        Get CurrentRowId of OrderHea_DD to riId
        Send FindByRowId of OrderHea_DD iFile riId
    End
End_Procedure
You could also try to make the find after an invalid record smarter so that it tries to find the record relative to the invalid record. This all starts to get complicated and can lead to cases of being "too clever for your own good". For starters, just make sure that you are not leaving an invalid record in your DDO.

There are a lot of useful techniques described here. Now go dress up your application!
Tags: john tuohy
Categories
Uncategorized

Comments