Using Expressions and Properties in Constraints
by
, 14-Aug-2009 at 08:00 AM (7499 Views)
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:Let’s explore the use of Constrain As to call DataFlex functions and uncover one of the little secrets of inherited constraints.You constrain against an expression, and the expression can be anything at all, including function calls executing DataFlex code and everything.
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:
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.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
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:
Each time a record is found, this internal expression is applied to the record. If it returns false the record is filtered.Code:( (Customer.Name>"D") and (IsEvenNumber(Self,Customer.Customer_Number) )
[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:
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.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
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:
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.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) )
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.
Constraints and PropertiesCode: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
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:
A Final QuizCode:// 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
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:
1. Which of the above constrain structures may result in errors?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
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.