Generic Graph Extensions: General Information
Suppose that you want to use the same business logic in multiple places in your application—that is, you need to insert the logic in at least two graphs. The graphs operate with data in the data access classes (DACs) and implement business logic through event handlers, actions, and other methods.
You can encapsulate the business logic that you want to reuse in a generic graph extension, which is a graph extension that doesn’t relate to any particular graph and can be used with any base graph.
Applicable Scenarios
- You want to reuse logic that’s implemented in MYOB Acumatica, such as multicurrency functionality, on a form that you’re creating.
- You’re implementing business logic and want to use it in multiple places in your application.
Implementation of Generic Graph Extensions
A generic graph extension works with data by using the mapped cache extensions, which are cache extensions that aren’t bound to any particular DAC and can extend any DAC.
To connect the mapped cache extensions to a particular DAC, you use a mapping class, which maps the fields of a mapped cache extension to the fields of a DAC. To connect the generic graph extension to a particular base graph, in the base graph, you define an implementation class, which inherits the generic graph extension.
In the following diagram, yellow rectangles represent the classes you need to implement to reuse the business logic.

These classes are described in detail in the sections below. (For details on the implementation of the classes, see To Implement Reusable Business Logic and To Insert Reusable Business Logic That Has Already Been Declared.)
Mapped Cache Extension
A mapped cache extension is an analog of a data access class (DAC) for a generic graph extension. In the mapped cache extension, you include the main fields that are used in the reusable business logic implementation. You map the fields of the mapped cache extension to the fields of a base DAC by using the mapping class, which is described in the next section.
The mapped cache extension can also include fields that aren’t mapped to any base DAC fields. (For details, see Mapped Cache Extensions and the Application Database.)
The class of a mapped cache extension inherits from the
PXMappedCacheExtension
abstract class, which derives from
PXCacheExtension and IBqlTable.
The declaration of a field of a mapped cache extension includes two required members, which are the same as the required members of a DAC field. For details about the declaration, see Data Access Classes in Fluent BQL.
The following code shows an example of a mapped cache extension.
//Mapped cache extension
public class Document : PXMappedCacheExtension
{
//BAccountID field
public abstract class bAccountID : PX.Data.BQL.BqlInt.Field<bAccountID> { }
{
}
protected Int32? _BAccountID;
public virtual Int32? BAccountID
{
get
{
return _BAccountID;
}
set
{
_BAccountID = value;
}
}
//CuryID field
public abstract class curyID : PX.Data.BQL.BqlString.Field<curyID> { }
{
}
protected String _CuryID;
public virtual String CuryID
{
get
{
return _CuryID;
}
set
{
_CuryID = value;
}
}
...
}
Mapping Class
A mapping class is a protected class that defines the mapping
between the fields of a mapped cache extension and the fields of a DAC. In a generic
graph extension, you declare a mapping class for each mapped cache extension that
you need to use in the reusable logic implementation.
A mapping class implements the IBqlMapping interface, which has the following two properties:
- Extension: The mapped cache extension
- Table: The DAC to which the extension is mapped
In the declaration of the mapping class, you also include declarations of the properties for each field of the mapped cache extension that you want to map to a field of the DAC, as the following code shows.
//A mapping class
protected class DocumentMapping : IBqlMapping
{
public Type Extension => typeof(Document);
protected Type _table;
public Type Table => _table;
public DocumentMapping(Type table)
{
_table = table;
}
public Type BAccountID = typeof(Document.bAccountID);
public Type CuryInfoID = typeof(Document.curyInfoID);
public Type CuryID = typeof(Document.curyID);
public Type DocumentDate = typeof(Document.documentDate);
}
The DAC fields are mapped as follows:
- If the name of a property field of the DAC is the same as the name of the mapping class property, the DAC field will be automatically mapped to the field of the mapped cache extension by the implementation class (which is described below).
- If the name of a property field of the DAC differs from the name of the mapping class field, you redefine the mapping manually in the implementation class.
- If no field in the DAC has the name of the mapping class field and no mapping is defined in the implementation class, the field of the mapped cache extension isn’t mapped to any base DAC field, as shown below.

Generic Graph Extension
A generic graph extension is a public abstract class encapsulating
business logic that can be used in multiple places of an MYOB Acumatica Framework-based application. The class inherits from the
PXGraphExtension<TGraph> class and can include multiple
base DACs as their type parameters.
In the generic graph extension, you declare the following items:
- The mapped cache extensions.
- The mapping classes.
- The
protected abstractmethods that return the mapping classes. You have to override these methods in the implementation class. - The views that can have either a mapping-based declaration or a standard declaration. You declare a mapping-based view by using the PXSelectExtension<Table> class.
- The event handlers, actions, and other methods.
The following code shows an example of a generic graph extension. Notice that the code includes simplified versions of the generic graph extension, which have fewer type parameters.
// A generic graph extension
public abstract partial class SalesPriceGraph<
TGraph, TDocument, TDetailOpt, TPriceClassSourceOpt> :
PXGraphExtension<TGraph>
where TGraph : PXGraph
where TDocument : class, IBqlTable, new()
where TDetailOpt : class, IBqlTable
where TPriceClassSourceOpt : class, IBqlTable
{
// Mapped cache extensions
public class Document : PXMappedCacheExtension
public class Detail : PXMappedCacheExtension
public class PriceClassSource : PXMappedCacheExtension
// Mapping classes
protected class DocumentMapping : IBqlMapping
protected class DetailMapping : IBqlMapping
protected class PriceClassSourceMapping : IBqlMapping
}
// Simplified versions of the generic graph extension
public abstract class SalesPriceGraph<TGraph, TPrimary> :
SalesPriceGraph<TGraph, TPrimary, IBqlTable, IBqlTable>
where TGraph : PXGraph
where TPrimary : class, IBqlTable, new()
{ }
public abstract class SalesPriceGraph<TGraph, TDocument, TDetail> :
SalesPriceGraph<TGraph, TDocument, TDetail, IBqlTable>
where TGraph : PXGraph
where TDocument : class, IBqlTable, new()
where TDetail : class, IBqlTable, new()
{ }
Implementation Class
An implementation class defines the implementation of a generic graph extension for a particular graph. You declare the implementation class as a class that derives from the generic graph extension class with the following type parameters:
- The base graph to which you add reusable logic
- The main DAC of the primary data view of the base graph
- Other DACs that correspond to the type parameters of the generic graph extension
In this class, you can override the mapping defined by the mapping class, override other methods of the base class, and insert your own views, methods, and event handlers, as the following code shows.
public class MultiCurrency : MultiCurrencyGraph<OpportunityMaint, CROpportunity>
{
protected override DocumentMapping GetDocumentMapping()
{
return new DocumentMapping(typeof(CROpportunity))
{
DocumentDate = typeof(CROpportunity.closeDate)
};
}
protected override CurySourceMapping GetCurySourceMapping()
{
return new CurySourceMapping(typeof(Customer));
}
public PXSelect<CRSetup> crCurrency;
protected PXSelectExtension<CurySource> SourceSetup =>
new PXSelectExtension<CurySource>(crCurrency);
protected virtual CurySourceMapping GetSourceSetupMapping()
{
return new CurySourceMapping(typeof(CRSetup))
{
CuryID = typeof(CRSetup.defaultCuryID),
CuryRateTypeID = typeof(CRSetup.defaultRateTypeID)
};
}
protected override CurySource CurrentSourceSelect()
{
...
}
}
You can define multiple implementation classes in one graph as follows (shown in the code below):
- In the
SalesPriceimplementation class, you map the Detail mapped cache extension to theRSSVOrderActualDAC. - In the
SalesPriceActualimplementation class, you map the Detail mapped cache extension to theRSSVOrderDetailDAC.
public abstract class SalesPriceWithoutDetail<TDetail> :
SalesPriceGraph<RSSVOrderEntry, RSSVOrder, TDetail>
where TDetail : class, IBqlTable, new()
{
#region Initialization
protected override DocumentMapping GetDocumentMapping()
{
return new DocumentMapping(typeof(RSSVOrder))
{
BAccountID = typeof(RSSVOrder.customerID),
DocumentDate = typeof(RSSVOrder.docDate),
};
}
protected override PriceClassSourceMapping GetPriceClassSourceMapping()
{
return new PriceClassSourceMapping(typeof(Location))
{
PriceClassID = typeof(Location.cPriceClassID)
};
}
#endregion
protected override void RecalculateMarkup(PXCache sender, Detail row) { }
}
public class SalesPrice : SalesPriceWithoutDetail<RSSVOrderDetail>
{
public static bool IsActive() => true;
protected override DetailMapping GetDetailMapping()
{
return new DetailMapping(typeof(RSSVOrderDetail))
{
LocationID = typeof(RSSVOrderDetail.customerLocationID),
CuryLineAmount = typeof(RSSVOrderDetail.curyExtPrice),
Descr = typeof(RSSVOrderDetail.tranDesc),
Quantity = typeof(RSSVOrderDetail.estimatedQty)
};
}
}
public class SalesPriceActual : SalesPriceWithoutDetail<RSSVOrderActual>
{
public static bool IsActive() => true;
protected override DetailMapping GetDetailMapping()
{
return new DetailMapping(typeof(RSSVOrderActual))
{
LocationID = typeof(RSSVOrderActual.customerLocationID),
CuryLineAmount = typeof(RSSVOrderActual.curyExtPrice),
Descr = typeof(RSSVOrderActual.description),
Quantity = typeof(RSSVOrderActual.actualQty),
CuryUnitCost = typeof(RSSVOrderActual.curyStdCost),
CuryUnitPrice = typeof(RSSVOrderActual.curyUnitPrice),
};
}
}
