View RSS Feed

Development Team Blog

Using Expressions and Properties in Constraints

Rate this Entry
In Sonny Falk’s blog about constraints and local variables he noted that you should never use local variables inside of a Constrain As expression. In talking about Constrain As he states:
You constrain against an expression, and the expression can be anything at all, including function calls executing DataFlex code and everything.
Let’s explore the use of Constrain As to call DataFlex functions and uncover one of the little secrets of inherited constraints.

Constraints and User Functions

We will start with a simple problem. Let’s say you’ve decided that the order entry system would be much better if you only displayed even numbered customers whose name is greater than "D". We will start with the Customer entry view in the Order Entry Sample and add a constraint to the Customer data dictionary object as follows:
Code:
Object Customer_DD is a Customer_DataDictionary
 
 Function IsEvenNumber Integer iNum Returns Boolean
     Function_Return (Number(iNum/2) = iNum/2.0)
 End_Function
 
 Procedure OnConstrain
     Forward Send OnConstrain
     Constrain  Customer.Name Gt "D"
     Constrain Customer As (IsEvenNumber(Self,Customer.Customer_Number))            
 End_Procedure
 
End_Object
Now in this case we really did not need to create a separate function to perform this check, but we are doing this as an example of using the Constrain As expression to call a function that executes additional DataFlex code.

Let’s review how this works. When the DDOs become in-use (i.e., are activated) the message Rebuild_Constraints is sent to the DD objects. Rebuild_Constraints builds an internal expression to be evaluated for each found record. Rebuild_Constraints creates this expression by sending OnConstrain to itself and to the DDO's ancestors (i.e., all upward DDOs that are connected via Set DDO_Server). Inside of OnConstrain each Constrain command that is executed will add that constraint to the DDO’s internal expression. In our example we have a single DDO with two Constrain commands so the internal expression for the Customer DD will be something like this:
Code:
( (Customer.Name>"D") and (IsEvenNumber(Self,Customer.Customer_Number) )
Each time a record is found, this internal expression is applied to the record. If it returns false the record is filtered.

[Minor technical note: It's actually a little more complicated. The constraints are split up into multiple internal expressions and the constraint engine determines when and how these expressions are applied. This manages things like jump-in/jump-out, pre-relate/post-relate, etc. Ok, so maybe its actually a lot more complicated but for the purpose of this discussion that specific detail is irrelevant, so we'll pretend it's all just one internal expression to make it easier.]

If you add this to your Customer Entry view it will work as expected. So far - no surprises.

Now let's add the exact same constraint to the Customer DD in the Order Entry view. To do this, we will replace the Order Entry view’s DDO structure with the following:
Code:
 
Object Vendor_DD is a Vendor_DataDictionary
End_Object
 
Object Invt_DD is a Invt_DataDictionary
    Set DDO_Server to Vendor_DD
End_Object
 
Object Customer_DD is a Customer_DataDictionary
 
 Function IsEvenNumber Integer iNum Returns Boolean
     Function_Return (Number(iNum/2) = iNum/2.0)
 End_Function
 
 Procedure OnConstrain
     Forward Send OnConstrain
     Constrain Customer.Name GT "D"
     Constrain Customer as (IsEvenNumber(self,Customer.Customer_Number))            
 End_Procedure
 
End_Object
 
Object SalesP_DD is a Salesp_DataDictionary
End_Object
 
Object OrderHea_DD is a OrderHea_DataDictionary
    Set DDO_Server to Customer_DD
    Set DDO_Server to SalesP_DD
End_Object
 
Object OrderDtl_DD is a OrderDtl_DataDictionary
    Set DDO_Server to OrderHea_DD
    Set DDO_Server to Invt_DD
End_Object
Surprise! If you try running this you may not be pleased with the results. Every time you try to find an order you will get many unhandled errors stating “Invalid Message. GET_ISEVENNUMBER”. Darn.

What happened?

The answer lies in the inheritance of parent DDO constraints. Remember we said that sending Rebuild_Constraints to a DDO builds an internal constraint expression by sending OnConstrain to itself and to its DDO ancestors. When you have multiple DDOs each DDO will have its own internal expression made up of constraints from itself and its parent DDOs. In the case of Order Entry view the DDOs will have the following constraints:
Code:
 
Vendor_DD: none
 
Invt_DD: none
 
Customer_DD: ( (Customer.Name>"D") and (IsEvenNumber(Self,Customer.Customer_Number) )
 
SalesP_DD: none
 
OrderHea_DD: ( (Customer.Name>"D") and (IsEvenNumber(Self,Customer.Customer_Number) ) 
 
OrderDtl_DD: ( (OrderDtl relates to OrderHea) and (Customer.Name>"D") and (IsEvenNumber(Self,Customer.Customer_Number) )
OrderHea_DD inherited the constraints from Customer_DD. OrderDtl_DD inherited the constraints from OrderHea_DD which includes the constraints inherited from Customer_DD. This means that any time an OrderHea record is found it will apply a constraint expression that includes constraints from its parent Customer. OrderDtl will apply a constraint expression that includes constraints from OrderHea, Customer and SalesP. The important piece of information here is that constrains are inherited and this inheritance occurs when the constraint expression is being built during Rebuild_Constraints. Here's the other important piece of information. If any of those constraints contain references to self the value of self may not be what you expect. In this case, IsEvenNumber(Self,Customer.Customer_Number) is being evaluated when self is OrderHea or OrderDtl. In these cases it cannot find the Customer_DD's IsEvenNumber function in that object or anywhere in that object’s delegation path. The result is errors and lots of them.

Once you understand the problem there are multiple ways to solve this. The simplest approach is to move the function IsEvenNumber out one level so it can be found via delegation by all DDOs.
Code:
 
Function IsEvenNumber Integer iNum Returns Boolean
 Function_Return (Number(iNum/2) = iNum/2.0)
End_Function
 
Object Vendor_DD is a Vendor_DataDictionary
End_Object
 
Object Invt_DD is a Invt_DataDictionary
    Set DDO_Server to Vendor_DD
End_Object
 
Object Customer_DD is a Customer_DataDictionary
 
 Procedure OnConstrain
     Forward Send OnConstrain
     Constrain Customer.Name GT "D"
     Constrain Customer as (IsEvenNumber(self,Customer.Customer_Number))            
 End_Procedure
 
End_Object
 
Object SalesP_DD is a Salesp_DataDictionary
End_Object
 
Object OrderHea_DD is a OrderHea_DataDictionary
    Set DDO_Server to Customer_DD
    Set DDO_Server to SalesP_DD
End_Object
 
Object OrderDtl_DD is a OrderDtl_DataDictionary
    Set DDO_Server to OrderHea_DD
    Set DDO_Server to Invt_DD
End_Object
Constraints and Properties

This above applies to any self reference inside of a Constrain As expression including properties. If a property is used within a Constrain As expression it needs to be reachable by all DDOs. For example:
Code:
 
// property is created outside of the DDO
Property Boolean pbActiveCustOnly True
 
Object Customer_DD is a Customer_DataDictionary
 
     Procedure OnConstrain
        Forward Send OnConstrain
        Constrain  Customer as (not(pbActiveCustOnly(self)) or Customer.Status="Y")
     End_Procedure
 
End_Object
A Final Quiz

Keep in mind that this only applies to Constrain As and it only applies to code within the Constrain As expression. To see if you really understand this consider the following samples:
Code:
 
// A)
//
Object Customer_DD is a Customer_DataDictionary
 
    Property Boolean pbActiveCustOnly True
 
    Procedure OnConstrain
        Boolean bActiveOnly
        Get pbActiveCustOnly to bActiveOnly
        Forward Send OnConstrain
        If (bActiveOnly) Begin
            Constrain  Customer.Status eq "Y"
        End
    End_Procedure
 
End_Object
 
 
// B)
//
Object Customer_DD is a Customer_DataDictionary
 
    Property Boolean pbActiveCustOnly True
 
    Procedure OnConstrain
        Boolean bActiveOnly
        Get pbActiveCustOnly to bActiveOnly
        Forward Send OnConstrain
        If (bActiveOnly) Begin
            Constrain  Customer as (Customer.Status="Y")
        End
    End_Procedure
 
End_Object
 
 
// C)
//
Object Customer_DD is a Customer_DataDictionary
 
    Property Boolean pbActiveCustOnly True
 
    Procedure OnConstrain
        Forward Send OnConstrain
        Constrain  Customer as (not(pbActiveCustOnly(self)) or Customer.Status="Y")
    End_Procedure
 
End_Object
 
 
// D)
//
Property Boolean pbActiveCustOnly True
 
Object Customer_DD is a Customer_DataDictionary
 
    Procedure OnConstrain
        Forward Send OnConstrain
        Constrain  Customer as (not(pbActiveCustOnly(self)) or Customer.Status="Y")
    End_Procedure
 
End_Object
1. Which of the above constrain structures may result in errors?

2. Assuming a proper index is supplied, which of the above constrain structures will be the most optimal?

3. Example A or B may exhibit a different behavior than example D. Ignoring performance issues, explain what that difference is?


There is a lot more to discuss. You don’t always want constraints to be inherited and there is a way to disable this (pbInheritConstraints). Using properties opens up the ability to create dynamic constraints. As soon as you get involved in that you need to know how and when to manually rebuild constraints. The short answer is to broadcast send Rebuild_Constraints to all DDOs. The longer answer, which will explain why you should do this, is a topic for a different day.

Updated 14-Aug-2009 at 08:36 AM by Stephen W. Meeley (Minor formatting.)

Categories
Uncategorized

Comments