Hi Community,
This post is my attempt to make a comprehensive compilation of all I know about Acumatica projection DACs and all materials I managed to find about them. The post turned out rather lengthy, use the table of contents to navigate to a part you want to read.
Overview
The PXProjectionAttribute
(also referred to as the projection) binds the DAC to an arbitrary data set defined by a BQL Select command. You can think of DACs marked by the PXProjection
attribute as the Acumatica Framework’s version of SQL views. The attribute defines a named view implemented by the server side rather than the database.
The projection attribute is placed on the DAC class declaration. The DAC with the projection attribute is frequently called projection DAC or projection. The framework doesn't bind such DAC to a database table—that is, it doesn't select data from the table having the same name as the DAC. Instead, you specify an arbitrary BQL Select
command (with any BQL clauses) in the attribute's declaration on a DAC that is executed to retrieve data for the DAC:
[PXProjection(typeof(
SelectFrom<Supplier>.
InnerJoin<SupplierProduct>.On<
SupplierProduct.accountID.IsEqual<Supplier.accountID>>))]
public class SupplierAccounts: PXBqlTable
{}
This BQL Select
command is sometimes called the projection query.
Constructors and Important Properties
Before start with a short overview of PXProjectionAttribute
important APIs: There are two PXProjectionAttribute
constructors:
public PXProjectionAttribute(Type select)
. Theselect
parameter is the BQL command that defines the data set, based on theSelect
class or any other class that implementsIBqlSelect
.public PXProjectionAttribute(Type select, Type[] persistent)
. The first parameter,select
, was described previously. Thepersistent
parameter is the list of DACs that represent the tables that should be updated during the persistence of the projection DAC to the database.
There is also important Persistent
property:
public virtual bool Persistent { get; set; }
This property indicates whether the instances of projection DAC can be saved to the database. If the property equals true
, the attribute parses the Select
command and determines the tables that should be updated. Alternatively, you can pass the list of tables to the corresponding attribute's constructor. By default, the property equals false
so the DAC is read-only.
Projection DAC Differences From Regular DAC
A projection DAC declaration is very similar to declarations of normal DACs. You need to create a DAC class, starting from Acumatica 2024 R1 it should be derived from the PXBqlTable
class, or implement the IBqlTable
interface, if you use an older version of Acumatica. The projection DAC has the same DAC fields as the normal DAC. I would like to remind you that by DAC field we understand a pair like this:
#region DacField
// This public abstract class is usually called *DAC BQL field* and represents the DAC field in BQL queries,
// and consumed by some Platform APIs. This class has the same name as the C# property but in camelCase.
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }
// This C# property is usually called *DAC field property* (also called *DAC property* for brevity).
[PXDBString(50, IsUnicode = true)]
public virtual string DacField{ get; set; }
#endregion
However, projection DACs also have several important differences.
First of all, the name of the projection DAC is not bound to any DB table by the Acumatica Framework. Therefore, you can use an arbitrary name for your projection DAC. Of course, this name is still limited by the restrictions applied to class names by C#.
Second, you need to declare PXProjectionAttribute
on the DAC class declaration which was already mentioned before.
Finally, projection DACs do not declare regular DAC fields bound to database table columns. They can declare three kinds of DAC fields:
- Mapped DAC fields. These are DAC fields that have their values retrieved from other DACs by the BQL query specified in the projection attribute (the projection query).
- Unbound DAC fields. These are fields that are calculated on the fly in RAM on the server side.
- DAC fields with
PXDBScalar
andPXDBCalced
attributes. These fields are calculated on the database side on the fly.
Let's take a closer look at each of these field types.
Mapped DAC Fields
Mapped DAC fields are the fields that you most often see in projection DACs. Quite many projection DACs have only these fields. The mapping here means that the mapped DAC field is associated with some other DAC field from another DAC and the mapped DAC field's value is taken from that original DAC field. The original DAC field should belong to one of DACs retrieved by the projection query.
The term original DAC field will be used in several places later in the text with the same meaning together with the term projection DAC field. Both these terms come from the mapping described above. The projection DAC field is declared on the projection DAC and the framework obtains its value via mapping from the original DAC field which is declared on the original DAC which is one of the DACs retrieved by the projection query.
The attributes declared on the mapped DAC field property can differ from attributes on the original DAC field property. But data type attributes that configure the type of the original DAC field's value (such as PXDBInt
or PXDBString
) are usually copied to the mapped DAC field:
// Original DAC field property
[PXDBString(10, IsUnicode = true)] // Data type attribute
[PXUIField(DisplayName = "Payment Method", Visibility = PXUIVisibility.Visible)]
[PXSelector(...)]
[PXDefault(...)]
public virtual string PayTypeID { get; set; }
...
// Mapped DAC field property
[PXDBString(10, IsUnicode = true, BqlField = typeof(APInvoice.payTypeID))] // Data type attribute is the same except BqlField part
[PXUIField(DisplayName = "Payment Method", Visibility = PXUIVisibility.Visible)]
public virtual string PayTypeID { get; set; }
To declare a mapping for the mapped DAC field you need to specify the original DAC field which will provide values to it. There are three ways to do this:
BqlField
binding,BqlTable
binding,-
Auto-binding by inheritance.
Let's look at each of these approaches.
BqlField Binding
The BqlField
binding is the most popular way to define a mapping for a projection DAC field. To use it, you set the BqlField
property of the data type attribute (such as PXDBString
and PXDBDecimal
) that binds the field to the database, as follows:
[PXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : PXBqlTable
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}
[PXDBInt(IsKey = true, BqlField = typeof(Contact.contactID))] // mapping with Contact.СontactID
public virtual int? ContactID { get; set; }
#endregion
#region AddressLine1
public abstract class addressLine1 : PX.Data.BQL.BqlString.Field<addressLine1> {}
[PXDBString(50, IsUnicode = true, BqlField = typeof(Address.addressLine1))] // mapping with Address.AddressLine1
public virtual string AddressLine1 { get; set; }
#endregion
...
}
BqlTable Binding
Another way to map a field from projection DAC is to use the BqlTable
property instead of the BqlField
property. With this approach you only specify in the BqlTable
property the DAC class containing the original DAC field. The mapping mechanism will choose the DAC field implicitly by looking in the specified DAC for a DAC field with the same name as the name of the projection DAC field. The previous example can be rewritten with the BqlTable
approach:
[PXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : PXBqlTable
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}
[PXDBInt(IsKey = true, BqlTable = typeof(Contact))] // mapping with Contact.СontactID
public virtual int? ContactID { get; set; }
#endregion
#region AddressLine1
public abstract class addressLine1 : PX.Data.BQL.BqlString.Field<addressLine1> {}
[PXDBString(50, IsUnicode = true, BqlTable = typeof(Address))] //mapping with Address.AddressLine1
public virtual string AddressLine1 { get; set; }
#endregion
...
}
The BqlTable
approach is sensitive to the renaming of the original DAC field because you don't explicitly specify the original field's name in the mapping. If the original DAC field is renamed, then all mapped DAC field should be renamed too. Unfortunately, the standard renaming tools provided by IDEs such as Visual Studio won't rename the mapped DAC fields automatically for you.
The renaming of a DAC field is a rare and quite troublesome activity, its probability is very low. Still, if you wish to completely eliminate this risk, consider using the BqlField
binding which does not have this flaw instead of BqlTable
binding.
Auto-binding by Inheritance
Declaring all mapped DAC fields in the projection DAC can be tedious. The auto-binding by inheritance approach allows you to avoid writing part of the boilerplate code. To use it you need to inherit your projection DAC from one of the DACs in the projection query. Projection DAC can use a DAC field from its base DAC even if it does not contain a corresponding mapped DAC field declaration:
[PXProjection(typeof(Select2<
Contact,
LeftJoin<Address,
On<Contact.bAccountID, Equal<Address.bAccountID>,
And<Contact.defAddressID, Equal<Address.addressID>>>>>))]
public class ContactExtAddress : Address //inherit from Address DAC
{
#region ContactID
public abstract class contactID : PX.Data.BQL.BqlInt.Field<contactID> {}
[PXDBInt(IsKey = true, BqlField = typeof(Contact.contactID))] // mapping with Contact.СontactID
public virtual Int32? ContactID { get; set; }
#endregion
//You don't have to declare fields from Address DAC
...
}
The choice of a base DAC is arbitrary. It can be any DAC from the projection query. However, it would be practical to choose a DAC that would add the largest number of mapped DAC fields to projection DAC. Deriving from this DAC allows you to avoid writing declarations for all those fields.
Unbound DAC Fields
Previously, it was mentioned that you can declare unbound DAC fields in projection DACs. These fields are stored only in the server memory, they are not persisted to the database. You can read more about them from the Acumatica Help Portal:
https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=3f6ee8e9-b29e-4dab-b4f8-4406c3ef101d
Unbound DAC fields are not mapped to DAC fields from DACs retrieved by the projection query. They are usually used in following scenarios:
- Add some temporary state to DAC:
[PXProjection(typeof(Select<Organization>))] public class Organization : PXBqlTable { #region Selected public abstract class selected : PX.Data.BQL.BqlBool.Field<selected> { } [PXBool] [PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)] [PXUIField(DisplayName = "Selected")] public virtual bool? Selected { get; set; } #endregion }
-
Make simple calculations on the server side (usually with
PXFormulaAttribute
):[PXProjection(...))] public class BalancedAPDocument : APRegister { #region VendorRefNbr public abstract class vendorRefNbr : PX.Data.BQL.BqlString.Field<vendorRefNbr> { } [PXString(40, IsUnicode = true)] [PXUIField(DisplayName = "Vendor Ref.")] [PXFormula(typeof(IsNull<BalancedAPDocument.invoiceNbr, BalancedAPDocument.extRefNbr>))] public string VendorRefNbr { get; set; } #endregion }
PXDBScalar
and PXDBCalced
DAC Fields
DAC fields with PXDBScalar
and PXDBCalced
attributes are rather special. They are not bound to any DB table column but rather calculated on the fly on the database side:
- The
PXDBScalar
attribute defines a sub-query that selects the value assigned to the DAC field on which the attribute is specified. - The
PXDBCalced
attribute defines an expression that is translated into SQL. This expression calculates on the DB side the DAC field value from other DAC fields of the same DAC record.
You can read more details in the following article on the Help Portal:
https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=95f32fae-7e43-4998-8c17-4236039a9da9
You can use fields with PXDBScalar
and PXDBCalced
attributes in projection DACs. Similar to unbound DAC fields they are not mapped to DAC fields from DACs retrieved by the projection query:
[PXProjection(typeof(Select<CuryAPHistory>))]
public class CuryAPHistoryTran : PXBqlTable
{
#region FinPtdCrAdjustments
public abstract class finPtdCrAdjustments : PX.Data.BQL.BqlDecimal.Field<finPtdCrAdjustments> { }
[PXDecimal]
[PXDBCalced(typeof(Switch<Case<Where<CuryAPHistory.finPeriodID, Equal<CurrentValue<APHistoryFilter.finPeriodID>>>,
CuryAPHistory.finPtdCrAdjustments>, Zero>), typeof(decimal))]
public virtual decimal? FinPtdCrAdjustments { get; set; }
#endregion
}
CurrentValue
in Projection DACs
Have you noticed the usage of the CurrentValue
BQL parameter in the PXDBCalced
attribute in the previous example?
The CurrentValue
parameter is the equivalent of the Current
parameter for projection DACs. You need to use it instead of Current
in the projection query and in BQL declared in DAC field attributes. If you need the FBQL variant of CurrentValue
you can use Field.FromCurrent.Value
parameter.
Using Projection DACs in Code
Projection DACs can be used in the same places in code as regular DACs. In the following example, the ARTaxTran
projection is used in the BQL query in the data view declaration:
public PXSelectJoin<
ARTaxTran,
LeftJoin<Account,
On<Account.accountID, Equal<ARTaxTran.accountID>>>,
Where<
ARTaxTran.module, Equal<BatchModule.moduleAR>,
And<ARTaxTran.tranType, Equal<Required<ARTaxTran.tranType>>,
And<ARTaxTran.refNbr, Equal<Required<ARTaxTran.refNbr>>>>>,
OrderBy<
Asc<Tax.taxCalcLevel>>>
ARTaxTran_TranType_RefNbr;
A projection DAC can be even used in another projection DAC. Here is the FABookHistoryMax
projection DAC declaration:
[PXProjection(typeof(Select4<
FABookHistory,
Aggregate<
GroupBy<FABookHistory.assetID,
GroupBy<FABookHistory.bookID,
Max<FABookHistory.finPeriodID>>>>>))]
public class FABookHistoryMax : PXBqlTable
{
#region AssetID
public abstract class assetID : PX.Data.BQL.BqlInt.Field<assetID> {}
[PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.assetID))]
public virtual int? AssetID { get; set; }
#endregion
#region BookID
public abstract class bookID : PX.Data.BQL.BqlInt.Field<bookID> {}
[PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.bookID))]
public virtual int? BookID { get; set; }
#endregion
#region FinPeriodID
public abstract class finPeriodID : PX.Data.BQL.BqlString.Field<finPeriodID> {}
[GL.FinPeriodID(BqlField = typeof(FABookHistory.finPeriodID))]
public virtual string FinPeriodID { get; set; }
#endregion
}
Now we will use the FABookHistoryMax
projection DAC in another projection DAC declaration:
[PXProjection(typeof(Select2<
FABookHistoryMax,
InnerJoin<FABookHistory,
On<FABookHistoryMax.assetID, Equal<FABookHistory.assetID>,
And<FABookHistoryMax.bookID, Equal<FABookHistory.bookID>,
And<FABookHistoryMax.finPeriodID, Equal<FABookHistory.finPeriodID>>>>,
InnerJoin<FABook,
On<FABook.bookID, Equal<FABookHistory.bookID>>>>>))]
public class FABookHistoryRecon : PXBqlTable
{
#region AssetID
public abstract class assetID : PX.Data.BQL.BqlInt.Field<assetID> {}
[PXDBInt(IsKey = true, BqlField = typeof(FABookHistory.assetID))]
public virtual int? AssetID { get; set;}
#endregion
#region UpdateGL
public abstract class updateGL : PX.Data.BQL.BqlBool.Field<updateGL> {}
[PXDBBool(BqlField = typeof(FABook.updateGL))]
public virtual bool? UpdateGL { get; set;}
#endregion
...
}
Rules for Projection DAC Declaration
Projection DAC declarations must follow several rules:
- All DAC fields specified in
BqlField
mappings for mapped DAC fields of projection DACs must belong to DACs present in the projection query. Fields from other DACs will break the projection DAC and won't work. In short, you can only references fields form the dataset retrieved by the projection query.
This rule seems obvious, but it can be easy to break when you are copy pasting DAC fields of your projection DAC from another similar projection DAC. If you are copying 30 DAC fields between projection DACs, it's easy to forget to correctBqlField
mapping somewhere. - The same rule is true for
BqlTable
mapping. All DACs specified inBqlTable
mappings for mapped DAC fields of projection DACs must belong to DACs present in the projection query. - The projection query and BQL queries declared in the projection DAC should use the
CurrentValue
BQL parameter instead of theCurrent
BQL parameter. For FBQL queries you can use thefield.FromCurrent.Value
FBQL parameter. - You can pass to the projection query only
Select
queries derived from thePX.Data.SelectBase
classes that implement theIBqlSelect<Table>
interface. The passed query should be derived from theBqlCommand
class.
You can't passPXSelectBase
derived types to the projection attribute! It will cause runtime error!
- If projection DAC field specifies multiple
BqlField
andBqlTable
mappings via different attributes declared on it, then DAC fields and DACs specified in these mappings should all be the same.
This is a very rare scenario that was encountered only a couple of times in our experience. The general recommendation is to avoid such situations.
Unfortunately, these rules are not checked by Acuminator currently. But we have tasks in the Acuminator backlog to add them.
Persist Data with Projection DACs
By default, projection DACs are read-only. This means that any change made to them won't be saved to the database. However, you can make a projection DAC editable by setting the Persistent
property of PXProjectionAttribute
to true
. The attribute uses the Select
BQL command passed to it (the projection query) to determine which tables should be updated.
However, only the first table referenced by the Select
command is updated by default. If the data should be committed not only to the main table, but also to the joined tables, the fields that connect the tables must be marked with PXExtraKeyAttribute
, as shown in the following example:
[PXProjection(typeof(
Select2<ContractWatcher,
RightJoin<Contact,
On<Contact.contactID, Equal<ContractWatcher.contactID>>>>),
Persistent = true)] //make the ContractWatcher projection DAC updatable by default
public class SelContractWatcher : ContractWatcher
{
...
#region ContactContactID
public abstract class contactContactID : PX.Data.BQL.BqlInt.Field<contactContactID> {}
// mark that Contact DAC will be updated too on update of SelContractWatcher projection DAC,
// without this attribute only ContractWatcher DAC will be updatable
[PXExtraKey]
[PXDBInt(BqlField = typeof(Contact.contactID))]
public virtual int? ContactContactID { get; set; }
#endregion
...
}
SelContractWatcher
DAC can be updated somewhere in the graph with the following code:
var watchers = listWatchers.Cast<SelContractWatcher>()
.Select(item => (SelContractWatcher)Watchers.Cache.CreateCopy(item));
...
foreach (SelContractWatcher watcher in watchers)
{
watcher.ContractID = newContract.ContractID;
//ContractWatcher and Contact DACs will be updated
graph.Watchers.Insert(watcher);
}
Additionally, you can use the PXProjection
attribute's constructor with two parameters to explicitly provide the list of editable DACs:
public PXProjectionAttribute(Type select, Type[] persistent)
The persistent
parameter should contain DACs referenced by the Select
command. This constructor can be used to make editable only a part of all DACs specified in the projection query. The constructor also sets the Persistent
property to true
. Here is an example:
// Define projection with explicitly specified editable tables
[PXProjection(typeof(Select2<
SOLineSplit,
InnerJoin<SOOrderType,
On<SOOrderType.orderType, Equal<SOLineSplit.orderType>>,
InnerJoin<SOOrderTypeOperation,
On<SOOrderTypeOperation.orderType, Equal<SOLineSplit.orderType>,
And<SOOrderTypeOperation.operation, Equal<SOLineSplit.operation>>>>>>),
persistent: new Type[] { typeof(SOLineSplit), typeof(SOOrderType) })] //list of editable DACs
public class SOLineSplit2 : PXBqlTable
{
#region SOOrderType
public abstract class sOOrderType : PX.Data.BQL.BqlString.Field<sOOrderType> {}
[PXDBString(2, IsFixed = true, BqlField = typeof(SOLineSplit.sOOrderType))]
[PXExtraKey] //Enables updates for joined SOOrderType DAC
public virtual string SOOrderType{ get; set; }
#endregion
...
}
Then SOLineSplit2
projection DAC can be updated somewhere in the graph with the following code:
var current2 = shipmentEntry.Caches[typeof(SOLineSplit2)].Current as SOLineSplit2;
if (current2 != null)
{
current2.ShippedQty = 0;
//only SOLineSplit and SOOrderType DACs will be updated
shipmentEntry.Caches[typeof(SOLineSplit2)].Update(current2);
}
Typical Use Cases
The most popular projection DACs usage is to create a view in the code that will unite several related DACs into one single projection DAC. The projection DAC can be consumed like a regular DAC which significantly reduces the code size, simplifies the logic and increases the code readability. However, projection DACs also have other popular use cases.
Reducing the Number of DAC Fields Returned from Database
Projection DACs are frequently used to reduce the amount of data retrieved from the database. You can define a simple projection DAC based on some existing DAC with a much smaller number of DAC fields.
The following projection DAC is based on APAdjust
DAC. However, it returns from the database only two DAC fields instead of approximately 100 DAC fields declared in APAdjust
:
[PXProjection(typeof(Select<APAdjust>))]
[PXHidden]
public class APAdjust3 : PXBqlTable
{
#region AdjgDocType
public abstract class adjgDocType : PX.Data.BQL.BqlString.Field<adjgDocType> {}
[PXDBString(3, IsKey = true, IsFixed = true, InputMask = "", BqlField = typeof(APAdjust.adjgDocType))]
[PXUIField(DisplayName = "AdjgDocType", Visibility = PXUIVisibility.Visible, Visible = false)]
public virtual string AdjgDocType { get; set; }
#endregion
#region AdjgRefNbr
public abstract class adjgRefNbr : PX.Data.BQL.BqlString.Field<adjgRefNbr> {}
[PXDBString(15, IsUnicode = true, IsKey = true, BqlField = typeof(APAdjust.adjgRefNbr))]
[PXUIField(DisplayName = "AdjgRefNbr", Visibility = PXUIVisibility.Visible, Visible = false)]
public virtual string AdjgRefNbr { get; set; }
#endregion
}
Declare Filter for DAC Records
Projection DAC sometimes are used to declare a filter on DAC records. Then you can work with the projection DAC as with a separate entity which helps with the business logic understanding:
[PXProjection(typeof(Select<Vendor,
Where<Vendor.payToVendorID, Equal<CurrentValue<Vendor.bAccountID>>>>))]
public class SuppliedByVendor : Vendor { }
Define Projection DAC to Represent Aggregates
Projection DACs can be used not only to unite several DACs in a single entity but also to create an entity that will represent an aggregate calculated on other DAC. There is a good example of this approach with the retrieval of the latest records by fin periods from the GL history DB table.
The GLHistoryByPeriod
projection DAC is used to simplify selection and aggregation of proper GLHistory
records on various inquiry and processing screens of the General Ledger module. Its main purpose is to close the gaps in GL history records. Such gaps can appear in cases when GL history records do not exist for every financial period defined in the system. To close these gaps, GLHistoryByPeriod
projection DAC declares the LastActivityPeriod
field which is calculated from every existing FinPeriod
. This allows inquiries and reports that produce information for a given financial period to look at the latest available GLHistory
record:
[PXProjection(typeof(Select5<GLHistory,
InnerJoin<FinPeriod,
On<FinPeriod.finPeriodID, GreaterEqual<GLHistory.finPeriodID>>>,
Aggregate<
GroupBy<GLHistory.branchID,
GroupBy<GLHistory.ledgerID,
GroupBy<GLHistory.accountID,
GroupBy<GLHistory.subID,
Max<GLHistory.finPeriodID, // Note the aggregation by MAX
GroupBy<FinPeriod.finPeriodID>>>>>>>>))]
[PXPrimaryGraph(typeof(AccountByPeriodEnq), Filter = typeof(AccountByPeriodFilter))]
public class GLHistoryByPeriod : PXBqlTable
{
// Simplified list of projection DAC fields with their mappingsfor brevity
BranchID //BqlField = typeof(GLHistory.branchID)
LedgerID //BqlField = typeof(GLHistory.ledgerID)
AccountID //BqlField = typeof(GLHistory.accountID)
SubID //BqlField = typeof(GLHistory.subID)
// LastActivityPeriod will contain the calculated value Max<GLHistory.finPeriodID>
LastActivityPeriod //BqlField = typeof(GLHistory.finPeriodID)
FinPeriodID //BqlField = typeof(FinPeriod.finPeriodID)
}
PXProjection Attribute Override
Sometimes, you may wish to override a projection attribute of the existing projection DAC. This can be achieved with the usage of a graph extension. Let's see how you can override the projection attribute with an example. Here is the MyProjection
projection DAC which uses some BQL query:
[PXProjection(typeof(Select<...Old Projection Query...>))]
public class MyProjection: PXBqlTable
{
#region DocID
public abstract class docID: PX.Data.BQL.BqlInt.Field<docID> { }
[PXDBInt(IsKey = true, BqlField = typeof(SomeDAC.docID))]
public virtual int? DocID { get; set; }
#endregion
...
}
I haven't specified any concrete BQL query because the projection query can be arbitrary and is irrelevant to the subject. I will just call it the old projection query. I have also specified a single projection DAC field mapped to the SomeDAC.docID
field. It will be used to demonstrate how you can change mappings of projection DAC fields in a graph extension.
Our goal is to override this query with the new projection query and change all necessary mappings of the projection DAC fields. After the changes MyProjection
DAC should behave as if it was declared like this:
[PXProjection(typeof(Select<...New Projection Query...>))]
public class MyProjection: PXBqlTable
{
#region DocID
public abstract class docID: PX.Data.BQL.BqlInt.Field<docID> { }
[PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public virtual int? DocID { get; set; }
#endregion
...
}
Notice that the DocID
field's mapping has changed from SomeDAC.docID
field to AnotherDAC.uniqueID
field.
To implement the required changes, we must first find all graphs using MyProjection
. For simplicity, let's assume that MyProjection
DAC is used only by the MyGraph
graph:
public class MyGraph : PXGraph<MyGraph>
{
public PXSelect<MyProjection> MyView;
}
To override the projection attribute, we need to declare a graph extension MyGraphProjectionOverrideExt
. The overriding logic should be placed inside the Initialize
method. The mapping of a projection DAC field is changed by the cache attached event handler declared for the field with a new set of attributes:
public class MyGraphProjectionOverrideExt : PXGraphExtension<MyGraph>
{
// Overriding the projection query by directly modifying the PXCache graph cache
// that corresponds to MyProjection DAC
public override void Initialize()
{
var cache = Base.Caches<MyProjection>();
var child = cache.Interceptor.Child;
Type newProjectionQueryType = typeof(Select<...New Projection Query...>);
cache.Interceptor = new PXProjectionAttribute(newProjectionQueryType);
cache.BqlSelect = cache.Interceptor.GetTableCommand();
if (child != null)
cache.Interceptor.Child = child;
}
// Overriding the DocID field's mapping.
[PXMergeAttributes(Method = MergeMethod.Merge)]
[PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
}
The graph extension above will override the projection query and projection DAC fields' mappings for MyProjection
DAC but only for the MyGraph
graph because the MyGraphProjectionOverrideExt
graph extension is applied only to MyGraph
.
If there are other graphs using MyProjection
DAC, then they will use the old projection query.
In theory, you can declare a graph extension on the base PXGraph
type like this:
public class MyGraphExtensionForAllGraphs : PXGraphExtension<PXGraph> {}
This way, the graph extension should be applied to all graphs. However, such approach will affect the whole system and will consume system resources unreasonably. Usually, only a few graphs in the system use a particular projection DAC, and the rest of the graphs will only have a redundant graph extension that does nothing. This is why Acumatica discourages graph extensions declared for all graphs. You should declare graph extensions for graphs that use MyProjection
DAC instead.
Reuse Projection Overriding Logic with Generic Graph Extension
It can be tedious to create a separate graph extension for every graph that uses MyProjection
DAC, if there are multiple such graphs. There will be a lot of code duplication. Fortunately, you can avoid it by putting all logic into a generic graph extension that will serve as a base type for graph extensions extending concrete graphs:
public abstract class ProjectionOverrideExt<TGraph> : PXGraphExtension<TGraph>
where TGraph : PXGraph, new()
{
// Overriding the projection query by directly modifying the PXCache graph cache
// that corresponds to MyProjection DAC
public override void Initialize()
{
var cache = Base.Caches<MyProjection>();
var child = cache.Interceptor.Child;
Type newProjectionQueryType = typeof(Select<...New Projection Query...>);
cache.Interceptor = new PXProjectionAttribute(newProjectionQueryType);
cache.BqlSelect = cache.Interceptor.GetTableCommand();
if (child != null)
cache.Interceptor.Child = child;
}
// Overriding the DocID field's mapping.
[PXMergeAttributes(Method = MergeMethod.Merge)]
[PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
}
Notice that the ProjectionOverrideExt
is abstract and has a generic TGraph
parameter. Now the support for concrete graphs can be easily added by creating derived graph extensions that substitute TGraph
with the concrete graph type:
public class MyGraphProjectionOverrideExt : ProjectionOverrideExt<MyGraph> { }
public class MyAnotherGraphProjectionOverrideExt : ProjectionOverrideExt<MyAnotherGraph> { }
Customizing DAC Field Attributes in Cache Attached
I would like to delve a little more into the approach used for changing attributes on the DocID
DAC field:
[PXMergeAttributes(Method = MergeMethod.Merge)]
[PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
In the code above the PXMergeAttributes
attribute with the MergeMethod.Merge
mode is used to tell the runtime that during the calculation of the resulting attributes set it should prefer attributes declared on the cache attached event handler over the attributes declared on the DAC field. During the graph initialization the runtime will use the PXDBInt
attribute with a new mapping over the PXDBInt
attribute declared on MyProjection
DAC.
Quite frequently, developers use a more naïve and concise approach without PXMergeAttributes
:
[PXDBInt(IsKey = true, BqlField = typeof(AnotherDAC.uniqueID))]
public void _(Events.CacheAttached<MyProjection.docID> e) { }
This code looks shorter and simpler, but it's not what you need most of the time because this code will replace all attributes declared on the DAC field with the attributes declared on the cache attached event handler. This opens a way to the code consistency issues, when somebody adds a new attribute to the DAC field later but forgets to add this new attribute to the attributes declared on the cache attached event. Therefore, the graph with the cache attached event handler will never use the new attribute, because it will be removed from the DAC field.
Customizing only the required attribute is a much safer approach that avoids this problem. The changes are applied only to the attribute we wish to modify. If someone adds a new attribute to the DAC field later, the new attribute will be successfully used by the graph with the cache attached event handler.
There are many ways to customize DAC field's attributes in the cache attached event handler with a different level of control. You can read about them here:
- https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=d0634e8a-3a21-454c-963a-6741e7ec8390
- https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=dce254b6-4fff-887a-4d0c-914284b2ad8b
Projection DACs and XML Doc Comments
It is good practice to write XML doc comments for public API. It is especially helpful for DACs and DAC properties. The Acumatica DAC Schema Browser mechanism even uses XML doc comments to generate descriptions for DACs and DAC fields. If you follow this practice and document your DACs then there is extra advice for the documentation of projection DAC fields.
As you already know, projection DACs can have three types of fields. Unbound DAC fields and DAC fields with PXDBScalar
and PXDBCalced
attributes are documented as regular DAC fields. There is nothing unusual about them. But mapped DAC fields are different. In most cases, the mapped DAC field of the projection DAC represents the original DAC field and has the same data, behavior and usages. Therefore, it should have the same documentation. The naïve approach would be to just copy the XML doc comment from the original DAC field:
public class OriginalDAC : PXBqlTable
{
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }
/// <summary>
/// DAC field holding important data.
/// </summary>
[PXDBString(150, IsUnicode = true)]
public virtual string DacField{ get; set; }
#endregion
}
...
[PXProjection(typeof(Select<OriginalDAC>))]
public class ProjectionDAC : PXBqlTable
{
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }
/// <summary>
/// DAC field holding important data.
/// </summary>
[PXDBString(150, IsUnicode = true, BqlField = typeof(OriginalDAC.dacField))]
public virtual string DacField{ get; set; }
#endregion
}
However, this approach is very brittle to changes in the documentation. Imagine the situation when the XML doc comment on the original DAC field should be changed. All copy pasted XML doc comments on mapped projection DAC fields must be changed as well, otherwise the documentation will become inconsistent. As you can see, this is a very tedious and error prone approach.
Fortunately, .Net provides a mechanism for this scenario. With the <inheritdoc>
XML tag you can reuse the annotation from the specified API:
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/recommended-tags#inheritdoc
You can use <inheritdoc>
tag for the annotation of all mapped projection DAC field properties. The inheritdoc
tag should specify cref
attribute referencing the original DAC field property like this:
[PXProjection(typeof(Select<OriginalDAC>))]
public class ProjectionDAC : PXBqlTable
{
// Reuse the docs from the DacField DAC property to add documentation to the BQL field without copy paste.
/// <inheritdoc cref="DacField"/>
#region DacField
public abstract class dacField: PX.Data.BQL.BqlString.Field<dacField> { }
// Reuse the docs from the OriginalDAC's DacField DAC property.
/// <inheritdoc cref="OriginalDAC.DacField"/>
[PXDBString(150, IsUnicode = true, BqlField = typeof(OriginalDAC.dacField))]
public virtual string DacField{ get; set; }
#endregion
}