Skip to main content
This post is dedicated to Attribute Adjuster which is a useful Acumatica helper that allows you to more elegantly adjust attribute's state in runtime.
 

Problem Statement

 

A lot of business logic in Acumatica is implemented in attributes. Sometimes, the state of attribute should be adjusted in runtime, for example, in the graph row selected event like this:

protected virtual void _(Events.RowSelected<MyDac> e)
{
PXUIFieldAttribute.SetVisible<MyDac.balance>(e.Cache, e.Row, e.Row.Balance > 0);
}

The old approach used by Acumatica attributes to allow developers to adjust their state at runtime was to declare a static method in the attribute. Such methods frequently have very similar implementation. Here is an example:

public class PXSomeAttribute : PXEventSubscriberAttribute
{
public bool Flag{ get; set; }

public static void SetFlag<TField>(PXCache cache, object row, bool newFlagValue)
where TField : IBqlField
{
if (row == null)
cache.SetAltered<TField>(true);

var attributes = cache.GetAttributes<TField>(row).OfType<PXSomeAttribute>();

foreach (PXSomeAttribute attr in attributes)
attr.Flag = newFlagValue;
}
}

Then the property of the PXSomeAttribute attribute may be adjusted like this:

PXSomeAttribute.SetFlag<myField>(sender, e.Row, true);

However, this approach has several issues:

  • The attribute must have a separate wrapper for every property that need to be adjusted at runtime. This leads to the code duplication and boilerplate static methods.
  • Only properties with wrappers may be adjusted.
  • Called static methods sometimes may contain unexpected logic or lack some necessary logic. The calling code have very little control over how the attribute’s state is adjusted.
  • The attribute itself is hidden at the caller side, so adjusting an instance state via a static method call may be confusing for newbies.

Attribute Adjuster

 

To address the issues listed above Acumatica provides to developers a more general and centralized mechanism called attribute adjuster. It allows you to specify in a declarative manner how the attribute’s state should be adjusted.

Suppose, that you want to adjust attributes declared on DAC fields of SomeDAC DAC. The attribute adjuster mechanism is used in two or optionally three steps:

  1. You need to obtain the graph cache corresponding to SomeDAC DAC. Then you should call the Adjust  extension method from the PX.Data namespace on the obtained cache and specify the type of attribute you want to adjust:
    var adjuster = cache.Adjust<PXUIFieldAttribute>(e.Row)
    The method will return a special structure AttributeAdjuster<TAttribute> that is used to configure the attribute’s state. The TAttribute generic type parameter is replaced with the attribute type specified in the call.

    There are also other additional helper methods that are used as shortcuts for the Adjust method call for a specific attribute (usually, it is PXUIFieldAttribute) with a specific set of arguments. Describing all those extension methods for PXCache is out of scope of this post, but you can easily investigate them on your own. Their names start with the Adjust prefix.
  2. You need to call either For or ForAllFields method of the AttributeAdjuster<TAttribute> structure returned by the call to the Adjust method from the previous step. These methods accept a delegate with one parameter - the attribute instance. In the delegate you declare how the attribute’s instance passed to the delegate should be changed. The run-time will apply these changes to all instances of the attribute type specified in the previous step.

    For the For method developers must additionally provide the DAC field either via generic type parameter, or a string with DAC field name. The attributes from this field will be adjusted by the runtime with the changes specified in the delegate:
    var adjuster = cache.Adjust<PXUIFieldAttribute>(e.Row);
    var chained = adjuster.For<MyDac.MyField>(attribute => attribute.Enabled = false);
    As you can guess, the ForAllFields method does not need a DAC field. The changes specified in the delegate are applied to all DAC fields:
    var adjuster = cache.Adjust<PXUIFieldAttribute>(e.Row);
    var chained = adjuster.ForAllFields(attribute => attribute.Enabled = true);
    For or ForAllFields methods return special AttributeAdjuster<TAttribute>.Chained structure that can be optionally used to adjust the same attribute type for other DAC fields.
  3. This step is optional, but it is very frequently used when the same attribute should be adjusted for several DAC fields. You can call the For or ForAllFields methods of the AttributeAdjuster<TAttribute>.Chained structure from the previous step to specify how the attribute should be adjusted for other DAC fields. Everything is the same as in the step 2.

    However, there is also the SameFor method that only accepts the DAC field which can be passed either as a generic type parameter, or a string with DAC field name. This method is used to apply the changes specified by the previous call to the For or ForAllFields method to another DAC field:
    var adjuster = cache.Adjust<PXUIFieldAttribute>(e.Row);
    var chained = adjuster.For<MyDac.MyField>(attribute => attribute.Enabled = false);
    var chained = chained.SameFor<MyDac.MyOtherField>();

In the examples above I deliberately declared a separate local variable for the result of every call to make the usage of the helper structures more explicit. However, you should not write the code like this in practice. Attribute adjuster APIs were designed to be used in the fluent style where method calls are chained. This results in the declarative code which is in my opinion more readable. Here is an example:

cache.Adjust<PXUIFieldAttribute>(e.Row)
.ForAllFields(attribute => attribute.Enabled = true)
.For<MyDac.myField>(attribute => attribute.Enabled = false);
.SameFor<MyDac.myOtherField>();

// Make all DAC fields enabled first.
// Then make MyField disabled.
// Then make MyOtherField also disabled by applying changes from the previous For call

Benefits of the attribute adjuster approach:

  • There is no need to write any static wrapper methods for the attribute.
  • Developers can adjust every accessible attribute's property and field.
  • Logic is written in a clear and declarative manner.  It is impossible to have some hidden details.
  • An attribute instance is the only parameter of the delegate passed to the For and ForAllFields methods,
  • It is possible to reuse the common adjusting logic for several DAC fields with the SameFormethod.

Old Approach vs Attribute Adjuster

 

Let’s compare the old approach with the attribute adjuster on some real life example. A classic case of attribute's state adjusting could be found in RowSelected graph event handlers. Here is the old approach:

PXUIFieldAttribute.SetEnabled(cache, doc, true);
PXUIFieldAttribute.SetEnabled<SOOrder.refTranExtNbr>(cache, doc,
(doc.CreatePMInstance == true || doc.PMInstanceID.HasValue) &&
isPMCreditCard && isCashReturn && !isCCRefunded);
PXUIFieldAttribute.SetEnabled<SOOrder.status>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.orderQty>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.orderWeight>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.orderVolume>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.packageWeight>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyOrderTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyUnpaidBalance>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyLineTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyMiscTot>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyFreightCost>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.freightCostIsValid>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyFreightAmt>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyTaxTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.openOrderQty>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyOpenOrderTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyOpenLineTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyOpenTaxTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.unbilledOrderQty>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyUnbilledOrderTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyUnbilledLineTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyUnbilledTaxTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyPaymentTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.cCPaymentStateDescr>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.pCResponseReasonText>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.captureTranNumber>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyCCCapturedAmt>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.origOrderType>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.origOrderNbr>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyVatExemptTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyVatTaxableTotal>(cache, doc, false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyID>(cache, doc, curyenabled);
PXUIFieldAttribute.SetEnabled<SOOrder.preAuthTranNumber>(cache, doc, enableCCAuthEntering);
PXUIFieldAttribute.SetEnabled<SOOrder.cCAuthExpirationDate>(cache, doc,
enableCCAuthEntering && String.IsNullOrEmpty(doc.PreAuthTranNumber) == false);
PXUIFieldAttribute.SetEnabled<SOOrder.curyCCPreAuthAmount>(cache, doc,
enableCCAuthEntering && String.IsNullOrEmpty(doc.PreAuthTranNumber) == false);

As you can see there is a lot of code duplication. The most obvious one is an attribute's name, that is mentioned in every single line. Another code duplication is cache and row parameters, and an expression that is passed in the each call.

Attribute adjuster allows to simplify the adjusting by mentioning cache, attribute and row only once in the very beginning:

cache
.Adjust<PXUIFieldAttribute>(doc)
.ForAllFields(a => a.Enabled = true)
.For<SOOrder.refTranExtNbr>(
a => a.Enabled = (doc.CreatePMInstance == true || doc.PMInstanceID.HasValue) &&
isPMCreditCard && isCashReturn && !isCCRefunded)
.For<SOOrder.status>(a => a.Enabled = false)
.For<SOOrder.orderQty>(a => a.Enabled = false)
.For<SOOrder.orderWeight>(a => a.Enabled = false)
.For<SOOrder.orderVolume>(a => a.Enabled = false)
.For<SOOrder.packageWeight>(a => a.Enabled = false)
.For<SOOrder.curyOrderTotal>(a => a.Enabled = false)
.For<SOOrder.curyUnpaidBalance>(a => a.Enabled = false)
.For<SOOrder.curyLineTotal>(a => a.Enabled = false)
.For<SOOrder.curyMiscTot>(a => a.Enabled = false)
.For<SOOrder.curyFreightCost>(a => a.Enabled = false)
.For<SOOrder.freightCostIsValid>(a => a.Enabled = false)
.For<SOOrder.curyFreightAmt>(a => a.Enabled = false)
.For<SOOrder.curyTaxTotal>(a => a.Enabled = false)
.For<SOOrder.openOrderQty>(a => a.Enabled = false)
.For<SOOrder.curyOpenOrderTotal>(a => a.Enabled = false)
.For<SOOrder.curyOpenLineTotal>(a => a.Enabled = false)
.For<SOOrder.curyOpenTaxTotal>(a => a.Enabled = false)
.For<SOOrder.unbilledOrderQty>(a => a.Enabled = false)
.For<SOOrder.curyUnbilledOrderTotal>(a => a.Enabled = false)
.For<SOOrder.curyUnbilledLineTotal>(a => a.Enabled = false)
.For<SOOrder.curyUnbilledTaxTotal>(a => a.Enabled = false)
.For<SOOrder.curyPaymentTotal>(a => a.Enabled = false)
.For<SOOrder.cCPaymentStateDescr>(a => a.Enabled = false)
.For<SOOrder.pCResponseReasonText>(a => a.Enabled = false)
.For<SOOrder.captureTranNumber>(a => a.Enabled = false)
.For<SOOrder.curyCCCapturedAmt>(a => a.Enabled = false)
.For<SOOrder.origOrderType>(a => a.Enabled = false)
.For<SOOrder.origOrderNbr>(a => a.Enabled = false)
.For<SOOrder.curyVatExemptTotal>(a => a.Enabled = false)
.For<SOOrder.curyVatTaxableTotal>(a => a.Enabled = false)
.For<SOOrder.curyID>(a => a.Enabled = curyenabled)
.For<SOOrder.preAuthTranNumber>(a => a.Enabled = enableCCAuthEntering)
.For<SOOrder.cCAuthExpirationDate>(
a => a.Enabled = enableCCAuthEntering &&
String.IsNullOrEmpty(doc.PreAuthTranNumber) == false)
.For<SOOrder.curyCCPreAuthAmount>(
a => a.Enabled = enableCCAuthEntering &&
String.IsNullOrEmpty(doc.PreAuthTranNumber) == false);

But that’s not the end! The code above can be simplified even more by using the SameFor method that reuses the previously defined delegate to adjust the same attribute of another field:

cache
.Adjust<PXUIFieldAttribute>(doc)
.ForAllFields(a => a.Enabled = true)
.For<SOOrder.refTranExtNbr>(
a => a.Enabled = (doc.CreatePMInstance == true || doc.PMInstanceID.HasValue) &&
isPMCreditCard && isCashReturn && !isCCRefunded)
.For<SOOrder.status>(a => a.Enabled = false)
.SameFor<SOOrder.orderQty>()
.SameFor<SOOrder.orderWeight>()
.SameFor<SOOrder.orderVolume>()
.SameFor<SOOrder.packageWeight>()
.SameFor<SOOrder.curyOrderTotal>()
.SameFor<SOOrder.curyUnpaidBalance>()
.SameFor<SOOrder.curyLineTotal>()
.SameFor<SOOrder.curyMiscTot>()
.SameFor<SOOrder.curyFreightCost>()
.SameFor<SOOrder.freightCostIsValid>()
.SameFor<SOOrder.curyFreightAmt>()
.SameFor<SOOrder.curyTaxTotal>()
.SameFor<SOOrder.openOrderQty>()
.SameFor<SOOrder.curyOpenOrderTotal>()
.SameFor<SOOrder.curyOpenLineTotal>()
.SameFor<SOOrder.curyOpenTaxTotal>()
.SameFor<SOOrder.unbilledOrderQty>()
.SameFor<SOOrder.curyUnbilledOrderTotal>()
.SameFor<SOOrder.curyUnbilledLineTotal>()
.SameFor<SOOrder.curyUnbilledTaxTotal>()
.SameFor<SOOrder.curyPaymentTotal>()
.SameFor<SOOrder.cCPaymentStateDescr>()
.SameFor<SOOrder.pCResponseReasonText>()
.SameFor<SOOrder.captureTranNumber>()
.SameFor<SOOrder.curyCCCapturedAmt>()
.SameFor<SOOrder.origOrderType>()
.SameFor<SOOrder.origOrderNbr>()
.SameFor<SOOrder.curyVatExemptTotal>()
.SameFor<SOOrder.curyVatTaxableTotal>()
.For<SOOrder.curyID>(a => a.Enabled = curyenabled)
.For<SOOrder.preAuthTranNumber>(a => a.Enabled = enableCCAuthEntering)
.For<SOOrder.cCAuthExpirationDate>(
a => a.Enabled = enableCCAuthEntering &&
String.IsNullOrEmpty(doc.PreAuthTranNumber) == false)
.SameFor<SOOrder.curyCCPreAuthAmount>();

That’s all about attribute adjuster mechanism. Thank you for your attention!

Thank you for sharing this tip with the community @snikomarov36!


Thank you @snikomarov36 great summary.


Reply