AcumaticaFramework DevelopmentGuide
AcumaticaFramework DevelopmentGuide
DEVELOPMENT GUIDE
DEVELOPER GUIDE
Acumatica ERP 2019 R2
| Contents | 2
Contents
Copyright.................................................................................................................8
Acumatica Framework Guide...................................................................................9
Acumatica Framework Overview........................................................................... 10
Acumatica Cloud xRP Platform....................................................................... 10
Acumatica Framework Development Tools........................................................14
Runtime Architecture of an Application Based on Acumatica Framework............... 18
Getting Started with Acumatica Framework..........................................................21
Data Querying..............................................................................................22
Business Logic Controller Declaration.............................................................. 24
Data View and Cache....................................................................................25
Data Modification Scenarios........................................................................... 26
Business Logic Implementation...................................................................... 35
Designing the Application..................................................................................... 38
Designing the Database Structure and DACs....................................................38
System and Application Tables................................................................... 39
Table and Column Naming Conventions....................................................... 39
Common Columns and Data Types..............................................................41
Primary Key............................................................................................. 42
Foreign Keys and Nullable Columns............................................................ 43
Audit Fields..............................................................................................44
Concurrent Update Control........................................................................ 45
Attachment of Additional Objects to Data Records........................................ 45
Preservation of Deleted Records................................................................. 45
Multitenancy Support................................................................................ 46
Designing the User Interface......................................................................... 48
Development Environment Options............................................................. 48
Form and Report Numbering...................................................................... 49
Item Grouping on the Form Toolbar............................................................ 50
Naming the Graphs and Event Handlers.......................................................... 51
Graph Naming..........................................................................................51
Naming Conventions for Event Handlers Defined in Graphs............................ 52
Configuring ASPX Pages and Reports....................................................................53
Overview of ASPX Pages in Acumatica Framework............................................ 53
Technical Overview of the User Interface..................................................... 54
Processing of a Button Click.......................................................................59
Configuring the ASPX Page............................................................................ 63
Configuration of the Datasource Control...................................................... 64
Configuration of Callback Commands.......................................................... 64
Configuration of Callbacks......................................................................... 66
| Contents | 3
Configuring Containers.................................................................................. 67
Configuration of Container Controls............................................................ 67
Use of the DataMember Property of Containers............................................ 68
Use of the SkinID Property of Containers.....................................................69
Use of the Caption Property of Containers................................................... 70
Use of Multiple Data Views for Boxes in Containers....................................... 71
Use of the PXPanel Container.....................................................................71
Configuring Tables........................................................................................ 72
Configuration of Grids............................................................................... 72
Use of the SyncPosition Property of PXGrid.................................................. 74
Use of the DisplayMode Property of PXGridColumn........................................74
Use of the Type Property of PXGridColumn.................................................. 75
Controls for Joined Data Fields................................................................... 76
Configuring Tabs...........................................................................................77
Conditional Hiding of a Tab Item................................................................ 77
Configuring Boxes.........................................................................................78
Input Controls.......................................................................................... 78
Use of the CommitChanges Property of Boxes.............................................. 80
Use of the DataField Property of PXGroupBox...............................................81
Use of the Caption Property of PXGroupBox................................................. 81
Use of the RenderStyle Property of PXGroupBox........................................... 81
To Enable Callback for a Control................................................................. 82
Configuring Layout and Size.......................................................................... 83
Predefined Size Values.............................................................................. 83
Use of the StartRow and StartColumn Properties of PXLayoutRule................... 84
Use of the ColumnWidth, ControlSize, and LabelsWidth Properties of
PXLayoutRule........................................................................................... 86
Use of the ColumnSpan Property of PXLayoutRule.........................................87
Use of the Merge Property of PXLayoutRule................................................. 88
Use of the GroupCaption, StartGroup, and EndGroup Properties of
PXLayoutRule........................................................................................... 89
Use of the SuppressLabel Property of PXLayoutRule...................................... 91
Maintaining Reports...................................................................................... 92
Display of Reports.................................................................................... 92
Display of Analytical Reports...................................................................... 97
Accessing Data.................................................................................................... 100
Querying Data in Acumatica Framework........................................................ 100
BQL and LINQ........................................................................................ 101
Data Access Classes................................................................................103
PXView and PXCache of the Data View...................................................... 106
PXView Type and Views Collection.............................................................108
| Contents | 4
Copyright
No part of this document may be reproduced, copied, or transmitted without the express
prior consent of Acumatica, Inc.
11235 SE 6th Street, Suite 140 Bellevue, WA 98004
Restricted Rights
The product is provided with restricted rights. Use, duplication, or disclosure by the United
States Government is subject to restrictions as set forth in the applicable License and
Services Agreement and in subparagraph (c)(1)(ii) of the Rights in Technical Data and
Computer Software clause at DFARS 252.227-7013 or subparagraphs (c)(1) and (c)(2) of
the Commercial Computer Software-Restricted Rights at 48 CFR 52.227-19, as applicable.
Disclaimer
Trademarks
In This Guide
• Accessing Data
• Glossary
| Acumatica Framework Overview | 10
• Add-on applications that interact with Acumatica ERP through the web services API
• Applications embedded into Acumatica ERP through the built-in customization tools
In this part of the guide, you can find an overview of the Acumatica Cloud xRP Platform
and the place of Acumatica Framework in this platform. This part also includes an overview
of Acumatica Framework tools and a high-level overview of the runtime architecture of
applications based on Acumatica Framework.
In This Part
The Acumatica Cloud xRP Platform consists of a number of components, which are
highlighted with light blue in the following diagram. These components serve different
purposes, which are described in detail in this topic, and can be used either separately or
combined to achieve your business purposes.
| Acumatica Framework Overview | 11
The base of the Acumatica Cloud xRP Platform is formed by the components and tools
that provide the basic application functionality, such as multitenancy support, role-based
access, and localization tools. These components and tools are available out-of-the-box in
Acumatica ERP, any embedded in Acumatica ERP applications, or applications based purely
on Acumatica Framework applications. This means that you do not need to worry about
implementing mechanisms similar to these components during the design or programming of
your application based on the Acumatica Cloud xRP Platform.
Acumatica Cloud xRP Platform contains the basic components and tools listed in the
following table.
| Acumatica Framework Overview | 12
Acumatica Framework
Acumatica Framework provides the platform API, web controls, and other tools for the
development of the UI and business logic of an ERP application. The platform API is used for
the development of Acumatica ERP and any embedded applications (that is, customizations
of Acumatica ERP). Acumatica Framework can also be used to develop an ERP application
from scratch. You can find detailed information about the development of applications with
Acumatica Framework in this guide.
Acumatica Framework consists of the development tools listed in the following table.
Component Description
Platform API The API for implementing application business logic.
Web controls A set of web controls implementing access to business logic through
the web UI.
Template application The application and database structures providing frameset, lay-
out, and navigation services. You can deploy a template application
along with the template website by using the Acumatica Framework
Configuration Wizard.
Visual Studio tem- The project template for the creation of a new application and a set
plates of page templates that automate the creation of typical page lay-
outs.
| Acumatica Framework Overview | 13
Component Description
Application creation A set of components to automate the creation of the application da-
wizards ta access classes from the database tables and the web forms dur-
ing application development.
For details about the development tools, see Acumatica Framework Development Tools.
Acuminator
Acuminator is a static code analysis and colorizer tool for Visual Studio that simplifies
development with Acumatica Framework. Acuminator provides diagnostics and code fixes
for common developer challenges related to the platform API. Also, Acuminator can colorize
and format business query language (BQL) statements, and can collapse attributes and parts
of BQL queries. You can find related information and download Acuminator at Visual Studio
Marketplace.
With Acumatica Customization Platform, you can perform end-customer customizations and
create complex solutions for multiple customers. In these customizations, you can modify
the user interface, business logic, and database schema without recompilation and re-
installation of the application. Customizations are stored separately from the core application
code as metadata and can be modified, exported, or imported. Because customizations are
stored separately, they are preserved with the updates and upgrades of the core application.
The Acumatica Cloud xRP Platform provides multiple types of web services APIs for
development of applications integrated with Acumatica ERP. These applications can perform
data migration and data import, integration of Acumatica ERP with external systems, and
execution of long-running operations.
You can use the contract-based REST API, contract-based SOAP API, or screen-based SOAP
API to access the same business logic as is accessed in the UI. All types of the web services
APIs can be used with any customization applied to Acumatica ERP. The contract-based
REST API supports the OpenAPI 2.0 (formerly known as Swagger 2.0) specification.
For details on the web services APIs, see Contract-Based Web Services API and Screen-Based
Web Services API.
Acumatica ERP supports the OAuth 2.0 mechanism of authorization for add-on applications
that interact with Acumatica ERP through application programming interfaces (APIs). For
details on the authorization of applications, see Authorizing Client Applications to Work with
Acumatica ERP.
| Acumatica Framework Overview | 14
Mobile API
Acumatica ERP provides the Acumatica mobile application, which allows a user to work with
Acumatica ERP through the mobile devices. You can customize the mobile application by
using the mobile API. For details on the mobile API, see Working with Mobile Framework.
Related Links
Platform API
The Platform API provided with Acumatica Framework is an event-driven programming API,
which is traditional in rich GUI applications. This model covers database access, business
logic, GUI behavior, and error handling. All coding is done with only C#.
The following code gives an example of the business logic implemented in the business logic
controller: The code updates the receipt total when one of the transactions related to the
receipt is updated.
When a user selects a document transaction in the table on a form and updates the settings
of the transaction, the RowUpdated event is triggered, and the code above is executed and
updates the receipt total, as shown in the following screenshots.
| Acumatica Framework Overview | 15
All of Acumatica Framework's web controls have the same rendering and a similar
appearance in design mode in the IDE and runtime mode in a web browser. Thus, the
developer can utilize all the facilities of the Visual Web Designer component of Visual Studio.
The application developer can use the convenient drag-and-drop mechanism to create an
application form layout, to perform form visual editing, and to set a control's properties
and behavior through an intuitive graphical interface. This approach does not require any
knowledge of HTML or Java Script, yet allows the developer to create a professional and
appealing web GUI.
The following screenshots illustrate the design (left) and runtime (right) rendering.
The Visual Studio project and item templates provide reusable and customizable project
and item stubs that accelerate the development process, removing the need to create new
| Acumatica Framework Overview | 16
projects and items from scratch. Project templates provide the basic files needed for a
particular project type, include standard assembly references, and set the default project
properties and compiler options.
• A set of page templates that automate the creation of typical page layouts
The master pages mechanism in ASP.NET allows for the creation of an application that looks
and feels consistent. Master pages define the standard appearance and behavior that is
common in all application pages. You create individual content pages that refer to the master
page. When a content page is requested, it merges with the master page to produce output
that combines the layout and base functionality of the master page with the content of the
requested page.
Acumatica Framework fully supports the master pages mechanism and provides you with a
set of predefined master pages. You can design your own master pages or modify existing
ones.
A web application written with Acumatica Framework supports style modification through
Cascading Style Sheets (CSS).
Acumatica Framework provides a set of wizards for automating the creation of data access
classes and webpages. The use of these wizards eliminates the manual steps associated with
data access class creation and data binding configuration.
The Data Access Class Generator, which is shown in the following screenshot, provides the
application developer with a way to create and modify data access classes. It implements
the following functionality:
• Reading the data structure from a table, SQL query, or external data source
• Creating a data access class based on the data structure received from external data
source
• Reading the data access class structure from its definition and merging this structure
with the data structure received from the external data source
The Layout Editor, which is shown in the following screenshot, automates the creation of new
web forms. It uses metadata stored in the business logic controller and data access class to
help the application developer create new web forms or to modify existing ones. The Layout
Editor wizard implements the following features:
• Reading metadata from the business logic controller and the data access class and
creating a list of controls that could be added to the webpage
• Updating the webpage controls with the changed business logic controller and the data
access class metadata
| Acumatica Framework Overview | 18
An application written with Acumatica Framework has n-tier architecture with a clear
separation of the presentation, business, and data access layers, as shown in the following
diagram. You can find details about each layer in the sections below.
| Acumatica Framework Overview | 19
The data access layer of an application written using Acumatica Framework is implemented
as a set of data access classes (DACs) that wrap data from database tables or data received
through other external sources (such as Amazon Web Services).
The instances of data access classes are maintained by the business logic layer. Between
requests, these instances are stored in the session. On a standalone Acumatica ERP server,
session data is stored in the server memory. In a cluster of application servers, session data
| Acumatica Framework Overview | 20
For details about data storage in session, see Session. For details on working with the data
access layer, see Accessing Data.
The business logic is implemented though the business logic controller (also called graph).
Graphs are classes that you derive from the special API class (PXGraph) and that are tied to
one or more data access classes.
• Data views, which include the references to the required data access classes, their
relationships, and other meta information
• Business logic, which consists of actions and events associated with the modified data.
Each graph can be accessed from the presentation layer or from the application code that
is implemented within another graph. When the graph receives an execution request, it
extracts the data required for request execution from the data access classes included in
the data views, triggers business logic execution, returns the result of the execution to the
requesting party, and updates the data access classes instances with the modified data.
For details on working with the business logic layer, see Implementing Business Logic.
Presentation Layer
The presentation layer provides access to the application business logic through the UI, web
services, and Acumatica mobile application. The presentation layer is completely declarative
and contains no business logic.
The UI consists of ASPX webpages (which are based on the ASP.NET Web Forms technology)
and reports created with Acumatica Report Designer. The ASPX webpages are bound to
particular graphs.
When the user requests a new webpage, the presentation layer is responsible for processing
this request. Webpages are used for generating static HTML page content and providing
additional service information required for the dynamic configuration of the web controls.
When the user receives the requested page and starts browsing or entering data, the
presentation layer is responsible for handling asynchronous HTTP requests. During
processing, the presentation layer submits a request to the business logic layer for
execution. Once execution is completed, the business logic layer analyzes any changes in
the graph state and generates the response that is sent back to the browser as an XML
document.
For details on the configuration of ASPX webpages, see Configuring ASPX Pages and Reports.
Web services and mobile app pages provide alternative interfaces to the application business
logic. From the side of the graph, a request from a webpage, the web services, or an mobile
app page are identical and, thus, cause the execution of exactly the same business logic.
| Getting Started with Acumatica Framework | 21
System Requirements
You can find the full list of system requirements in System Requirements for Acumatica
Framework 2019 R2 in the Installation Guide.
Installation
For detailed information about the installation of Acumatica Framework, see Installing
Acumatica Framework in the Installation Guide.
Application Design
For the information about the design of the database structure and user interface of
applications based on Acumatica Framework, see Designing the Application in this guide.
Before you begin developing application code, we recommend that you complete the
following training courses:
For a quick overview of application programming, refer to the topics in this part of the guide.
In the Acumatica Framework Guide, you can find reference information and additional
information that is not covered in the training courses. This information is provided in the
following parts of the guide:
• Configuring ASPX Pages and Reports: About the development of ASPX pages
• Accessing Data: About business query language (BQL) and working with data in cache and
session
• Implementing Business Logic: About events, attributes, long-running operations, and other
topics related to business logic development
For a detailed description of the Acumatica Framework API, see API Reference.
| Getting Started with Acumatica Framework | 22
Website Management
If you want to modify the position of a form in the UI, add a form to a workspace, or remove
a form from the UI, you configure the UI as described in Customizing the User Interface in the
System Administration Guide.
You need to grant access rights to each new form. For details on the configuration of access
rights, see Managing User Access Rights in the System Administration Guide.
You can create help topics for any application you have developed with Acumatica
Framework by using the built-in wiki-based content management system. For details on
creating help topics, see Managing Wikis.
In This Part
• Data Querying
Data Querying
Acumatica Framework provides a custom language called BQL (business query language)
that developers can use for writing database queries. BQL is written in C# and based on
generic class syntax, but is still very similar to SQL syntax.
Acumatica Framework provides two dialects of BQL: traditional BQL and fluent BQL. We
recommend that you use fluent BQL because statements written in fluent BQL are simpler
and shorter than the ones written with traditional BQL. "Further in this topic, the examples
are written in fluent BQL.
You can also use LINQ to select records from the database or to apply additional
filtering to the data of a BQL query. For details, see Creating LINQ Queries.
BQL has almost the same keywords as SQL does, and they are placed in the same order as
they are in SQL, as shown in the following example of BQL.
SelectFrom<Product>.Where<Product.availQty.IsNotNull.
And<Product.availQty.IsGreater<Product.bookedQty>>>
If the database provider is Microsoft SQL Server, the framework translates this expression
into the following SQL query.
BQL extends several benefits to the application developer. It does not depend on the
specifics of the database provider, and it is object-oriented and extendable. Another
important benefit of BQL is compile-time syntax validation, which helps to prevent SQL
syntax errors.
Because BQL is implemented on top of generic classes, you need data types that represent
database tables. In the context of Acumatica Framework, these types are called data access
classes (DACs). As an example of a DAC, you would define the Product data access class
as shown in the following code fragment to execute the SQL query from the previous code
example.
using System;
using PX.Data;
Each table field is declared in a data access class in two different ways, each for a different
purpose:
• As a public virtual property (which is also referred to as a property field) to hold the
table field data
If the DAC is bound to the database, it must have the same class name the database table
has. DAC fields are bound to the database by means of data mapping attributes (such
| Getting Started with Acumatica Framework | 24
as PXDBIdentity and PXDBDecimal), using the same naming convention as fields in the
database.
The following code demonstrates an example of how to obtain data records from the
database.
Related Links
The following example shows the declaration of a simple business logic controller.
• Products: The data view that can be used for querying and modifying the data
• Cancel: The action that discard all the changes made to the data and reloads it from the
database
• Save: The action that commits the changes made to the data to the database and then
reloads the committed data
| Getting Started with Acumatica Framework | 25
Data views are declared in business logic controllers as public fields of PXSelectBase-
derived type. The following data view declaration uses the SelectFrom<Type>.View class,
which is derived from PXSelectBase.
The data view type is a business query language (BQL) statement that selects data to be
manipulated through the data view. The main DAC of a data view is the first type parameter
in the declaration. The data view that is specified as the primary view for the ASPX page
must be defined the first one in the graph. For details about, BQL, see Querying Data in
Acumatica Framework.
Based on this declaration, the system automatically instantiates the DAC cache.
A DAC cache object in the Acumatica Framework is the primary interface for working with
individual records from the graph business logic. It has two components and two primary
responsibilities:
• The Cached collection: In-memory cache that contains modified entity records. The
Cached collection is instantiated based on the corresponding DAC declaration and
managed by the cache.
• The controller: The cache component that implements basic CRUD (create, read, update,
delete) operations on the Cached collection and triggers a sequence of data manipulation
events when modifying or accessing the data in the Cached collection. These events can
be later subscribed from the graph to implement the business logic associated with the
data modification.
The diagram below shows the internal graph structure and responsibilities of the data view
and the cache.
| Getting Started with Acumatica Framework | 26
The framework executes data views in the order requested by the form. You don't have to
execute a data view explicitly to retrieve data for the UI.
In this example, the framework first executes the Orders data view to retrieve the master
data record, and then executes the OrderDetails data view. To pass the OrderNbr field
value as a parameter to the OrderDetails data view, we use the Current property of the
cache that keeps the data record that is currently selected in the UI. Thus the last data
record retrieved by the Orders data view is available through the Current property of the
cache. (But we expect to have only one master record available at a time.) Also, when you
create the new master data record, it also gets available through the Current property of
the cache.
If the Current property is null or the field value is null, the parameter is replaced by the
default value.
Related Links
user interface indirectly invokes the same methods as the direct call from the business logic
controller.
The data can be requested through the Select method of the data view. During this
operation, the systems executes BQL command from the data view declaration. The data
returned by the BQL command is passed to the requester. The following diagram illustrates
this process.
An existing record can be updated through the Update(record) method of the data view.
This method places the modified record into the cache.
If the data record is not found in the Cached collection, the cache controller loads the data
record from the database, adds it to the Cached collection, marks it as updated, and updates
it with the new values. The search of the data record in the Cached collection and loading
of the data record from the database is based on the DAC key fields. The diagram below
illustrates this scenario.
| Getting Started with Acumatica Framework | 28
If the updated record exists in the Cached collection the cache controller locates it and
updates it with the new values. The diagram below illustrates this scenario.
| Getting Started with Acumatica Framework | 29
A new record can be inserted into the cache through the Insert(record) method of
the data view. The new inserted record is added to the Cached collection and marked as
inserted. The diagram below illustrates this scenario.
| Getting Started with Acumatica Framework | 30
An existing record can be deleted from the cache using the Delete(record) method, of the
data view.
If the data record is not found in the Cached collection, the cache controller loads the data
record from the database, adds it to the Cached collection, and marks it as deleted. The
search of the data record in the Cached collection and loading of the data record from the
database is based on the DAC key fields. The diagram below illustrates this scenario.
| Getting Started with Acumatica Framework | 31
If the deleted record is found in the Cached collection, the cache controller locates it and
marks as deleted. The diagram below illustrates this scenario.
| Getting Started with Acumatica Framework | 32
The data can be modified and then queried again. In this scenario, the data records stored in
the cache memory are merged with the result of the BQL command execution. Data record
merge is based on DAC key fields. The final result of the Select() execution incorporates
all the earlier record modifications that have not been preserved to the database yet. The
diagram below illustrates this scenario.
| Getting Started with Acumatica Framework | 33
When the data is modified, the system has two different versions of the data: the new one
stored in the caches memory and the original one persisted in the database. At this point
you have two options:
• Save the new version of data to the database using the Persist() method of the graph
• Discard all in-memory changes and load the original data version using the Clear()
method of the graph
From the user interface these methods are called by invocation of the Save and Cancel
actions. These actions are predefined and mapped to the Persist() and Clear() methods.
Preserving the Data Version Between the Round Trips and Handling the
Subsequent Selects from the Views
Related Links
The business logic can be divided into common logic relevant to different parts of the
application and the logic specific to an application form (webpage). The common logic is
implemented through event handler methods defined in attributes, while the form-specific
logic is implemented as methods in the associated graph.
You implement the common business logic by defining event handlers in attributes. If such
attribute is added to the declaration of a data access class, attribute logic is applied to the
data records of this type for any graph used to access this table.
There are a number of predefined attributes implemented in the framework. For example, in
the following declaration of a data field for a column, the PXDBDecimal attribute binds this
field to a database column of the decimal type.
[PXDBDecimal(2)]
public virtual string AvailQty { get; set; }
The attributes that bind a field to a specific data type exist for most database data types.
[PXDBDecimal(2)]
[PXUIField(DisplayName = "Available Qty", Enabled = false)]
public virtual string AvailQty { get; set; }
You can also defined your own attributes, as shown in the following code.
These custom attributes can also be added to the DAC declaration, as shown in the following
example.
[PXDBDecimal(2)]
[PXUIField(DisplayName = "Available Qty", Enabled = false)]
[MyAttribute]
| Getting Started with Acumatica Framework | 37
For a specific screen, the application can redefine the common logic or extend it. For this
purpose, you should define event handlers in the graph associated with the screen. Each
event handler method is tied to a particular table or a table field via the naming convention.
For example, you can verify a value of a column as shown in the following code.
Related Links
• Prepare the database schema and the data access class design.
• Plan the forms that provide the user interface of the application. You create application
forms from specific Acumatica Framework form templates.
• Plan the business logic controller (also referred as graph) for each form, which
encapsulate business processes and use-cases that should be implemented in the
application.
Each of these steps is iterated for multiple times as the development is progress.
This part of the guide contains the design guidelines for the database schema and
applications built on Acumatica Framework.
In This Part
In This Chapter
• Primary Key
• Audit Fields
• Multitenancy Support
Related Links
• System tables: Those that are created by default for the application template and not
used to store your application data
• Application tables: Acumatica ERP tables (which exist if you have implemented
customization) and your own tables
Do not add columns to system tables or modify them in any other way. Such modifications
could corrupt the application and would be lost during the next database upgrade.
Regarding your own application tables, you have to design and create the needed tables that
store your application data. You then map these application tables to data access classes
(DACs) that define the object model of the application. In one table, you can keep data
records of multiple entities, each of which is defined as a separate data access class in the
application object model.
Related Links
When you are creating a table, you should consider the following suggestions regarding
naming conventions:
• Make sure that table and column names are valid C# identifiers, because these names
match the names of the classes and properties you declare in the application. Do not
start a table or column name with a digit.
• Do not use the underscore symbol (_) in table or column names, because it is a reserved
symbol in Acumatica Framework. For example, TenantType is a valid column name, while
Tenant_Type is invalid.
| Designing the Application | 40
• Use singular nouns for table names. Typically, a table is mapped to a data access class
that represents the entity. For instance, the SOShipment table contains data records that
represent instances of the SOShipment entity.
Acumatica Framework generates SQL statements with table and column names in
the same letter case (that is, uppercase or lowercase) as the corresponding data
access classes and fields are declared in the application. Also, the DAC Generator
tool produces data access class declarations in the same letter case as the tables
and columns are defined in the database schema.
• Use two prefixes in table names: a two-letter tenant name and then a two-letter
application module prefix. For example, the MTSVAppointment table can be used in the
Services (SV) module for the MyTenant tenant (which corresponds to the MT prefix).
These prefixes help to distinguish your application tables from Acumatica ERP tables and
tables of other vendors if you create an add-on project or extension library.
• If you add a column to an Acumatica ERP table, start the column name with the Usr
prefix followed by the two-letter tenant name. For instance, you could use UsrMTColumn
for the column of the MyTenant tenant. In this case, the column will be preserved during
upgrades. In your own application tables, there are no strict requirements to start
column names with any prefixes.
• Be sure that custom indexes on Acumatica ERP tables start with the Usr prefix followed
by the two-letter tenant name, so that the indexes will be preserved during upgrades.
Related Links
Related Links
Primary Key
You have to define the primary key in each application table that you create. The primary
key may consist of one column or multiple columns. The primary key must include the
CompanyID column if one is defined in the table. For details on the CompanyID column, see
Multitenancy Support.
For each table, you can use one of the following typical variants of primary keys:
• One key column included in the primary key in the table and set as the key in the data
access class
• A pair of columns, with one column included in the primary key in the table and the other
column set as the key in the data access class
• Multiple columns that are included in the primary key and set as the compound key in
the data access class
In a setup table, only the CompanyID column must be included in the primary key.
You may use one key column for rather short tables. For instance, you can use the two-letter
country code from ISO 3166 as the key in the Country table.
If you want to represent a user-friendly key in the user interface (UI) that corresponds to
a surrogate key in the database, you can use a pair of columns and the key substitution
mechanism provided by Acumatica Framework. You can define two columns in a table, one
for the surrogate key (typically the database identity column) and one for the natural key,
and set only the surrogate key as primary in the table. In the application object model, you
set the key to only the data field that is a natural key. In this case, Acumatica Framework
provides the ability to transparently work with different keys at the database and application
levels. In the UI, users work with only the natural key while the database operates with the
surrogate key (see the graphic below, which illustrates key substitution).
| Designing the Application | 43
For instance, you can define two columns in the Product table, ProductID and ProductCD.
ProductID is the identity column that is the only column included in the primary key of
the table. ProductCD is the string key of a product instance, which is entered by the user
through the UI. The ProductCD column isn't included in the primary key and is handled as
the unique key column by Acumatica Framework.
A compound key consisting of multiple columns may be used for complex entities. For
instance, you can include two columns, DocType and DocNbr, in the primary key for the
Document table. In the DocDetail table, you may use DocNbr and DocDetailNbr as the
compound primary key. The corresponding data fields should be also set as the key fields in
the data access class.
Related Links
There are no strict requirements to define column-level constraints and foreign keys in
application tables. Whether you define the constraints at the database level depends on
the design approach you use. At a higher level of the application object model, which is
represented by data access classes, you can flexibly define any level of constraints, including
default values, nullable fields, and parent-child relationships between data access classes. If
you aren't sure whether a column should allow a null value, you can allow null values for it in
the database. Later, in the data access class, you can make the data field either required or
nullable; you can even make the field required on one form and optional on another.
| Designing the Application | 44
For Boolean and decimal columns, we recommend that you define default values
either in the database or in data access classes. This simplifies the application code by
helping to avoid checking of values for nulls multiple times.
Related Links
Audit Fields
Audit fields keep meta information on the creation and the last change of a database record.
Audit fields are updated automatically by the framework.
To enable the tracking of audit data for a particular table, you should add the columns listed
below to the table and declare the corresponding audit data fields in the data access class.
You have to add the corresponding type attribute to each audit field. If the audit columns
are properly created in the database table and the corresponding data fields are declared
in the data access class, Acumatica Framework automatically updates audit data in these
fields every time a data record is modified from the application. The date and time values
are stored in the database in UTC.
The audit column parameters and DAC attributes are given below.
Database Column Data Type (SQL Data Type Type Attribute on the
Name Server) (MySQL) Data Field
CreatedByID uniqueidentifi- CHAR(36) with [PXDBCreatedByID]
er; not null ASCII character
set; not null
CreatedByScreenID char(8); not null CHAR(8) with [PXDBCreatedByScreenID]
ASCII character
set; not null
CreatedDateTime datetime; not DATETIME; not [PXDBCreatedDateTime]
null null
LastModifiedByID uniqueidentifi- CHAR(36) with [PXDBLastModifiedByID]
er; not null ASCII character
set; not null
LastModified- char(8); not null CHAR(8) with [PXDBLastModified-
ByScreenID ASCII character ByScreenID]
set; not null
LastModifiedDate- datetime; not DATETIME; not [PXDBLastModifiedDate-
Time null null Time]
Related Links
Database Column Data Type (SQL Data Type Type Attribute on the
Name Server) (MySQL) Data Field
TStamp timestamp; not TIMESTAMP(6); [PXDBTimestamp]
null not null
Related Links
You turn on or off support for data record attachments for each particular table individually.
To turn on support for data record attachments, add a column that stores the global data
record identifier (typically, NoteID) to the table and declare the corresponding field in the
data access class. For more information on uploading files through an Acumatica ERP form,
see To Display an Attached Image on the Form. See below for the parameters of the global
identifier column and the attribute that should be added to the corresponding DAC field.
Database Column Data Type (SQL Data Type Type Attribute on the
Server) (MySQL) Data Field
Global data record uniqueidentifi- CHAR(36) [PXNote]
identifier (typically er; null
named NoteID)
Related Links
query that marks the data record as deleted but does not permanently remove the data
record from the table. When data records are selected from the table, the data access layer
generates the SQL query, which returns only data records that are not marked as deleted.
The data records that are preserved in this way can be restored.
You can turn on or off the preservation of deleted data records for each table individually.
To preserve data records in a particular table, add the DeletedDatabaseRecord column
to the table and do not declare the data field in the data access class. When a data record
is deleted in the table, the framework automatically preserves the deleted data record
transparently to the application developer.
Database Column Data Type (SQL Data Type Type Attribute on the
Server) (MySQL) Data Field
DeletedDatabaseRe- bit; not null TINYINT(1); not Not declared in DAC
cord null
Related Links
Multitenancy Support
Multiple tenants can work on the same instance of an Acumatica Framework-based
application with completely isolated data. The application looks identical to all tenants, but
each tenant has exclusive access to its data only. Data is isolated at the lowest level of the
application, in the data access layer that executes SQL queries for the tenant of the user
who is currently signed in.
Multitenancy Support
The following graphic illustrates how different logical tenants work with the Acumatica
Framework-based application in a multitenant configuration. They work with the same
application but have isolated data access, as if they are working with different database
instances.
Multitenancy support is turned on or off for each particular table individually. To turn on
multitenancy support for a table, add the CompanyID column to it and include the column
in the primary key (see the column parameters in the table below) and all indexes. The
CompanyID column is handled automatically by the framework and should not be declared in
data access classes. If a table doesn't have the CompanyID column, all data from the table is
| Designing the Application | 47
fully accessible to all tenants that exist in the database. For more information, see Support of
Multiple Tenants.
Database Column Data Type (SQL Data Type Type Attribute on the
Name Server) (MySQL) Data Field
CompanyID int; not null; in- INT; not null; in- Not declared in DAC
cluded in primary cluded in primary
key and all index- key and all index-
es es
The graphic below shows a possible multitenant configuration with shared data access
between Tenant 1, Tenant 2, and Tenant 3. The users of Tenant 2 have access to the data
of all three tenants. The users from each of the other two tenants have access to their
company's individual data only. Physically, the data of all three tenants is stored in a single
database instance.
Support for shared data access is turned on or off for each particular table individually. To
turn on support for shared data access for a table, add the CompanyMask column to the
table (see the column parameters in the table below). The CompanyMask column is handled
automatically by the framework and should not be declared in data access classes. If a table
doesn't have the CompanyMask column, shared data access is not available for this table.
Database Column Data Type (SQL Data Type Type Attribute on the
Name Server) (MySQL) Data Field
CompanyMask varbinary(32), VARBINARY(32), Not declared in DAC
not null, default not null, default
0xAA 0xAA
| Designing the Application | 48
CompanyMask is a 32-bit mask. In this mask, each two bits correspond to each tenant. The
first of these two bits specifies whether the record may be read by this tenant, and the
second bit specifies whether the record may be written to by this tenant. For example,
suppose that CompanyMask is set to 0xBE02 for a record. That is, it specifies the following
mask: 10 11 11 10 00 00 00 10, which designates that the record may be both read and
written to by the tenants with company IDs 2 and 3, the record may be read by the tenants
with IDs 4 and 5 and the system tenant (which has ID 1), and the record may not be read or
written to by other tenants.
CompanyMask: 10 11 11 10 00 00 00 10
CompanyID: 4 3 2 1 8 7 6 5
The default value of CompanyMask is 0xAA, which means that the record may be read by all
tenants.
Related Links
In This Chapter
Related Links
We recommend the following settings for the Microsoft Visual Studio environment to ensure
a uniform ASPX page appearance:
| Designing the Application | 49
1. Under the Tools > Options > Web Forms Designer > CSS section, set the following
options:
2. Under the Tools > Options > Web Forms Designer > CSS Styling section, select
Auto Style Application, and specify the following settings:
• Use width and height attributes for image instead of CSS: Selected
• Use <strong> and <em> for bold and italic text: Cleared
We also recommended that you use the following settings of the Design mode of ASPX pages
in Visual Studio:
Related Links
Form Numbering
When you are numbering forms in Acumatica ERP, use the following conventions:
XX999999
| | | |_ Subscreen Sequential Number
| | |___ Screen Sequential Number
| |_____ Screen Type:
| Designing the Application | 50
| 10: Setup
| 20: Maintenance
| 30: Data Entry
| 40: Inquiry
| 50: Processing
| 60: Reports
|_______ Two-Letter Module Code
Report Numbering
When you are numbering reports in Acumatica ERP, use the following conventions in addition
to those outlined above:
XX6X9999
|
|____ Report Type:
61: Review Reports (Reports for document review prior to release)
62: Register Reports (Reports used to print audit information
on processed documents or entities)
63: Balance Reports (Reports reflecting current or historical
balance information)
64: Forms (Printed webpages)
65: Inquiry Reports (Reports that provide status information
required for operational management)
66: Statistical Reports (Reports that provide statistical or
historical information)
Related Links
• Actions: Use this menu to group the operations that give the user the ability to process
the document, including the actions that navigate to related data entry forms (with the
system filling in appropriate settings) so users can quickly create linked documents.
For example, see the Enter Payment/Apply Memo action on the Invoices and Memos
(AR301000) form. The most frequently used operations can be placed on the toolbar
outside any groups as separate buttons that provide quick access to the actions. For
example, notice the Release action on the Invoices and Memos form.
• Reports: Use this menu to group the actions that open related Report Designer reports
and printable forms of documents.
• Inquiries: Use this menu to group the actions that navigate to related inquiry forms.
| Designing the Application | 51
Inquiry Forms
• Actions: Use this menu to group the operations that give the user the ability to navigate
to related data entry forms.
• Reports: Use this menu to group the actions that open the related Report Designer
reports.
Maintenance Forms
• Actions: Use this menu to group the operations that update the settings of the master
record and navigate to related data entry forms.
• Reports: Use this menu to group the actions that open related Report Designer reports.
• Inquiries: Use this menu to group the actions that navigate to related inquiry forms.
Related Links
In This Chapter
• Graph Naming
Related Links
Graph Naming
When you are creating business logic controllers (graphs), use the following suffixes in the
names of the graphs, depending on the types of the forms they are used for:
• Maint: For the graphs for maintenance forms that are helper forms used for the input of
data on the data entry and processing forms, and for the graphs for the setup forms that
provide the configuration parameters for the application. For example, CountryMaint can
be the name of the graph for the Countries maintenance form, which provides editing of
the list of countries.
• Entry: For the graphs for data entry forms that are used for the input of business
documents. For example, SalesOrderEntry can be the name of the graph for the Sales
| Designing the Application | 52
Order data entry form, which provides the basic functionality for working with sales
orders.
• Inq: For the graphs for inquiry forms, which display a list of data records selected by the
specified filter. For example, SalesOrderInq can be the name of the inquiry form named
Sales Order Inquiry, which provides the list of documents selected by the specified
customer.
• Process: For the graphs for processing forms that provide mass processing operations.
For example, SalesOrderProcess can be the name of the Approve Sales Orders
processing form, which provides mass approval of sales orders.
Related Links
The name of a data record event handler must have the following segments, which are
separated by the _ symbol:
Therefore, the name of a data record event handler must be in the following format:
DACName_EventName (such as SOOrder_RowSelected).
The name of a data field event handler must have the following segments, which are
separated by the _ symbol:
2. The name of the data field declared within the DAC whose name is specified in the first
segment
Therefore, for a field event handler, the name must be in the following format:
DACName_FieldName_EventName (such as SOOrder_CustomerID_FieldUpdated).
Related Links
In This Part
• Configuring Containers
• Configuring Tables
• Configuring Tabs
• Configuring Boxes
• Maintaining Reports
In this chapter of the guide, you can find information about how to create the ASPX code
of the forms. This chapter also includes a technical overview of the user interface of an
Acumatica Framework-based application. For information about the configuration of the UI
from the back end, see Configuring the UI from the Back End.
You can configure the appearance and behavior of forms by using the properties of the ASPX
objects that can be used on the Acumatica ERP or Acumatica Framework ASPX pages. For
details on how to work with the ASPX code of the form, see the following topics:
• To configure the layout (PXLayoutRule) and size of controls: Configuring Layout and Size
For more details on the elements of the UI, see Acumatica ERP User Interface in the Interface
Guide.
Technologies in the UI
• The navigation frame, which is a webpage frame that can be used for navigation between
Acumatica ERP forms.
• The Acumatica ERP form, which is located in the inner frame, which is completely
independent of the navigation frame.
The webpage renders the navigation elements of the navigation frame and the forms
separately by using different technologies. The work of Acumatica ERP forms is based on
the ASP.NET Web Forms technology, while the navigation frame uses the ASP.NET MVC
technology with the Razor view engine.
The server side of the navigation frame uses the ASP.NET Web API framework. The client
side uses the React library, which is a JavaScript library, to render main menu items,
workspaces, tiles, and other navigation elements.
The following diagram shows how the browser renders the elements of the navigation frame.
This process is described in more detail in the remaining sections of this topic.
| Configuring ASPX Pages and Reports | 55
Request of main.cshtml
The browser retrieves main.cshtml, which is an ASP.NET MVC view, by sending the HTTP
GET request. On the server side, this request is processed by the MainController.Main()
method (PX.Web.UI.Frameset.Controllers), which creates a System.Web.Mvc.ViewResult
object that renders a view to the response. The returned view contains the basic skeleton of
the webpage, which includes the calendar control, the search control, and the empty menu.
| Configuring ASPX Pages and Reports | 56
The getSiteMap function in site.js uses jQuery to send an AJAX request to ASP.NET
Web API of the application server. On the server side, this request is processed by the
SiteMapController class (PX.Web.UI.Frameset.WebApi.Controllers).
To match the incoming request to the appropriate processing classes, the system uses
the ASP.NET MVC attribute routing. For example, the SiteMapController class is
annotated with the [FramesetRoutePrefix("sitemap")] attribute, which defines the
"frameset/sitemap" route.
To get the site map structure, the SiteMapController class uses the SiteMapRepository
class, which implements the ISiteMapRepository interface. The SiteMapRepository class
fetches different entities of the navigation frame and assembles them in one structure, which
is then passed to the browser. The system serializes the structure to JSON format by using
the standard ASP.NET Web API classes.
The SiteMapRepository class uses other classes that have the Repository suffix in
their names, such as TileRepository and WorkspaceRepository, to retrieve the entities
that are used in the navigation frame. These classes are completely independent from
the database. To fetch the entities from the database, the Repository classes use the
classes that implement the IEntitySet interface (PX.Web.UI.Frameset.Model), such as
ScreenEntitySet. The classes use the MUIGraph graph to fetch data from the database.
(The graph performs only simple data operations, and does not contain any complicated
business logic). For each entity, there is a DAC that is used to access data in the database.
The DACs correspond to the following database tables, which are used to store data for the
elements of the navigation frame.
Table Description
MUIWorkspace Stores information about the workspaces in the application. For more in-
formation on the workspaces, see Workspaces in the Interface Guide.
MUIFavorite- Stores information about the workspaces that have been pinned to the
Workspace main menu. The workspaces that are not included in this list are dis-
played when a user clicks the More Items menu item. For details about
the main menu, see Main Menu in the Interface Guide.
MUIArea Stores information about the areas to which workspaces belong. Areas
are used to group workspaces in the More Items menu by types.
MUISubcategory Stores information about the categories of Acumatica ERP forms. Cate-
gories are used to group forms in a workspace by types. For details on
the categories, see Categories.
MUIScreen Stores information about the locations of the Acumatica ERP forms in
the user interface. The table is connected to the SiteMap table by the
NoteID column.
MUIPinnedScreen Stores information about the Acumatica ERP forms pinned to work-
spaces.
| Configuring ASPX Pages and Reports | 57
Table Description
MUIFa- Stores information about the Acumatica ERP forms that have been
voriteScreen added to favorites.
MUITile Stores information about the tiles in workspaces. A tile is a special but-
ton on a workspace that you click to open a form or report with prede-
fined settings. For details on the tiles, see Tiles.
MUIFa- Stores information about the tiles that have been added to favorites.
voriteTile
MUIUserPrefer- Stores information about the position of the main menu, which can be
ences on the left of the browser page (default) or on the top of the browser
page.
The following diagram illustrates the process of retrieving data for the navigation frame.
The main script that is used to render the navigation frame is site.js. It contains classes
that use the React library to render elements of the navigation frame. Each such class
includes the render method, which uses React classes to render the element. The following
tables lists the main classes and their methods.
| Configuring ASPX Pages and Reports | 58
Class Description
MenuModules Renders the main menu (which contains the list of workspaces). For
more information on the main menu, see Main Menu in the Interface
Guide.
In addition to the render method, the class has the following methods:
• onClick: Processes the clicking of the Edit and Delete buttons for the
items of the main menu in Menu Editing mode.
In addition to the render method, the class has the following methods:
• onClick: Processes the clicking of the Edit and Delete buttons for the
tiles in Menu Editing mode and clicking of the Favorite button.
In addition to the render method, the class has the following methods:
• onClick: Processes the clicking of the Edit and Delete buttons for a
form in a workspace in Menu Editing mode.
In addition to the render method, the class has the following methods:
Class Description
The site.js script also contains webpage event handlers, such as handlers for button-clicking
events, which use jQuery to handle the events.
An administrator can configure the user interface to fit the work purposes of the
organization, as described in Customizing the User Interface in the System Administration
Guide.
To change the styles of the elements of the navigation frame, the developer can change the
CSS related to these elements.
If a developer has added a new form or report to the Acumatica ERP site in a customization
project, the location of the form in the user interface is included in the customization project
along with the SiteMap customization project item, which is created either automatically
or manually for the new item. For details, see To Add a New Custom Form to a Project and To
Add a Custom Analytical Report to a Project in the Customization Guide. For the custom generic
inquiries and dashboards, the information about the location in the user interface is included
in the GenericInquiryScreen and Dashboard customization project items, respectively.
Related Links
When the user clicks a button on a webpage, the page creates an HTTP request to the
server. The request includes the following information:
• The values of the key fields of the record currently displayed on the page
• The changes that have been made to the data on the page
• The information on the command that was initiated by the user—the data source ID and
the callback name
The PXDataSource.TypeName property defines the graph that processes data for the page.
When the application server receives the request, the server creates a new instance of the
graph to process the data of the request. The properties of PXDSCallbackCommand, such as
CommitChanges and RepaintControls, indicate which operations should be performed on
the server in addition to the operation initiated by the user.
The diagram below shows how the HTTP request is sent to the server.
| Configuring ASPX Pages and Reports | 61
If the callback command initiated by the user has the CommitChanges property set to
true or the CommitChangesIDs property specified, the server commits the changes before
executing the command. The graph instance commits the changes to the cache in the order
in which the data views are defined in the graph, as shown in the following diagram.
| Configuring ASPX Pages and Reports | 62
After the changes have been committed, the graph instance executes the operation initiated
by the user, such as saving data to the database. You can find details on the sequence of
events raised when data is inserted, updated, deleted, or saved to the database in Data
Manipulation Scenarios.
When the command execution is completed, the application server does the following:
2. The application server executes the Select method for each data view of the graph.
| Configuring ASPX Pages and Reports | 63
The application server sends the response to the page. The response includes data in XML
format; the parameters that are necessary for the controls to be repainted are specified in
JSON format, as shown in the following fragment of the response.
<Controls>
<Control ID="ctl00_phF_form_edDocType"
Props="{items:"INV|Invoice|1;DRM|Debit Memo|1;CRM|
Credit Memo|1;FCH|Overdue Charge|1;SMC|Credit WO|1",
value:"FCH"}" />
<Control ID="ctl00_phF_form_edRefNbr" Props="{value:"AR005254"}" />
...
</Controls>
The scripts in the browser (the scripts from PX.Web.UI.Scripts) find the controls to be
repainted by IDs and repaint these controls by using the data provided in the response.
Most of the scripts in PX.Web.UI.Scripts contain a class that works with one control. For
example, px_textEdit.js includes the PXTextEdit class, which works with the PXTextEdit
control.
For the buttons not found on the main toolbar, the process described in this topic may
slightly differ.
For example, for the table toolbar buttons, which perform actions on particular rows of grids,
the scripts translate the data in XML format, which is returned in the response, to HTML
format by using XSLT.
For the buttons in dialog boxes (the PXSmartPanel control), no commit of changes is
performed. Whether data is selected from the database depends on the particular dialog
box. The data that is returned from the server is in HTML format.
In This Chapter
• Configuration of Callbacks
| Configuring ASPX Pages and Reports | 64
• Is represented by the toolbar on the form that corresponds to the ASPX page
TypeName and PrimaryView are two required properties of a datasource control. For
the TypeName property, you have to specify the graph that works with the page. For the
PrimaryView property, you need to specify the data view that is used for navigation. The
navigation actions work with the main DAC of this data view. Acumatica Framework adds
callback commands to the datasource control for those actions whose data access class
(DAC) is the same as the main DAC of PrimaryView.
The callback command may be statically defined in the ASPX page markup or dynamically
added by the framework. When the server initializes the page object, it dynamically adds
additional callback commands to the datasource control. Along with system callback
commands, the server adds callback commands from the actions defined in the graph.
Callback commands are added for only those actions whose DAC is the same as the main
DAC of the data view specified in the PrimaryView property.
If the graph is derived from PXGraph<,> with two type parameters, the graph includes
the default actions, which are shown in the following code.
{
public PXSave<TPrimary> Save;
public PXCancel<TPrimary> Cancel;
public PXInsert<TPrimary> Insert;
public PXCopyPasteAction<TPrimary> CopyPaste;
public PXDelete<TPrimary> Delete;
public PXFirst<TPrimary> First;
public PXPrevious<TPrimary> Previous;
public PXNext<TPrimary> Next;
public PXLast<TPrimary> Last;
}
The framework creates the callback commands for the default actions if the DAC in
the second type parameter of PXGraph<,> is the same as the main DAC of the data
view specified in the PrimaryView property.
The framework determines the properties of callback commands by merging the properties
specified for the callback command on the ASPX page and the properties of the PXButton
(or derived) attribute specified for the action. For example, you can set the CommitChanges
property in both the callback command and the PXButton attribute added to the action in
the graph. The system uses the logical OR to the two values: If either of the values is true,
the resulting value is true.
If you change a default property of a dynamically added callback command, the definition of
the PXDSCallbackCommand component is added to the page, as the following code shows.
• Visible: Makes the button visible (if the property is set to true) or invisible (if it is
set to false) on the toolbar. The value of this property is merged with the value of the
Visible parameter of the PXUIField attribute specified for the action. The system uses
the logical AND of the two values: If either of the values is false, the resulting value is
false. By default, this property equals true.
• CommitChanges: Enables the posting of modified data when the callback command is
invoked. Its value is merged with the value of the CommitChanges parameter of the
PXButton (or derived) attribute specified on the action. The system uses the logical OR
of the two values: If either of the values is true, the resulting value is true. By default,
this property is equal to false.
| Configuring ASPX Pages and Reports | 66
• DependOnGrid: Enables the posting of the current row selected in the grid when the
callback command is invoked. The value is the ID of the PXGrid control.
Acumatica Framework adds buttons to the toolbar of the form; these buttons correspond
to the callback commands of the datasource control. A button appears on the toolbar if the
Visible property of the callback command is equal to true. To make a button invisible on
the toolbar, set the Visible property to false for the appropriate PXDSCallbackCommand
component as shown in the following code.
Configuration of Callbacks
Events are generated on the server side of your application when the client side requests
data or posts changes made by a user. When the client posts data to the server, the client is
initiating a callback to the server. Callbacks are initiated in the following cases:
• Focus is lost for an input control with the CommitChanges property set to True.
• Focus is lost for a grid column with the CommitChanges property set to True.
• A user finishes editing a row in a grid by proceeding to the next row or by pressing Ctrl
+Enter on the keyboard.
By default, an Acumatica Framework form executes a callback when a user clicks one of
the standard buttons on the form toolbar and when a user changes the value in a key field.
You can also enable a callback for any input control or button by setting the CommitChanges
property to True in the ASPX page. By default, the callback is off, so when a user changes a
value in some field and shifts the focus away from it, the browser doesn't post anything to
the server; it does, however, keep the change in memory.
When a callback is triggered, the client posts all the data from the whole form that has been
modified since the last callback.
For information on how to configure the callback on a control, see To Enable Callback for a
Control. For details on how to configure the callback for a button, see Configuration of Actions.
| Configuring ASPX Pages and Reports | 67
Configuring Containers
In this chapter, you can find information about the configuration of different types of
containers, such as PXFormView, PXGrid, PXTab, PXTreeView, and PXPanel.
In This Chapter
• PXFormView (form)
• PXGrid (grid)
• PXTreeView
• PXPanel
To configure a container control, you have to specify the data binding properties for it and
add input controls to the container. The following properties are required for every container
control:
• DataMember: Specifies the data view that provides data for the container
In the DataSourceID property, you have to specify the ID of the datasource control on the
page. When you create a page from a template, this property is already specified for the
default containers added from the template. When you add a nested container, you have
to specify the DataSourceID property. By default, in all page templates, the datasourse
control has the ds ID. After you have specified a DataSourceID for the container, you can
specify the DataMember property for it. The DataMember property links the container to the
data view defined in the graph. In the container, you can define input controls for data fields
available through that data view.
| Configuring ASPX Pages and Reports | 68
In Acumatica Framework, DataMember is used to specify a data view for the following
container types:
• PXFormView
• PXGrid
• PXTab
• PXTreeView
By default, a nested container inherits the DataMember property from the parent container. If
a nested container is PXFormView, PXGrid, or PXTab, it can be bound to another data view.
If the DataMember property is available for other ASPX objects, it has a special purpose. For
example, you can specify the DataMember property for a PXSelector lookup control to define
the appropriate data view for the grid of the lookup window.
Property Value
Each DataMember property value can correspond to any data view name of the BLC. Any data
view except for the main data view can be used by an unlimited number of containers. The
main data view must be bound to a single container.
For a container to contain a box for a data field, the container must be bound to a data view
declared within a BLC for the following reasons:
• A data field is declared in a data access class (DAC). An instance of the DAC record can
exist in the cache of a BLC that contains the declaration of a data view with the DAC
reference in the BQL statement.
• Each time a data record is selected in the container, the container creates a callback to
the PXDataSource control that is specified in the DataSourceID property of the container.
The data source control creates a remote procedure call to the application server to
execute the Display operation on the data view that is specified as the DataMember for
the container. The data view checks the existence of the record in the cache; if the check
fails, the data view executes the BQL request and stores the obtained record in the
cache.
| Configuring ASPX Pages and Reports | 69
• The data view provides all data exchange operations with the database, cache, and
PXDataSource control.
Predefined Skins
The following table lists and describes the predefined skins that are recommended to use for
the PXFormView, PXGrid, and PXPanel containers.
<sm:Container Name="ValueOfTheCaptionProperty">
...
</sm:Container>
Otherwise, in the WSDL schema, the API web service assigns to the container the name of
the first child element. If you use this name in the mobile site map, an error may occur after
the container content is reordered because the container name might be changed in the
WSDL schema.
| Configuring ASPX Pages and Reports | 71
For example, you can create a container and set the DataMember property to the name of
the data view that provides most of the fields for boxes in the container. If you also want to
create a control for a data field that cannot be accessible through that data view but can be
accessible through another data view of the same graph specified in the TypeName property
of the PXDataSource control, you can specify the required data view immediately in the
DataField property, as follows.
The following code snippet shows how to use the MyDataView, AnotherDataView, and
OnceMoreDataView data views declared in the same graph or in extensions for the graph to
define boxes for data fields in a PXFormView container on an Acumatica ERP form.
If you need to add controls that display DAC fields retrieved by an another data view, use
a nested form (a PXFormView control) instead of a panel. Unlike PXPanel, the PXFormView
control has the DataMember property and can be bound to a data view.
You can configure the appearance of a PXPanel container in the UI by specifying the
following properties of the control:
• Caption: Defines the caption for the set of controls enclosed in the panel.
• RoundBorder (default): The panel border and caption are displayed in the UI. This
style requires the Caption property.
• Fieldset: The underlined caption is displayed in the UI. This style requires the
Caption property.
• Simple: No border or caption is displayed in the UI. This style doesn't require the
Caption property.
In the ASPX code, the PXPanel container is always nested in a form or tab item and is
defined as follows.
Configuring Tables
In this chapter, you can learn how to configure tables (grids) on ASPX pages by using the
PXGrid and PXGridColumn ASPX objects.
In This Chapter
• Configuration of Grids
Configuration of Grids
To configure a grid on a form, you do the following:
1. Specify the DataMember property of the grid to bind the grid to a data view that provides
data to work through the grid.
3. Specify the SkinID property of the grid, which defines a set of default grid toolbar
buttons.
| Configuring ASPX Pages and Reports | 73
• DataMember: Specifies the data view that provides data for the grid. This property is
required for data binding, along with the predefined DataSourceID.
• SkinID: Defines the style and behavior of the grid; this property includes a set of default
grid toolbar buttons and the rendering of parameters of the grid.
• SyncPosition: Enables the synchronization of the selected row with the Current
property of the cache object. The Current property is set to each row selected by the
user in the grid.
• StatusField: Specifies the field that is displayed at the bottom of the grid as the grid
information status.
• AutoSize>Enabled: Enables the grid height to expand to the entire area of the parent
container.
• Width: Sets the width of the grid within the parent container (a standard ASP.NET
property of a control). To expand a nested grid to the entire width of the parent
container, set this property to 100%.
• AutoAdjustColumns: Makes the whole text of the column headers displayed if space
permits.
Columns are generally the only required items for a grid. If columns are defined, Acumatica
Framework creates input controls at runtime based on the field state of the appropriate DAC
field to provide row editing. However, you also have to define input controls for the grid in
either of the following cases:
• You want to set specific properties for an input control or use a different input control.
You may add only controls with specific properties; other controls will be generated with
default properties at runtime.
• You have to arrange the controls for form view mode of the grid.
| Configuring ASPX Pages and Reports | 74
The order of the controls in form view mode doesn't have to match the order of
the columns in the grid.
To define the set of toolbar buttons that appear in a grid, select the needed value in the
SkinID property of the grid. For details about the SkinID property, see Use of the SkinID
Property of Containers.
The SkinID property, used primarily to define the set of toolbar buttons for a grid, also
defines a complete set of UI parameters of the control, such as the column headers and the
initial size of the control.
To configure form view mode for a grid, you have to complete the following steps:
1. Add controls to the grid, set their properties, and arrange them by using layout rules in
the same way as you do on an ordinary form.
2. Enable callbacks for input controls to trigger in form view mode, and enable callbacks
for input controls that correspond to columns with the enabled callback. To enable the
callback for an input control, set the CommitChanges property to True for the control.
3. Enable form view mode for the grid by setting the AllowFormEdit property of Mode to
True.
To use the Current property of a PXCache object to access the record highlighted in a grid,
the Current property must be synchronized with record highlighting in the grid. To force the
system to provide this synchronization, you have to set the SyncPosition property of the
PXGrid container to True.
Value Description
Value Default value. An indicator that the column cell contains the value of the
field.
Text If there is a description defined for the field, an indicator that the col-
umn cell contains the description of the field.
Hint If there is a description defined for the field, an indicator that the col-
umn cell contains both the value and the description of the field.
The priority of the Type property is higher than the priority of the DisplayMode
property. If the Type property is set, for example, to CheckBox, the DisplayMode
property is ignored.
Value Description
NotSet The default value. An indicator that the field value is displayed in the
column as a plain string that is formed based on the field data format.
CheckBox An indicator that the field value is displayed in the column as a check
box, which is selected if the field value is True.
HyperLink An indicator that the field value is displayed in the column as a hyper-
link.
DropDownList An indicator that the column cell is rendered as a drop-down list that
contains all the values specified for the referred data field.
Icon An indicator that the field value contains an image URL and is displayed
in the column as the referred image.
Example
The following code fragment defines the grid columns on the Automation Schedule Statuses
(SM205030) form.
...
<px:PXGridColumn AllowUpdate="False" DataField="LastRunStatus" Width="40px"
Type="Icon" TextAlign="Center" />
<px:PXGridColumn DataField="ScreenID" DisplayFormat="CC.CC.CC.CC"
Label="Screen ID" LinkCommand="AUScheduleExt_View" />
<px:PXGridColumn DataField="Description" Label="Description" Width="200px" />
<px:PXGridColumn AllowNull="False" DataField="IsActive" Label="Active"
TextAlign="Center" Type="CheckBox" Width="60px" />
...
| Configuring ASPX Pages and Reports | 76
In the code, the Type property for the LastRunStatus data field (which corresponds to
the Status column shown in the screenshot below) is set to Icon. Because the field value
contains the image URL, the column cell displays the referred image.
For the IsActive data field (which corresponds to the Active column), the Type property is
set to CheckBox. As you can see in the screenshot, the column cells are rendered as check
boxes.
The following code example shows the SupplierProducts data view with a left join to
Product.
public SelectFrom<SupplierProduct>.
LeftJoin<Product>.On<Product.productID.IsEqual<SupplierProduct.productID>>.
Where<SupplierProduct.supplierID.IsEqual<Supplier.supplierID.FromCurrent>>.View
SupplierProducts;
You can add grid columns or input controls that display product information, such as the
unit price. To specify a field of a joined data access class (DAC), you use two underscore
symbols between the joined DAC name and the field name, as shown in the bolded text of
the following code.
In this case, the columns that display Product fields will be disabled on the grid, because
Product instances cannot be edited through the SupplierProducts data view. Only
SupplierProduct records can be edited through this data view, because SupplierProduct
is the main DAC of the data view.
| Configuring ASPX Pages and Reports | 77
Configuring Tabs
In this chapter, you can learn how to configure tables on ASPX pages by using the PXTab and
PXTabItem ASPX objects.
In This Chapter
• Set the AllowSelect property of PXCache of the data view in a RowSelected event
handler.
Using AllowSelect
In a RowSelected event handler, you can configure the AllowSelect property of PXCache
of the data view that corresponds to the tab item. In ASPX, you also need to set the
RepaintOnDemand property of PXTabItem to False.
For example, the following code makes the Applications tab of the Invoices (SO303000)
form visible or invisible depending on the document type.
Using VisibleExp
The VisibleExp property contains a condition expression that defines a Boolean value used
to set the visibility of the tab item. The expression must consist of two parts and an operator
to compare these parts. The expression can contain the values of controls that belong to the
container specified in the BindingContext property.
As an example of the conditional hiding of a tab item, on a form with form and tab
containers, if you need to set the visibility of a tab item to depend on a check box of the
form container, you can define the VisibleExp and BindingContext properties of the
PXTabItem element, as illustrated in the following ASPX code snippet.
| Configuring ASPX Pages and Reports | 78
Configuring Boxes
In this chapter, you can find information about the configuration of different types of boxes,
such as PXTextEdit, PXCheckBox, and PXGroupBox.
In This Chapter
• Input Controls
Input Controls
You can add input controls and columns to a container in the Screen Editor of the
Customization Project Editor or directly in the ASPX code of the form in Visual Studio. The
Screen Editor generates control definitions based on the attributes of DAC fields. It also lists
data fields and controls based on the field state.
The type of an input control correlates with the attributes of the DAC field that provides data
for the control. To define an input control, you have to add to the DAC field the attributes
that correspond to the needed type of the input control (see the table below).
In ASP.NET markup, the following properties are required for every input control in a
container:
• ID: Identifies the control within the page. This property is required by ASP.NET.
| Configuring ASPX Pages and Reports | 79
• runat="Server": Indicates that the server should create an object of the specified class.
This property is required by ASP.NET.
Particular types of controls may need additional properties, which are shown in the following
table.
Field State
On each round trip, the system generates a field state object for each data field
that is displayed in the UI. The field state object is initialized and configured on the
FieldSelecting event, which happens each time the data is prepared for displaying in the
UI. All of the following attributes implement FieldSelecting event handlers and take part in
the configuration of the field state object:
• Attributes that define the data type of a field (such as PXString and PXDBDecimal)
• Attributes that configure special types of input controls (such as PXSelector and
PXStringList)
The application can also define its own FieldSelecting event handlers.
| Configuring ASPX Pages and Reports | 80
The field state object includes properties common to the ASPX page controls. The properties
specified in the field state object have greater priority and replace the values set for the
controls on the ASPX page.
If callback is enabled for a box in a container on a page, the user has changed the box
value, and focus is no longer on the box on the page, the container immediately collects all
the modified data and a callback is created to pass the data to the PXDataSource control of
the page (see the diagram below).
The PXDataSource control creates a remote procedure call to the application server to
execute the Update operation with the modified data on the data view that is specified as
the DataMember property for the container. The data view executes the sequence of events
for update of a data record (for details, see Sequence of Events: Update of a Data Record) on the
data in the cache object of the business logic controller. The cache object raises the events
that you can handle to process the modified data.
The group box must contain a radio button for each value defined in the list of the
field.
In the DataField property of the PXGroupBox element, you can specify the name of a data
field that is accessible through another data view of the same graph. See Use of Multiple Data
Views for Boxes in Containers for details.
If the RenderStyle property of a PXGroupBox element is set to Simple, the Caption property
is ignored. See Use of the RenderStyle Property of PXGroupBox for details.
The Acumatica Framework supports the following RenderStyle values for the PXGroupBox
element.
To enable callback for an input control in the form or tab item container, set the
CommitChanges property to True for the control, as the following example shows.
Callback in a grid is enabled separately for columns and input controls. When a user works
directly in the grid (that is, in grid view mode of the grid), the callback is triggered on a
column. To enable callback for a grid column, set the CommitChanges property to True for
the column.
To enable callback for an input control if the callback is triggered in form view mode of the
grid, set the CommitChanges property to True for the control in the grid. For more details on
grids, see Configuration of Grids.
In This Chapter
Predefined Values
The following table shows the values in pixels that correspond to the predefined constants.
| Configuring ASPX Pages and Reports | 84
Note the following points about setting the predefined sizes of controls and their labels:
• For any property for which there are predefined values, you can specify a value in pixels,
such as 55px. (This format is mandatory if you don't use abbreviations, because the
property value can be defined only in pixels.)
• There is no predefined value for the Width property of a control. Therefore, you can
specify a value for this property by typing any value in pixels, such as 55px. Before
specifying the Width property value for a control, you must define the Size property
value for the control as Empty.
Default Layout
By default, the system places all the controls of a container into a column within the
first row, as shown in the diagram below. To do this, the system initially sets to True the
StartRow property value for the uppermost PXLayoutRule component in a container.
| Configuring ASPX Pages and Reports | 85
For the proper layout, the StartRow property value must be set to True for the
uppermost PXLayoutRule component of a container.
You can place controls in multiple columns within a row by adding PXLayoutRule
components with the StartColumn property value set to True. This property creates a new
column of controls within the current layout row, as the following diagram shows.
Every new PXLayoutRule component that has the StartRow property value set to True
initializes a new independent placeholder of controls, which are placed in a single column by
default. To place controls in multiple columns within the new row, you should include in the
placeholder a new layout rule with the StartColumn property value set to True, as shown in
the following diagram.
Because the values of the ColumnWidth, ControlSize, and LabelsWidth properties are
never inherited from the previously declared PXLayoutRule component, you might need to
define these properties exclusively for every new row and column.
Required Properties
Every PXLayoutRule component that has the StartRow or StartColumn property value set
to True must have one of the following sets of properties defined:
The following diagram illustrates the meaning of the LabelsWidth, ControlSize, and
ColumnWidth properties.
You should not set property values for both ColumnWidth and ControlSize for the
same PXLayoutRule component; in this case, the system would use the value of the
ControlSize property.
Please note the following points about setting the sizes of controls and their labels:
• You can change the size of a single control or its label by defining values for the Size,
Width, and LabelsWidth properties of the control. Property values that are set for a
control have a higher priority than the property values of the PXLayoutRule component.
• You can assign a predefined size abbreviation (such as XXS, L, or XL) for the
ColumnWidth, LabelsWidth, and ControlSize properties of a layout rule and the
LabelsWidth and Size properties of a control. (See Predefined Size Values for details.)
• The PXDateTimeEdit and PXNumberEdit control types have a predefined Width property
value, which you cannot change by setting the ColumnWidth or ControlSize property
values for the appropriate PXLayoutRule component. To change the width of this control,
set a value for the Size or Width property of the control.
Example
As an example of the use of the ColumnSpan property, the form container on the Customers
(AR303000) form has three columns of boxes, and there is a layout rule with the
ColumnSpan property set to 2 in the first column. This property forces the system to make
the box span two columns, as shown in the following screenshot.
| Configuring ASPX Pages and Reports | 88
Dependencies
• The LabelsWidth property value is always inherited from the previously declared
PXLayoutRule component that has the StartRow or StartColumn property value set to
True.
• If a value for the ColumnWidth or ControlSize property is specified for the component,
this value is ignored.
Example
As an example of the use of the Merge property, the Billing Settings tab item on the
Customers (AR303000) form has three pairs of merged check boxes in the Print and Email
Settings group. This property forces the system to render the boxes in one column, as
shown in the following screenshot.
| Configuring ASPX Pages and Reports | 89
Dependencies
A PXLayoutRule component with the Merge property value set to True is handled as follows:
• If the ColumnWidth property value is set for the same PXLayoutRule component, the
value is ignored.
• The default values for the ControlSize and LabelsWidth properties are inherited from
the previously declared PXLayoutRule component with the StartRow or StartColumn
property value set to True. You can override these property values, if necessary, by
specifying the ControlSize and LabelsWidth property values from the predefined list of
options. (See Predefined Size Values for details.)
Grouping of Controls
To group multiple controls within a column, generally you have to add two PXLayoutRule
components that have the following properties set to define the first and the last controls in
the group, respectively:
• GroupCaption and EndGroup: To create a group with the caption specified in the
GroupCaption property
| Configuring ASPX Pages and Reports | 90
You can specify both the GroupCaption property and the StartGroup property for a
PXLayoutRule component that starts a group.
For example, by specifying the GroupCaption property value for the corresponding
PXLayoutRule components placed above a control, you start the group of controls and
set up the header for the group. You should also add a PXLayoutRule component with the
EndGroup property value set to True below (in the code) the last control that is included in
the group.
You end a group by using a PXLayoutRule component with a GroupCaption, StartGroup,
or EndGroup property specified. Therefore, if there is another group that starts immediately
below a group, you can omit the layout rule that ends the upper group, as shown in the third
column of the row displayed in the example in following diagram.
Dependencies
The system works as follows for all PXLayoutRule components with the GroupCaption or
StartGroup property value specified:
• The default values for the ControlSize and LabelsWidth properties are inherited from
the previously declared PXLayoutRule component with the StartRow or StartColumn
property value set to True. You can override these property values, if necessary, by
specifying the ControlSize and LabelsWidth property values in the layout rule that
starts a group. (See Predefined Size Values for details.)
| Configuring ASPX Pages and Reports | 91
SuppressLabel Property
To hide the labels of the controls placed within a column, you should set the SuppressLabel
property value of the PXLayoutRule component of the column to True. Then within the
column, all check boxes are placed without any space to the left of the input control, and the
labels of other controls are hidden.
If needed, you can left-align a check box in the column by setting to True the
AlignLeft property of the control.
The SuppressLabel property affects all of the controls of the group that are placed under
the PXLayoutRule component with the True value of this property. The SuppressLabel
property value must be defined for every PXLayoutRule component for the controls placed
beneath the component and included in the same column; this property is never inherited
from the previously declared property.
Example
The Parent Info group on the Billing Settings tab item on the Customers (AR303000) form
is initially displayed with Parent Account displayed, as shown in the following screenshot.
If you set the SuppressLabel property of the group layout rule to True, the label of the
Parent Account box is hidden and all check boxes are displayed without any space to the
left of the check boxes, as shown in the following screenshot.
Figure: The same group of controls after applying the SuppressLabel property to the group
Maintaining Reports
In Acumatica Report Designer, you can create custom reports or modify existing reports and
then use these reports in Acumatica ERP or an Acumatica Framework-based application.
For more information on the creation of the custom reports with the Report Designer, see
Acumatica Report Designer Guide.
In this chapter, you can find details about how the system renders reports in Acumatica ERP.
In This Chapter
• Display of Reports
Display of Reports
In this topic, you can find information about how Acumatica ERP displays reports that are
created with the Acumatica Report Designer.
What a Report Is
A report is an RPX file (which is created with the Acumatica Report Designer) that contains
the report schema in XML format—that is, the description of the data that should be
displayed in the report and the description of the report layout.(See the following diagram.)
The description of the data of the report includes the following: the database tables that
provide data for the report, the relationships between these tables, the parameters that can
be specified before the report is run, and the filtering and grouping parameters. The report
layout is a tree of headers, details, and footers.
| Configuring ASPX Pages and Reports | 93
Reports can be saved in files on disk, or in the UserReport table of the database of an
Acumatica ERP instance. (The UserReport table uses the file name of the report as a key
and stores the schema of the report in XML format in the Xml column.)
The saved report can be published on an Acumatica ERP site—that is, added to the site map
and to any applicable workspaces so that the users can work with the report.
For details on creating reports with the Acumatica Report Designer, see the S130 Reporting:
Inquiry, Report Writing, Dashboards training course.
Acumatica ERP provides the following ways to run a report that you have created by using
the Acumatica Report Designer:
• From a report form, which is added to the site map, when the user clicks the Run
Report button on the form toolbar
| Configuring ASPX Pages and Reports | 94
• From a maintenance or entry form, when the user clicks the action button whose name is
associated with the report name
When a user opens the report form, the webpage performs the POST HTTP request to the
ReportLauncher.aspx page, passing the name of the report file as the ID query string
parameter of the request, as shown in the following example.
http://localhost/AcumaticaDB/frames/ReportLauncher.aspx?id=YF123456.rpx&HideScript=On
1. Loads the report schema from the file on disk or from the database (by using the
LoadReport method) to a PX.Reports.Controls.Report object (which stores the report
schema in memory and provides the methods for working with this schema).
b. Binds the Report object to the data source that is specified by the
PXSoapDataSource control of the ASPX page. The PX.Web.UI.PXSoapDataSource
class instantiates a SoapNavigator object, which will be then used to retrieve data
for the report from the database.
The server returns an XML response with the report parameters to display and with the ID
of the instance of WebReport in the session. The browser displays the report parameters and
other options on the report form.
After the user has selected the values of the parameters of the report and clicked the
Run Report button on the form toolbar of the report form, the webpage sends the GET
request to PX.ReportViewer.axd on the server with the ID of the WebReport instance in
the session (which was created when the report was launched), as shown in the following
example.
http://localhost/AcumaticaDB/PX.ReportViewer.axd?
InstanceID=10bf4a13a38c4af39cafc80926e407f2&OpType=Report
&PageIndex=0&Refresh=True
To process the request, the server invokes the Render method of the WebReport class, which
launches the generation of the report as a long-running operation in a separate thread. For
details on how the long-running operation is processed for a report, see Processing a Report as
a Long-Running Operation.
To retrieve the data of the report from the database, the system uses the
PX.Data.Reports.SoapNavigator object (to which a reference is stored in the Report
object). SoapNavigator instantiates a PXGraph object (without a type parameter) and
composes a BQL command as an instance of the PX.Data.Reports.BqlSoapCommand class.
BqlSoapCommand inherits from the PX.Data.BqlCommand class and is optimized for retrieving
data for the reports. BqlSoapCommand has the IndexReportFields method, which uses the
PX.Data.PXDependsOnFieldsAttribute attribute to get the dependent fields of the report
recursively. For details on how BQL commands are used to retrieve data from the database,
see Translation of a BQL Command to SQL.
| Configuring ASPX Pages and Reports | 96
The system processes the data and creates a ReportNode object. That is, the system creates
the sections of the report based on the data retrieved from the database and on the report
schema from the Report object, and calculates all formulas inside the sections. Then the
system uses the resulting ReportNode object, which contains all sections with all needed
values, to render data in the needed format.
After the long-running operation has completed, the PXReportViewer control displays the
report on the report form.
If a report is displayed in HTML format and the user turns the pages of the report, the
webpage sends the GET request to PX.ReportViewer.axd. The query string parameters of
the request are the ID of the report in the session and the number of the page, as shown in
the following request URL.
http://localhost/AcumaticaERP/PX.ReportViewer.axd?
InstanceID=008f2a8afb1a4998ade98bc64fc30ad9&OpType=Report
&PageIndex=0
The format in which the report is displayed (either PDF or HTML) is specified in the OpType
query string parameter of the request, as shown in the following request URL.
http://localhost/AcumaticaERP/PX.ReportViewer.axd?
InstanceID=008f2a8afb1a4998ade98bc64fc30ad9&OpType=PdfReport&Refresh=True
If a user turns the pages of the report or changes the format of the report, the system
creates the results of the report from the ReportNode object stored in the session by using
the renderer for the needed format. That is, the system does not retrieve the data of the
report from the database and does not processes this data to create a ReportNode object
once again.
On a form, when a user clicks the action button to generate a report, the data source control
of the form creates a request to the Acumatica ERP server to execute the action delegate
defined for the button. The server creates an instance of the graph, which provides the
business logic for the form and invokes the action delegate method. The action delegate
obtains from the form the data required to define the report parameters and throws an
exception of the PX.Data.PXReportRequiredException type with the report ID and these
parameters. The system processes the exception, saves the report parameters to the
session, and redirects the user to the ReportLauncher.aspx page.
The ReportLauncher.aspx page loads the report schema and instantiates a WebReport
object, as described in How the Report Is Launched from the Report Form. Instead of retrieving
the values of report parameters from the webpage, the system opens the report with the
parameters stored in the session. The system retrieves data for the report, as described in
How the Report Data Is Retrieved.
| Configuring ASPX Pages and Reports | 97
Related Links
• Asynchronous Execution
An analytical report is a specific type of report that you can construct in Acumatica ERP by
using the Analytical Report Manager (ARM) toolkit. You will likely want to use the ARM toolkit
rather than the Acumatica Report Designer to create the following types of reports:
• Financial reports that display data that is posted to the general ledger accounts and
accumulated in the General Ledger module. The system gets the data of the general
ledger accounts from the GLHistory table of the database.
• Project accounting reports that display data that is accumulated in the Project module.
The system gets the data for these reports from the PMHistory table of the database.
For details on analytical reports, see the F350 Reporting: Analytical Reports training course.
The schema of analytical reports are stored in a set of database tables that have RM prefix
in their names, such as RMReport, RMRowSet, and RMColumnSet. The analytical report
is identified in the system by its code, which you specify in the Code box on the Report
Definitions (CS206000) form. The report code is stored in the ReportCode column of the
RMReport table.
The schema of an analytical report can include the position of the report in the site map so
that the users can work with the report. To run the report, a user clicks the Run Report
button on the form toolbar of the report form.
When a user opens the report form of an analytical report, the webpage performs the POST
HTTP request to the RMLauncher.aspx page, passing the report code with .rpx appended
as the ID query string parameter of the request, as shown in the following example.
http://localhost/AcumaticaDB/frames/rmlauncher.aspx?id=dbsp.rpx&HideScript=On
On the server side, the system (by using an instance of the PX.Web.UI.PXReportViewer
class) processes the request as follows:
| Configuring ASPX Pages and Reports | 98
1. Loads the parameters of the analytical report from the database (by using the
LoadReport method) to a PX.Reports.Controls.Report object as follows:
b. The system initializes the ProcessMethod field of the WebReport object with the
processing function for analytical reports.
The server returns an XML response with the report parameters to display and with the ID of
the instance of WebReport in the session. The browser displays the report parameters on the
report form.
After the user has selected the values of the parameters of the analytical report and clicked
the Run Report button on the form toolbar of the report form, the webpage sends the GET
request to PX.ReportViewer.axd on the server with the ID of the WebReport instance in
the session (which was created when the report was launched).
To process the request, the server invokes the Render method of the WebReport class, which
launches the generation of the report as a long-running operation in a separate thread. For
details on how the long-running operation is processed for a report, see Processing a Report as
a Long-Running Operation.
To retrieve the data of the analytical report from the database, the system
uses the PX.Objects.CS.RMReportReaderGL (for financial reports) and
PX.Objects.CS.RMReportReaderPM (for project accounting reports) extensions of the
PX.CS.RMReportReader graph.
After the long-running operation has completed, the PXReportViewer control displays the
analytical report on the report form.
Related Links
• Asynchronous Execution
| Accessing Data | 100
Accessing Data
The topics in this part of the guide explain how an application based on Acumatica
Framework can access data from the application database and the data stored in the
session.
In This Part
To query data from the database, you can also use language-integrated query (LINQ), which
is a part of the .NET Framework. In the code of Acumatica Framework-based applications,
you can use both the standard query operators (provided by LINQ libraries) and the
Acumatica Framework-specific operators that are designed to query database data.
This chapter explains the aspects that are common to traditional BQL, fluent BQL, and LINQ
and provides a high-level comparison of the approaches for querying data in Acumatica
Framework.
In This Chapter
BQL is written in C#; it is based on generic class syntax, which is similar to SQL syntax.
Thus, BQL has almost the same keywords as SQL does, placed in the order in which they are
used in SQL. BQL offers several benefits to the application developer. BQL does not depend
on the specifics of the database provider, and it is object-oriented and extendable. Also, BQL
provides compile-time syntax validation, which helps to prevent SQL syntax errors.
You can also use language-integrated query (LINQ) provided by the System.Linq library
when you need to select records from the database in the code of Acumatica Framework-
based applications or if you want to apply additional filtering to the data of a BQL query.
However, you still have to use BQL to define the data views in graphs and to specify the data
queries in the attributes of data fields.
Acumatica Framework provides two dialects of BQL: fluent BQL and traditional BQL.
Traditional BQL was the initial language for data queries in Acumatica Framework; it provides
the benefits described above. Fluent BQL provides the following advantages as compared to
traditional BQL:
• It is easier to read and edit fluent BQL queries than traditional BQL queries because each
section of a fluent BQL query does not depend on the others and can appear in only
specific places of the query. Also, fluent BQL queries contain fewer commas and angle
brackets and do not use numbered classes (such as Select2 or Select6).
• You do not need to select a suitable class for a fluent BQL query (such as
PXSelectOrderBy<,> or PXSelectJoinOrderBy<,,>); instead, you simply start typing
the command, and IntelliSense in Visual Studio offers continuations that are relevant for
the current query state.
For a detailed list of differences between the dialects, see Comparison of Fluent BQL, Traditional
BQL, and LINQ.
The following code shows an example of a data view written in fluent BQL.
| Accessing Data | 102
SelectFrom<Product>.
Where<Product.availQty.IsNotNull.
And<Product.availQty.IsGreater<Product.bookedQty>>>.View products;
The following code shows the same data view written in traditional BQL.
PXSelect<Product,
Where<Product.availQty, IsNotNull,
And<Product.availQty, Greater<Product.bookedQty>>>> products;
Suppose the database provider is Microsoft SQL Server. Acumatica Framework translates the
fluent and traditional BQL queries shown above into the following SQL query, where [list
of columns] is the list of columns of the Product table.
Acumatica Framework explicitly enumerates the columns of the database table in the SQL
query. For details on which columns are enumerated, see Translation of a BQL Command to SQL.
LINQ
To configure a LINQ query, you can use the following variants of syntax:
• Query expressions, which use standard query operators from the System.Linq
namespace (such as where or orderby) or Acumatica Framework-specific operators
from the PX.Data.SQLTree namespace (such as SQL.BinaryLen, which is shown in the
following example of this syntax).
• Explicit (method-based) syntax. The arguments of the methods used in this syntax are
lambda expressions. In these expressions, you can use the standard C# operators and
Acumatica Framework-specific operators from the PX.Data.SQLTree namespace (such as
| Accessing Data | 103
SQL.BinaryLen, which is shown in the following code). The code below is equivalent to
the query expression shown above.
Related Links
DAC Fields
For each table column, you add a data field to the corresponding data access class by
declaring the following members:
You use this class to reference the table column in a business query language (BQL)
statement. The declaration of the class field is different in the fluent BQL dialect than it
is in the traditional BQL dialect. For details about the declaration, see Data Access Classes
in Fluent BQL and Data Access Classes in Traditional BQL. We recommend that you use the
fluent BQL style of DAC declaration because it can be used both in fluent BQL and in
| Accessing Data | 104
traditional BQL. The style of class field declaration is not important for queries defined
with language-integrated query (LINQ).
You bind the data field to the table column by specifying the type attribute that is derived
from the PXDBFieldAttribute class, such as PXDBString, and specifying the name
of the column as the name of the property. If you don't need to bind the property to a
database column (for example, if you want the value of the property to be calculated
from the database fields), you specify an unbound type attribute, such as PXDBCalced.
You assign the property a name that starts with an uppercase letter. For the lists of
bound and unbound type attributes, see Bound Field Data Types and Unbound Field Data
Types.
You use the property, which, in the system, holds the column data of the table, in the
queries defined with LINQ. In the SQL command generated from BQL, the framework
explicitly lists columns for all bound data fields defined in the DAC. For the unbound data
fields whose property attribute defines a BQL command, if this data field is used in a BQL
query, the system translates the BQL command of the property to SQL when the BQL
query is translated to SQL. For more information on the translation of BQL to SQL, see
Translation of a BQL Command to SQL.
The following code shows an example of the Product DAC declaration in the fluent BQL
style.
using System;
using PX.Data;
A data field can be bound (mapped) or unbound (not mapped) to a database column. The
type attribute on a DAC field specifies whether the field is bound or unbound. DB in the type
attribute name denotes whether the field is bound. In the code below, the OrderNbr field is
| Accessing Data | 105
bound because it has the PXDBString type attribute, while the Description field is unbound
because of the PXString attribute.
The framework provides bound and unbound types for many data types, including string,
Boolean, decimal, integer, and date and time. These types are abstracted from specific
database types.
The only mandatory attributes that you should add to DAC fields are:
Key Fields
You define the key fields in DACs independently of the database. Database key fields may
not be key fields in the DAC. To mark a field as a key field, you set the IsKey property to
true in the type attribute, as follows.
Product is the example of a DAC whose key field is different from the database key field.
The primary key of the Product table in the database consist of the ProductID field. In the
Product DAC, you mark the ProductCD field as the key field. The key fields defined in a DAC
are used to identify DAC data records in cache objects.
DACs in Acumatica ERP can include a pair of ID and CD fields (such as ProductID and
ProductCD). Typically, the ID field is represented by the identity column in the database
(automatically incremented integer) and serves as the surrogate field. The CD field is the
natural key, usually string, which is recognizable by a human.
For more information on attributes on DAC fields, see Working with Attributes.
Concurrency Management
You should add the SQL Server timestamp column to a table to make Acumatica Framework
able to handle concurrent updates.
| Accessing Data | 106
[PXDBTimestamp()]
public virtual byte[] TStamp
{
...
}
The corresponding timestamp data field should be declared in the data access class. If the
timestamp data field is declared, Acumatica Framework handles the timestamp column
automatically. Acumatica Framework checks the row version every time the row is modified.
For details about the timestamp, see Concurrent Update Control.
Related Links
You define a data view with a class derived from the PXSelectBase class, such as
SelectFrom<>.View in fluent BQL and PXSelect<> in traditional BQL. The first DAC of the
data view is the main DAC of the data view. Below are the Orders and OrderDetails data
views that are defined in the SalesOrderEntry graph. Both data views provide data for
the UI. Acumatica Framework automatically instantiates the data views and invokes the
Select() method when either of these data views is requested by the client.
When a graph executes a data view, the graph creates the following objects:
• The PXView object, which contains the BQL command that corresponds to the data view
| Accessing Data | 107
• The PXCache<DAC> objects whose type parameter is defined by the data access classes
(DACs) that are used in the BQL command
The PXView object uses the BQL command to retrieve data from the database and stores the
retrieved data in the PXCache object. The data view stores references to the corresponding
PXView object and the PXCache object of the main DAC of the data view, as shown in the
following diagram.
PXCache<DAC> are objects that are created by the system to maintain data records retrieved
from the database and the modifications to these data records. The system serializes the
modified data records from all cache objects to the session between round trips and restores
them on each new round trip.
Each data view is connected to a cache object by the main DAC of this data view. For
example, the Orders data view from the example above is related to a cache object of
PXCache<SalesOrder> type. To insert a new data record, update an existing one, or delete
a data record, you use the data view methods, which invoke the corresponding cache
methods.
| Accessing Data | 108
Cache objects identify data records by their key fields, which are fields with the IsKey
property set to true in a type attribute.
Cache objects hold the data records that are modified but not yet saved to the database.
Each cache object holds the data records of a single DAC. In a graph, each data view
corresponds to the cache object that works with the main DAC of the data view.
For data binding, you specify a data view in the DataMember property of a UI container
control, such as a form, grid, or tab on the ASPX page.
Each graph contains the following: a collection of cache objects, called Caches; and
a collection of PXView objects, called Views. The framework handles these objects
automatically; you don't have to initialize and control them.
• The BQL command, which is defined by the type of the data view.
• The optional delegate, which customarily constructs the data set that is returned instead
of the result of the BQL command's execution. For details, see Data View Delegates.
PXView objects, like graphs, are initialized and destroyed on each round trip.
On an ASPX page, you bind each container control with a data view that provides data for
the container control. To bind a container control and a data view, you specify the name of
the data view in the DataMember property of the container control. When a form requests
data, the system invokes the ExecuteSelect() method of the graph with data view name
passed as an argument to execute every data view bound to the ASPX page's container
controls. The positioning of container controls on the ASPX page determines the order of
execution of the data views. Note that if a data view is not bound to any container control, it
is not executed by request from the UI.
The order in which data views are defined in a graph is important, because it defines
the order in which data is saved to the database. (This order does not, however,
define the order in which data views are executed.) The data view that you specify in
the PrimaryView property should always be defined first in the graph.
When a data record is modified on the form, the framework invokes the ExecuteInsert(),
ExecuteUpdate(), or ExecuteDelete() method of the graph, passing the name of the data
view as an argument. The graph obtains the data view by using its name and invokes the
corresponding method of the data view.
| Accessing Data | 109
You can use the data view delegate in the following cases:
• If the query retrieves data fields that cannot be calculated declaratively by using
attributes—for instance, if you retrieve values that are aggregated by calculated data
fields
• If the result set has data records that aren't retrieved from the database and are
composed dynamically in code
To define the delegate for a data view, define a method that has the same name as the
data view but uses a different case for the first letter. The delegate returns an IEnumerable
object, as shown in the code below.
The framework automatically adds the delegate by its name and invokes the method when
the data view object is requested.
| Accessing Data | 110
• If a delegate is defined, invokes the delegate and then does the following:
• If the delegate returns an object, reorders the result according to the OrderBy clause
of the BQL command.
In the delegate, you can execute any queries to get the needed data records. The result
set (PXResultset<> object) returned by the delegate must consist of objects of the same
DACs and in the same order as the classes are specified in the data view type. Thus, in
the example above, you can return a PXResult<SupplierProduct> object, but you cannot
return a PXResult<Supplier> object. To return a Supplier object from the delegate, you
have to return the PXResult<SupplierProduct, Supplier> object from the method.
The result set returned by the data view is always sorted by the OrderBy clause
specified in the type of the data view object. If you sort data records in a different
way within the delegate, the result set will be reordered before it is returned by the
data view.
You can dynamically construct the result set that is returned by the data view. To construct a
result set, you create an object of a generic PXResultset type and add PXResult objects to
it.
The following code creates a result set of PXResult objects than contain the joined data of
three data access classes: Supplier, OrderLine, and Product.
// Compose DAC objects, and set values for the needed fields
Supplier resultSupplier = new Supplier();
OrderLine resultLine = new OrderLine();
Product resultProd = new Product();
// Create a new PXResult object from DAC objects and add it to the result set
res.Add(new PXResult<Supplier, OrderLine, Product>(
resultSupplier, resultLine, resultProd));
A PXResultset collection implements the IEnumerable interface; you can return the
collection in a data view delegate.
...
// In a data view delegate, you can return the entire result set
return res;
}
• Stage 3: When the developer casts the result of the query to a data access class (DAC)
or an array of DACs, the system does the following:
• 3a: If the result of the query contains the expression tree created by LINQ, the
system configures the SQL query tree that corresponds to the LINQ expression tree,
and executes the SQL query tree.
• 3b: If the result of the query is created only by BQL, the system configures the SQL
query tree for the delayed query and executes this query tree.
In code, you execute a business query language (BQL) statement in one of the following
ways:
• You use the static Select() method of a PXSelectBase-derived class with a graph
object as the parameter.
• You instantiate a class derived from the BqlCommand class (such as a Select class in
traditional BQL or FromSelect in fluent BQL), create a PXView object that uses this
BqlCommand class, create a graph object, and call one of the view's Select() methods.
When the Select() method is executed, Acumatica Framework does the following:
2. Caches the delayed query by using the PXContext.SetSlot method. (For details on the
slots, see Use of Slots to Cache Data Objects.)
3. Returns a PXResultset<T> object whose type parameter is set to the DAC specified as
the type parameter of the SelectFrom class (in fluent BQL) or as the first type parameter
of the PXSelect class (in traditional BQL). This result set contains information about the
delayed query.
You can iterate through the result set in a foreach loop, obtaining either DAC instances or
PXResult<> instances. A PXResult<> instance represents a tuple of joined records from the
result set. PXResult<> can be cast to any of the DAC types joined in the BQL statement.
For more information on the use of the PXResultset<T> class, see To Process the Result of the
Execution of the BQL Statement.
Once you cast the result of the execution of the Select() method to a DAC or an array of
DACs, or if you iterate through the DACs in the result by using the foreach statement, the
system performs the following steps:
1. The system calls the PXDelayedQuery.GetRows method for the delayed query of the
result set. This method internally calls the PXView.Select() method for the data view
referred to in the delayed query.
| Accessing Data | 114
2. If the select delegate is provided, inside the PXView.Select() method, the system
invokes the select delegate by using the PXView.InvokeDelegate method and saves the
result in the query cache of the graph. (The query cache stores the result set obtained by
the execution of a specific BQL command.)
3. Inside the PXView.Select() method, the system searches for the requested records in
the query cache by using the PXView.LookupCache method. If no records are found, the
system requests data from the database by using the PXView.GetResult method. For
details on the retrieval of records from the database, see Translation of a BQL Command to
SQL.
4. The system merges the records retrieved from the database or from the query cache
with the modified records stored in PXCache by using the PXView.MergeCache method.
For details about the merge, see Merge of the Records with PXCache.
5. The system saves the result of the query in the query cache by using the
PXView.StoreCached method.
Once you iterate over the LINQ expression tree, the system performs the following steps:
2. The system builds the SQL query tree from the Remotion.Linq.QueryModel by calling
the SQLinqExecutor.ExecuteCollection<T>() method. In this method, the system
executes the SQLinqQueryModelVisitor.VisitQueryModel() method, which does the
following:
b. Builds the SQL query tree for the rest of the Remotion.Linq expression tree by
calling the methods of SQLinqQueryModelVisitor for particular clauses and the
columns included in the result of the query. If the system cannot build the SQL query
tree for particular elements of the Remotion.Linq expression tree, the system falls
back to the execution of the delayed query for the base BQL statement. For details
about the fallback, see Fallback to the LINQ to Objects Mode.
4. The system merges the records retrieved from the database with the modified records
stored in PXCache. For details about the merge, see Merge of the Records with PXCache.
Related Links
To request the SQL query tree of the command, the system recursively calls the following
methods:
1. PXView.GetResult
2. PXGraph.ProviderSelect
3. PXDatabaseProviderBase.Select
4. BqlCommand.GetQuery
2. For each Join clause, the IBqlJoin.AppendQuery method adds to the Query instance
the Joiner instance that corresponds to the Join clause.
The IBqlJoin.AppendQuery method obtains the type of Join and, for all classes
in the On clause that implement the IBqlCreator interface, successively executes
the IBqlCreator.AppendExpression method, starting from the On class and then
proceeding with enclosed classes, such as the Where classes and comparison classes.
| Accessing Data | 116
3. For all classes in the Where and GroupBy clauses that implement the IBqlCreator
interface, the system successively executes the IBqlCreator.AppendExpression
method, which appends to the Query instance the SQL expression that
corresponds to the classes. For the DAC fields (IBqlField-derived classes), the
BqlCommand.GetSingleExpression method obtains the SQL expression.
For each sorting class (a IBqlSortColumn-derived class) in the OrderBy clause, the
IBqlSortColumn.AppendQuery method adds to the Query instance the OrderSegment
instance that corresponds to the sorting column. For the DAC fields (IBqlField-derived
classes), the BqlCommand.GetSingleExpression method obtains the SQL expression.
If the original BQL statement does not specify ordering, the system adds to the Query
instance sorting by the DAC key fields (in ascending order).
The following diagram shows the conversion of a BQL command to an SQL query tree.
| Accessing Data | 117
To obtain the SQL tree expression of each field of the BQL command, the BqlCommand
instance creates a PXCache instance that corresponds to the data access class (DAC)
to which the field belongs. The PXCache instance generates the OnCommandPreparing
event with the specified PXDBOperation type (which specifies the type of the database
operation). The attribute assigned to the DAC field (that is, the attribute that implements
the IPXCommandPreparingSubscriber interface) handles the event and returns the
PX.Data.SQLTree.SQLExpression instance that corresponds to the field or to the BQL
statement that is defined by the field attribute. (For example, the PXDBCalced and
PXDBScalar attributes define BQL statements.)
The following diagram shows how BqlCommand obtains the SQL tree expression for the fields
of a BQL command.
Figure: Retrieval of the SQL tree expression for the fields of a BQL command
Before the system requests the SQL query tree of a BQL command, the PXView object
retrieves the values of the parameters used in the query as follows:
• For a field with FromCurrent appended (in fluent BQL) or specified in the Current
parameter (in traditional BQL), the PXView object retrieves the field value from the
Current object of the PXCache object. If the current field value is null, the PXView
object triggers the FieldDefaulting event handlers and retrieves the default value from
the PXDefault attribute value (if any).
• For a field with AsOptional appended (in fluent BQL) or specified in the Optional
parameter, if the explicit field value is specified, the PXView object triggers the
FieldUpdating event, whose handlers can transform the external presentation of the
field value to an internal value (for example, transform ProductCD to ProductID). If the
| Accessing Data | 119
field value is not specified, the PXView object retrieves the field value from the Current
object of the PXCache object. If the current field value is null, the PXView object triggers
the FieldDefaulting event and retrieves the default value from the PXDefault attribute
value (if any).
When BqlCommand creates the SQL query tree that corresponds to the BQL command,
IBqlCreator.AppendExpression (which is implemented by the ParameterBase<Field>
class) includes the parameters in the SQL query tree. After BqlCommand has created the SQL
query tree that corresponds to the BQL command, the system inserts into the SQL query
tree the actual values of the parameters retrieved by PXView.
Related Links
1. If the query is read-only, the result set is not merged with any PXCache object. The
system returns the data records as they are currently stored in the database.
2. If the query is not read-only and contains filtering by data access class (DAC) fields
by using LINQ (that is, only the values in specific columns of the database tables are
returned in the results of the query), the system checks whether the primary key fields
of the primary DAC (the first DAC in the BQL statement) are returned in the results of
the query. If the primary key fields are not in the results, no merge with any PXCache
object is performed. If all primary key fields are in the results, the result set is merged
with the PXCache object that corresponds to the primary DAC.
| Accessing Data | 120
3. If the query is not read-only, does not contain filtering of DAC fields by using LINQ, and
does not contain joins, the result set is merged with the contents of the appropriate
PXCache object, and the system returns the result set updated with the modifications
stored in PXCache.
4. If the query is not read-only, does not contain filtering by DAC fields by using LINQ, and
joins data from multiple tables, the result set is merged with only the PXCache object
that corresponds to the first table of the BQL statement. The PXResultset<> object,
which represents the result set, contains objects of the generic PXResult<> type. This
type can be cast to the data access classes (DACs) that represent the joined tables. The
instance of the primary DAC to which PXResult<> is cast contains the records from the
database that are updated with the modifications stored in PXCache. Casting PXResult<>
to a joined DAC returns the instance that contains values from the database and has no
relation with the PXCache instances of the corresponding DAC types.
The following diagram illustrates the database records being merged with PXCache.
Related Links
Related Links
Select Commands
Search Commands
Join Clauses
Where Clause
Aggregate Clause
OrderBy Clause
Parameters
Comparisons
Related Links
When an Acumatica ERP form requests data, you do not need to execute a data view
manually; the system executes each data view automatically. If you need to manually
execute a BQL statement that defines a data view, do the following:
| Accessing Data | 131
2. Execute the data view by calling the Select() method of a PXSelectBase-derived class.
3. Cast the result of the query execution to a DAC or an array of DACs, or iterate through
DACs in the result by using the foreach statement. The following sample code shows the
approach of iterating through DACs. For details, see To Process the Result of the Execution of
the BQL Statement.
2. Cast the result of the query execution to a DAC or an array of DACs, or iterate through
DACs in the result by using the foreach statement. The following sample code shows the
approach of iterating through DACs. For details, see To Process the Result of the Execution of
the BQL Statement.
OrderBy<Asc<SalesOrder.orderNbr>>.View.Select(this))
...
}
}
1. Dynamically instantiate a data view in code. You should also provide the graph object as
a parameter to the data view constructor.
2. Execute the data view by using the Select() method of the instance of a PXSelectBase-
derived class.
3. Cast the result of the query execution to a DAC or an array of DACs, or iterate through
DACs in the result by using the foreach statement. The following sample code shows the
approach of iterating through DACs. For details, see To Process the Result of the Execution of
the BQL Statement.
Use parameters (such as AsOptional, FromCurrent, and @P.As[Type] in fluent BQL and
Optional, Current, and Required in traditional BQL) to pass specific values to a BQL
statement, as shown in the following example. For more details on how to construct a BQL
statement with parameters, see To Use Parameters in Fluent BQL Queries and To Use Parameters
in Traditional BQL.
// Declaration of a BLC
public class ReceiptDataEntry : PXGraph<ReceiptDataEntry, Document>
{
// When a screen associated with this BLC is first opened,
// the Optional parameter is replaced with the default DocType value.
public SelectFrom<Document>.
| Accessing Data | 133
Where<Document.docType.IsEqual<Document.docType.AsOptional>> Receipts;
If the data requested from the database cannot be described by a declarative BQL
statement, implement the data view delegate that is used instead of the standard Select()
logic to retrieve data from the database; this data view delegate must satisfy the following
requirements:
• The data view delegate must have the same name as the data view except for the first
letter, which must be lowercase.
• The data view delegate must return IEnumerable, as shown in the following example.
| Accessing Data | 134
If the data view delegate is not defined or it returns null, the standard Select()
logic is executed.
Related Links
In the foreach loop, cast each PXResult<T0> object in the collection to an object of the
main DAC. The PXResult<T0> object is implicitly converted to the T0 class. In the following
sample code, records are selected from the Document table.
1. In the foreach loop, cast each PXResult<T0> object in the collection to the needed
PXResult<T0, T1, T2, ...> object, where T0, T1, T2, and other type parameters are
joined DACs from the BQL statement. The PXResult<T0, T1, T2, ...> type must be
specialized with the DACs of all joined tables.
2. Cast each PXResult<T0, T1, T2, ...> item to any of the listed types to get the object
of this type.
The following sample code shows how to process the result set of a BQL statement joining
two tables.
Starting C# 7.0, you can also deconstruct the result set as shown in the following
code example.
Related Links
In this chapter, you can find information on how to create fluent BQL queries. For general
information about data querying, see Querying Data in Acumatica Framework. For details about
building queries with traditional BQL, see Creating Traditional BQL Queries.
In This Chapter
• Data Access Classes in Fluent BQL • To Use Parameters in Fluent BQL Queries
Fluent BQL uses nesting of generic classes. That is, each section of a fluent BQL query does
not depend on the other sections and can appear in only specific places of the query. The
order of the sections is shown in the following code.
SelectFrom<>.[Joins].Where<>.AggregateTo<>.OrderBy<>
SelectFrom<> is the only mandatory part of the query. You can add to the query any
number of Join sections and the Where<>, AggregateTo<>, and OrderBy<> sections of the
query, depending on whether you need the corresponding clauses of the query.
The query defined with fluent BQL as described above is equivalent to the Select command
in traditional BQL. To compose a query for different purposes (such as to define a data
view or to define a Search command in an attribute constructor), you need to prepend
additional elements to the query or append them to the query, as described in Search and
Select Commands and Data Views in Fluent BQL. You can find the equivalents of traditional BQL
in fluent BQL in Fluent BQL and Traditional BQL Equivalents.
SelectFrom<> Section
In the SelectFrom<> section of the query, you use the SelectFrom<> class, which uses a
data access class (DAC) as the type parameter. For details on DACs, see Data Access Classes
in Fluent BQL.
Join Sections
Each Join section of the fluent BQL query consists of the following components:
• The joining condition (On<>). This condition is not specified for CrossJoin<>.
• The single table modifier (SingleTableOnly). This optional part of each Join section
forces optimization if a DAC used in the query has the PXProjection attribute.
| Accessing Data | 138
The following code fragments show the Join sections with different types of joins.
.InnerJoin<TBqlTable>.On<TJoinCondition>.SingleTableOnly
.LeftJoin<TBqlTable>.On<TJoinCondition>.SingleTableOnly
.RightJoin<TBqlTable>.On<TJoinCondition>.SingleTableOnly
.FullJoin<TBqlTable>.On<TJoinCondition>.SingleTableOnly
.CrossJoin<TBqlTable>.SingleTableOnly
Conditions in the query are defined in the Where<> section and the On<> subsections of the
Join sections. The conditions can contain the following nested components:
• And<> subsections
• Or<> subsections
• Brackets<> subsections
The following code fragments show examples of an On<> subsection and a Where<> section.
.On<PMTask.projectID.IsEqual<PMProject.contractID>.
And<
PMTask.approverID.IsEqual<EPActivityFilter.approverID.FromCurrent>>>
.Where<PMProject.isActive.IsEqual<True>.
And<PMTask.taskID.IsNotNull.
Or<PMProject.approverID.IsEqual<
EPActivityFilter.approverID.FromCurrent>>>>
The AggregateTo<> and OrderBy<> sections of a fluent BQL query accept non-empty arrays
of the specific base type as the only generic parameters. To make it easier for developers to
write and read of the queries, fluent BQL includes groups of aliases that embed certain array
usage. These aliases are pregenerated for arrays with up to 32 elements.
The AggregateTo<> section can also include an optional Having<> subsection. In this
subsection, you include conditions that can contain only logical operators, constants,
parameters, and aggregated fields (that is, the fields with .Averaged, .Summarized,
.Maximized, .Minimized, or .Grouped appended).
The following code fragments show examples of AggregateTo<> and OrderBy<> sections.
| Accessing Data | 139
Related Links
Each class field of a DAC (that is, each public abstract class of a DAC) is strongly typed,
which makes it possible to perform compile-time code checks in Visual Studio. You derive
class fields not from the IBqlField interface (as you would in traditional BQL) but from the
specific fluent BQL classes that correspond to the type of the property field as shown in the
following table. You assign the class field a name that starts with a lowercase letter.
using System;
using PX.Data;
[Serializable]
public class Product : PX.Data.IBqlTable
{
// The class used in BQL statements to refer to the ProductID column
public abstract class productID : PX.Data.BQL.BqlInt.Field<productID>
{
}
// The property holding the ProductID value in a record
[PXDBIdentity(IsKey = true)]
public virtual int? ProductID { get; set; }
The DAC fields declared in fluent BQL style can be used in traditional BQL queries without
any modifications.
The class fields that are defined in the traditional BQL style (as described in Data Access
Classes) can be used in fluent BQL queries if you wrap these fields in the Use<>.As[Type]
class, where [Type] is one of the following: Bool, Byte, Short, Int, Long, Float, Double,
Decimal, Guid, DateTime, String, or ByteArray.
The following code shows the definition of the availQty class field in the traditional BQL
style and its use in a fluent BQL comparison.
SelectFrom<Product>.
Where<Use<Product.availQty>.AsDecimal.IsNotEqual<Zero>>.
View AvailableProducts;
| Accessing Data | 141
Though the DAC fields in the traditional BQL style can be used in fluent BQL queries, we
recommend that you use the fluent BQL style of DAC declaration for simplicity.
Related Links
Data Views
You can use any of the following approaches to define a data view:
• Use the PXViewOf<> class before the fluent BQL query, as shown in the following code
example.
PXViewOf<Product>.BasedOn<
SelectFrom<Product>.
Where<Product.isActive.IsEqual<True>>>.ReadOnly ActiveProducts;
The DACs that are specified as type parameters in PXViewOf<> and SelectFrom<> must
be the same; this is checked by the compiler. You can omit .BasedOn<> if you want to
declare a view that selects all records from one table. You append .ReadOnly to the view
definition if you need to define a read-only data view.
• Append .View to the fluent BQL query, as shown in the following code example.
SelectFrom<Product>.
Where<Product.isActive.IsEqual<True>>.View.ReadOnly ActiveProducts;
You append .ReadOnly to the view definition if you need to define a read-only data view.
The data views defined with fluent BQL are equivalent to the corresponding traditional BQL
data views. For the full list of equivalents, see Fluent BQL and Traditional BQL Equivalents. Also,
the fluent BQL data views have the same static methods as the traditional BQL data views
have.
Select Commands
The query defined with fluent BQL, as described in Fluent Business Query Language, is
equivalent to the Select BQL command. For the full list of equivalents, see Fluent BQL and
Traditional BQL Equivalents.
Search Commands
You can use any of the following approaches to define a Search BQL command:
| Accessing Data | 142
• Use the SearchFor<> class before the fluent BQL query, as shown in the following code
example.
SearchFor<Product.productId>.In<
SelectFrom<Product>.
Where<Product.isActive.IsEqual<True>>>
• Append .SearchFor<> to the fluent BQL query, as shown in the following code example.
SelectFrom<Product>.
Where<Product.isActive.IsEqual<True>>.SearchFor<Product.productId>
The Search commands defined with fluent BQL are equivalent to the corresponding
traditional BQL commands. For the full list of equivalents, see Fluent BQL and Traditional BQL
Equivalents.
Because SearchFor<> and SelectFrom<> are derived from the BqlCommand class, they can
be used in dynamic query building through the WhereAnd, AppendJoin, and OrderByNew
functions. However, fluent BQL commands (which are derived from the FbqlCommand
class) aren't decomposed by BqlCommand.Decompose() directly. That is, the Decompose
function checks whether a command has a FbqlCommand type, retrieves the type of its
underlying BqlCommand, and decomposes this underlying BqlCommand command. Therefore,
the elements of the array produced by the Decompose function are components of the
BqlCommand, and are not components of the passed FbqlCommand.
PXViewOf<TBqlTable> also supports all dynamic query building actions that the traditional
BQL PXView supports.
Related Links
If you need to use a custom constant in a fluent BQL query, you define this constant by
using the class that corresponds to the C# type of the constant. The following table lists the
constant classes that correspond to C# types.
The predefined constants and the constants defined as described in the previous section can
be used in traditional BQL without any changes.
The constants defined in the traditional BQL style (that is, derived from the Constant<Type>
class) can be used in the fluent BQL queries if you wrap these constants in the
Use<>.As[Type] class, where [Type] is one of the following: Bool, Byte, Short, Int, Long,
Float, Double, Decimal, Guid, DateTime, or String.
The following code shows the declaration of the decimal_0 constant in traditional BQL style
and its use in a fluent BQL comparison.
SelectFrom<Table>.
Where<Table.decimalField.AsDecimal.IsEqual<Use<decimal_0>.AsDecimal>>.
View records;
| Accessing Data | 144
Although the constants in the traditional BQL style can be used in fluent BQL queries, we
recommend that you use the fluent BQL style of constant declaration for simplicity.
In this topic, you can find a description of the fluent BQL parameters and the difference
between them.
To insert into the SQL query the field value of the Current object from the PXCache object,
you append .FromCurrent to the field name in a fluent BQL query. If the Current object
from the PXCache object is null, FromCurrent retrieves the default value of the field. If you
do not need to retrieve the default value if the Current object is null, you need to append
.FromCurrent.NoDefault to the field name in a fluent BQL query. In this case, the system
doesn't retrieve the default value and inserts null.
By using the current field value from PXCache in the declaration of a data view, you can refer
to another view to relate these data views to each other. A typical example is referencing the
current master record on master-detail forms. For details on how the current field value is
used, see To Relate Data Views to Each Another.
To insert a specific value into the SQL query, you use the @P.As[Type] classes, where
[Type] corresponds to the C# type of the parameter. The following table lists the fluent BQL
types that correspond to C# types.
By using these classes, you can pass values to the SQL query, as described in To Pass a Field
Value to the SQL Query and To Pass Multiple Field Values to the SQL Query.
To insert an optional value into the query, you append .AsOptional to the field name in a
fluent BQL query. If you specify an explicit value for this parameter during the execution
of the BQL statement, AsOptional uses the specified value. If you don't specify an explicit
value for this parameter during the execution of the BQL statement, AsOptional works
similarly to FromCurrent—that is, retrieves the field value of the Current object from the
PXCache object and uses the default value of the field if the Current object is null. You can
append .AsOptional.NoDefault to the field name in a fluent BQL query to make the system
not use the default value and insert null.
By using AsOptional, you can pass the external presentations of the values to the SQL
query, as described in To Provide External Presentation of the Field Value to the SQL Query.
When a DAC includes more than one key field, you may need to use .AsOptional in the
primary data view of the graph. In this case, the primary data view typically filters the data
records by all of the key fields except the last one. For example, you can select documents
with the same document type as the current data record has and navigate through these
documents with different document numbers. In the following example, the Document DAC
has two key fields, DocType and DocNbr.
public SelectFrom<Document>.
Where<Document.docType.IsEqual<Document.docType.AsOptional>>.View Receipts;
.AsOptional could be replaced with .FromCurrent in the code above unless you need to
execute the Receipts data view in code to select a document with specific document type
and number.
To insert a value from the UI control into the SQL query, you use the Argument.As[Type]
classes, where [Type] corresponds to the C# type of the inserted value. The following table
lists the fluent BQL types that correspond to C# types.
| Accessing Data | 146
By using the Argument classes, you can pass values to the data view delegates. For more
information on how to use the Argument classes, see To Pass a Value from a UI Control to a Data
View.
Related Links
This topic describes how to compose Select statements by using fluent BQL. For details
on how to adjust these statements to define data views or to specify Search commands in
fluent BQL, see Search and Select Commands and Data Views in Fluent BQL.
In a SelectFrom<> class, you configure a query to the database. The actual request
to the database is performed once you cast the result of the query execution to a DAC
or an array of DACs, or when you iterate through DACs in the result with the foreach
statement. For details, see Data Query Execution.
• Make sure that the application database has the database tables from which you are
going to request data, and that the application defines the data access classes (DACs)
for these tables. For more information on defining DACs, see Data Access Classes in Fluent
BQL.
using PX.Data.BQL.Fluent;
using PX.Data.BQL;
1. Type the SelectFrom<> class with the needed DAC as the type parameter.
For example, suppose that you need to convert the following SQL statement to fluent
BQL.
SelectFrom<Product>
2. If you need to include JOIN clauses in the query, for each table that you want to join, do
the following:
b. Append to the statement the On<> clause with the joining conditions. Adhere to the
following rules when you specify the conditions:
• Use the And<>, Or<>, and Brackets<> classes to logically connect the conditions
and comparisons.
• To specify the fields that should be used in the conditions, use the class fields
defined in the DAC, such as Product.productID. (The name of the field class
starts with a lowercase letter. Do not confuse it with the property field, which has
the same name but starts with an uppercase letter.)
| Accessing Data | 148
• If you need to use constants in the fluent BQL statement, use one of the
predefined BQL constants or your own constant. (For details on using constants,
see Constants in Fluent BQL.)
• If you need to specify the values of the parameters at run time, use the fluent
BQL parameters. For information about parameters, see Parameters in Fluent BQL.
For information about how to use parameters, see To Use Parameters in Fluent BQL
Queries.
In the sample code that has been presented in this topic, you would add two Join
classes to the statement, as follows.
SelectFrom<Product>.
InnerJoin<SupplierProduct>.
On<SupplierProduct.productID.IsEqual<Product.productID>>.
InnerJoin<Supplier>.
On<Supplier.accountID.IsEqual<SupplierProduct.accountID>>
3. If you need to include a WHERE clause in the query, append the Where<> clause to the
statement and specify the conditions as follows:
• Use the And<>, Or<>, and Brackets<> classes to logically connect the conditions and
comparisons.
• To specify the fields that should be used in the conditions, use the class fields defined
in the DAC, such as Product.productID. (The name of the field class starts with a
lowercase letter. Do not confuse it with the property field, which has the same name
but starts with an uppercase letter.)
• If you need to use constants in the fluent BQL statement, use one of the predefined
BQL constants or your own constant. (For details on using constants, see Constants in
Fluent BQL.)
• If you need to specify the values of the parameters at run time, use the fluent BQL
parameters. For information about parameters, see Parameters in Fluent BQL. For
information about how to use parameters, see To Use Parameters in Fluent BQL Queries.
In the sample code that has been presented in this topic, you would append the Where<>
clause to the statement, as follows.
SelectFrom<Product>.
InnerJoin<SupplierProduct>.
On<SupplierProduct.productID.IsEqual<Product.productID>>.
InnerJoin<Supplier>.
On<Supplier.accountID.IsEqual<SupplierProduct.accountID>>.
Where<
Brackets<Product.bookedQty.IsNotNull.
And<Product.availQty.IsNotNull>.
And<Product.minAvailQty.IsNotNull>.
And<Product.active.IsEqual<True>.
Or<Product.active.IsNull>>.
| Accessing Data | 149
And<Product.bookedQty.IsGreater<Product.availQty>.
Or<Product.availQty.IsLess<Product.minAvailQty>>>>.
Or<Product.availQty.IsNotNull>>
4. If you need to group or aggregate records, append the AggregateTo<> clause to the
statement and specify the grouping conditions and aggregation functions by using the
GroupBy clauses and the Min, Max, Sum, Avg, and Count aggregation functions.
In the sample code that has been presented in this topic, you would append the
AggregateTo<> clause to the statement as follows.
SelectFrom<Product>.
InnerJoin<SupplierProduct>.
On<SupplierProduct.productID.IsEqual<Product.productID>>.
InnerJoin<Supplier>.
On<Supplier.accountID.IsEqual<SupplierProduct.accountID>>.
Where<
Brackets<Product.bookedQty.IsNotNull.
And<Product.availQty.IsNotNull>.
And<Product.minAvailQty.IsNotNull>.
And<Product.active.IsEqual<True>.
Or<Product.active.IsNull>>.
And<Product.bookedQty.IsGreater<Product.availQty>.
Or<Product.availQty.IsLess<Product.minAvailQty>>>>.
Or<Product.availQty.IsNotNull>>.
AggregateTo<GroupBy<Product.categoryCD>,
Min<Product.bookedQty>>
5. If you need to order records, append to the statement the OrderBy<> clause with the
Asc<> and Desc<> classes as the type parameters.
In the sample code that has been presented in this topic, you would append the
OrderBy<> clause to the statement as follows.
SelectFrom<Product>.
InnerJoin<SupplierProduct>.
On<SupplierProduct.productID.IsEqual<Product.productID>>.
InnerJoin<Supplier>.
On<Supplier.accountID.IsEqual<SupplierProduct.accountID>>.
Where<
Brackets<Product.bookedQty.IsNotNull.
And<Product.availQty.IsNotNull>.
And<Product.minAvailQty.IsNotNull>.
And<Product.active.IsEqual<True>.
Or<Product.active.IsNull>>.
And<Product.bookedQty.IsGreater<Product.availQty>.
Or<Product.availQty.IsLess<Product.minAvailQty>>>>.
Or<Product.availQty.IsNotNull>>.
AggregateTo<GroupBy<Product.categoryCD>,
Min<Product.bookedQty>>.
OrderBy<Product.unitPrice.Asc,
| Accessing Data | 150
Product.availQty.Desc>
Related Links
You may need to use BQL parameters to relate data views to each other, to pass field values
to the SQL query, to pass the external presentations of the values to the SQL query, or to
pass values from UI controls to the SQL query.
To relate data views to each another, in a data query, use the field value of the Current
object from the PXCache object, as shown in the following sample code.
using PX.Data;
using PX.Data.BQL.Fluent;
Acumatica Framework translates the fluent BQL query of the second view in the
sample code to the following SQL statement. In this SQL query, [parameter1] is the
DocNbr value and [parameter2] is the DocType value retrieved from the Current
property of the DocTransaction cache; [list of columns] is the list of columns of
the DocTransaction table.
1. Use the @P.As[Type] class of the needed type in the BQL statement, where [Type]
is one of the following: Bool, Byte, Short, Int, Long, Float, Double, Decimal, Guid,
DateTime, or String.
2. Specify the needed value as the Select() method argument. The value passed to
Select() must be of the same type as the type of the specified field.
The @P.As[Type] class must be used only in the BQL statements that are directly
executed in the application code. The data views that are queried from the UI will not
work if they contain this class.
The code below shows the execution of a BQL statement with a specific value passed in
code.
using PX.Data;
using PX.Data.BQL.Fluent;
using PX.Data.BQL;
Acumatica Framework translates the previous fluent BQL query to the following SQL
statement. In this SQL query, [parameter] is the value of the product.CategoryCD
variable at the moment the Select() method is invoked; [list of columns] is the
list of columns of the Category table.
1. Use multiple @P.As[Type] classes of the needed type in the fluent BQL statement, where
[Type] is one of the following: Bool, Byte, Short, Int, Long, Float, Double, Decimal,
Guid, DateTime, or String.
2. Specify the needed values as the Select() method arguments in the order in which the
parameters are specified in the BQL statement. The number of @P.As[Type] classes
must match the number of parameters passed to the Select() method.
The @P.As[Type] classes should be used in only the BQL statements that are
executed in the application code.
The following code shows an example of a fluent BQL statement with two Required
parameters.
using PX.Data;
using PX.Data.BQL.Fluent;
using PX.Data.BQL;
Document doc =
SelectFrom<Document>.
Where<Document.docNbr.IsEqual<@P.AsString>.
And<Document.docType.IsEqual<@P.AsString>>>.View.
Select(this, line.DocNbr, line.DocType);
Acumatica Framework translates the previous fluent BQL query to the following SQL
statement, where [list of columns] is the list of the columns of the Document
table.
1. Define a data view with the Argument.As[Type] class, where [Type] specifies the data
type of the expected value, as shown in the following sample BQL query.
using PX.Data;
using PX.Data.BQL.Fluent;
SelectFrom<TreeViewItem>.
Where<TreeViewItem.parentID.IsEqual<Argument.AsInt>>.
OrderBy<Asc<TreeViewItem.parentID>>.View GridDataSource;
Acumatica Framework translates the previous fluent BQL query to the following
SQL statement. In this SQL query, [parameter] will contain the value received
from the UI control and passed to the Select() method; [list of columns] is
the list of columns of the TreeViewItem table.
2. Define the data view delegate with parameters through which you can access the values
passed from the UI. (You can find more information on how to use data view delegates in
To Execute a BQL Statement in a Data View Delegate.)
When a BQL statement with the Argument parameter is executed in code, the
value must be specified in the parameters of the Select() method.
1. Add the PXSelector attribute with a substitute key to a DAC field, as shown in the
following example.
using PX.Data;
using PX.Data.BQL.Fluent;
[PXSelector(typeof(SearchFor<Product.productID>.In<SelectFrom<Product>>,
| Accessing Data | 154
new Type [] {
typeof(Product.productCD),
typeof(Product.productName)
},
SubstituteKey = typeof(Product.productCD))]
public virtual int? ProductID { get; set; }
In this example, PXSelector replaces the ProductID field in the user interface with
the human-readable ProductCD field. In the UI control for this field, the user enters
a ProductCD value. The PXSelector attribute implements the FieldUpdating event
handler, which replaces the ProductCD value with the corresponding ProductID value.
2. Use the AsOptional class to select records by the external presentation of the field
value, as shown in the following code for OrderLine records.
using PX.Data;
using PX.Data.BQL.Fluent;
3. In the Select() method, provide values for all AsOptional, @P.As[Type], and
Argument.As[Type] parameters up to the last @P.As[Type] or Argument.As[Type]
parameter in the fluent BQL statement, as shown in the following sample code.
using PX.Data;
using PX.Data.BQL.Fluent;
Acumatica Framework translates the fluent BQL query in the code to the following
SQL statement, where [list of columns] is the list of columns of the OrderLine
table.
In this chapter, you can find information on how to create traditional BQL queries. For the
general information about BQL, see Querying Data in Acumatica Framework. For details about
building queries with traditional BQL, see Creating Traditional BQL Queries.
In This Chapter
• The classes that correspond to database tables (data access classes) and columns. For
details on data access classes, see Data Access Classes in Traditional BQL.
• The classes that define data views in a graph and select data from the database in code
(PXSelect classes). For more information on these classes, see PXSelect Classes.
• The classes that compose BQL statements, such as Select, Search, Where, OrderBy,
And, and Add. For more information on these classes, see The Classes That Compose BQL
Statements.
• The classes that pass parameters to BQL statements, such as Current, Required,
Optional, Argument. For details on BQL parameters, see Parameters in Traditional BQL
Statements.
You derive each class field of a DAC (a public abstract class of a DAC) from the
IBqlField interface and assign it a name that starts with a lowercase letter.
The following code shows an example of the Product data access class declared in
traditional BQL style.
using System;
using PX.Data;
[Serializable]
public class Product : PX.Data.IBqlTable
{
// The class used in BQL statements to refer to the ProductID column
public abstract class productID : PX.Data.IBqlField
{
}
// The property holding the ProductID value in a record
[PXDBIdentity(IsKey = true)]
public virtual int? ProductID { get; set; }
[PXDBDecimal(2)]
public virtual decimal? AvailQty { get; set; }
}
The class fields declared in traditional BQL style cannot be used in fluent BQL queries.
The class fields that are defined in the fluent BQL style (as described in Data Access Classes in
Fluent BQL) can be used in traditional BQL queries without any modifications. Therefore, we
recommend that you use the fluent BQL style of DAC declaration.
Related Links
PXSelect Classes
In traditional business query language (BQL), you define a data view or request database
data in code by using one of the PXSelect classes (that is, the classes derived from
PXSelectBase).
PXSelect Classes
The instances of PXSelect classes are complex objects containing the following:
• A reference (through the PXView object) to the Select object, which is the business
query language (BQL) command to be executed
• A reference to the cache of the data access class (DAC) type that is specified in the first
type parameter of PXSelect
That is, through the PXSelect classes, you can execute the BQL command and interact with
the cache, as illustrated in the following diagram.
| Accessing Data | 158
Do not confuse the PXSelect classes with the Select classes. PXSelect is an
aggregate of the data view, cache, and graph. You can use PXSelect classes to read,
write, update, and delete records in the scope of a graph. Select classes simply
represent BQL commands. You cannot read records by using a BQL command without
instantiating a data view. For more information on the Select classes, see The Classes
That Compose BQL Statements.
The first type parameter of all PXSelect classes is a data access class (DAC) generally
bound to a database table. The resulting SQL query selects records from this table. Other
type parameters (such as Where, OrderBy, Join, and Aggregate) are optional and represent
clauses that can be added to the basic select statement.
Depending on the clauses that will be used in a query, you select the appropriate variant of
the PXSelect class.
For example, if you need to use the Where, OrderBy, and Join clauses, you can use the
PXSelectJoin<Table, Join, Where, OrderBy> class to create the query, as shown in the
following BQL sample code.
PXSelectJoin<Table1,
LeftJoin<Table2, On<Table2.field2, Equal<Table1.field1>>>,
Where<Table1.field3, IsNotNull>,
OrderBy<Asc<Table1.field1>>>
Acumatica Framework translates this statement to the following SQL query, where
[list of columns] is the list of columns of the joined tables.
For more information on how to use the BQL clauses, see To Select Records By Using Traditional
BQL.
• PXSelect<Table, Where>
• PXSelect<Table>
• PXSelectGroupBy<Table, Aggregate>
• PXSelectJoin<Table, Join>
• PXSelectOrderBy<Table, OrderBy>
• PXSelectReadonly<Table, Where>
• PXSelectReadonly<Table>
• PXSelectReadonly2<Table, Join>
• PXSelectReadonly3<Table, OrderBy>
Almost all classes that compose BQL statements are derived from the IBqlCreator
interface, which inherits from the IBqlVerifier interface. These interfaces provide the
following key methods:
Depending on the purpose of each BQL class, the class also implements the methods of the
interfaces derived from the IBqlCreator interface. For example, the aggregation functions—
such as Sum, Avg, Min, and Max—implement the methods of the IBqlFunction interface.
The high-level overview of BQL class inheritance is illustrated in the following diagram. For
descriptions of the interfaces and classes, see the API Reference.
| Accessing Data | 161
The sections below describe the classes derived from the BqlCommand class.
Select Classes
The Select classes, which are derived from the BqlCommand class, represent BQL commands
and select all bound fields of the DAC and the unbound fields with specific attributes, such as
PXDBCalced.
| Accessing Data | 162
More specific, the Select classes select all DAC fields that are decorated with the
attributes that subscribe to the PXCommandPreparing event. For details on which
fields are selected, see Translation of a BQL Command to SQL.
In a BQL expression based on Select, the first type parameter is a DAC, as shown in the
following sample BQL statement.
Select<Product>
The Select classes can parse themselves into SQL and provide methods for modifying the
BQL command. However, you cannot directly use the Select class to execute the BQL query.
Typically, you use Select in attributes in DACs, such as the PXProjection attribute.
Search Classes
The Search classes, which are derived from the BqlCommand class, select one field of a DAC
(while the Select classes select multiple fields).
In a Search-based statement, the first type parameter is a DAC field, as shown in the
following sample BQL expression. This expression selects the Product.unitPrice field.
Search<Product.unitPrice>
These classes can parse themselves into SQL and provide methods for modifying the
BQL command. However, you cannot directly use the Search class to execute the BQL
query. Typically, you use Search in attributes in DACs, such as the PXSelector attribute.
(PXSelectorAttribute requires a Search class and not a Select because the lookup
control, which is configured by this attribute, displays precisely one field (usually a key
field), which is what Search returns.)
BqlCommand Classes
The BqlCommand classes represent BQL commands. The system uses the following types of
BqlCommand classes:
• BqlCommand: This base class for the Select and Search classes is used by the system
during the processing of data queries on the data entry forms.
• BqlGenericCommand: This class, which is derived from the BqlCommand class, is used by
the system during the processing of generic inquiries.
• BqlSoapCommand: This class, which is derived from the BqlCommand class, is used by the
system during the processing of reports. For details on report processing, see Display of
Reports.
The main purpose of BqlCommand classes is to convert BQL commands to SQL text. The
BqlGenericCommand and BqlSoapCommand classes provide additional methods for generic
inquiry and report processing.
Related Links
In this topic, you can find the description of the BQL parameters and the difference between
them.
The Current parameter, as well as the Current2 parameter, inserts the field value of the
Current object from the PXCache object in the SQL query. If the Current object from the
PXCache object is null, the Current parameter retrieves the default value of the field, while
the Current2 parameter doesn't retrieve the default value and inserts null.
By using the Current or Current2 parameter in the declaration of a data view, you can refer
to another view to relate these data views to each other. A typical example is referencing the
current master record on master-detail forms. For details on how the Current and Current2
parameters are used, see To Relate Data Views to One Another.
Required
The Required parameter inserts a specific value into the SQL query.
By using the Required parameters, you can pass values to the SQL query, as described in To
Pass a Field Value to the SQL Query and To Pass Multiple Field Values to the SQL Query.
The Optional parameter works similarly to Current (as well as the Optional2 parameter
works similarly to Current2) if you don't specify an explicit value for this parameter during
BQL statement execution. However, you can also pass an explicit value of the parameter to
the SQL query.
By using the Optional or Optional2 parameters, you can pass the external presentations of
the values to the SQL query, as described in To Provide External Presentation of the Field Value to
the SQL Query.
When a DAC includes more than one key field, you may need to use Optional<> in the
primary data view of the graph. In this case, the primary data view typically filters the data
records by all of the key fields except the last one. For example, you can select documents
with the same document type as the current data record has and navigate through these
documents with different document numbers. In the following example, the Document DAC
has two key fields, DocType and DocNbr.
public PXSelect<Document,
Where<Document.docType, Equal<Optional<Document.docType>>>> Receipts;
| Accessing Data | 164
Optional<> could be replaced with Current<> in the code above unless you need to execute
the Receipts data view in code to select a document with specific document type and
number.
If a data view contains the Optional<> and Required<> parameters, you should
provide values for all Optional<> parameters that go before the Required<>
parameters. For example, if you have the following operands in the query, the number
of parameters is:
Argument
The Argument parameter passes values from UI controls to the SQL query.
By using the Argument parameters, you can pass values to the data view delegates. For
more information on how to use the Argument parameter, see To Pass a Value from a UI Control
to a Data View.
Related Links
In a PXSelect class, you configure a query to the database. The actual request to the
database is performed once you cast the result of the query execution to a DAC or an
| Accessing Data | 167
array of DACs, or iterate through DACs in the result with the foreach statement. For
details, see Data Query Execution.
Make sure that the application database has the database tables from which you are going
to request data, and that the application defines the data access classes (DACs) for these
tables. For more information on defining DACs, see Data Access Classes in Traditional BQL.
To select all data from one database table without applying any filtering conditions or
ordering, use one of the PXSelect classes that has DAC as the only type parameter, such as
the PXSelect<Table> or PXSelectReadonly<Table> class, as shown in the following sample
BQL statement.
PXSelect<Product>
In this BQL statement, you are selecting all data records (with the values of all bound fields)
from the Product table.
For example, suppose that the Product table has two columns, ProductID and
UnitPrice. In this case, Acumatica Framework translates the previous BQL statement
to the following SQL query. The framework adds ordering by the DAC key field (in
ascending order) to the end of the SQL query because the BQL statement does not
specify ordering.
To Filter Records
To filter records in the database table to be retrieved, construct a BQL statement with
conditions by doing the following:
1. Use one of the PXSelect classes that has the Where type parameter, such as
PXSelect<Table, Where>. For the full list of PXSelect classes, see PXSelect Classes.
2. Specify the filtering conditions by using the Where clause, as described in To Filter Records.
3. To specify the fields that should be used for filtering, use the class fields defined in the
DACs, such as Product.productID. (The name of the field class starts with a lowercase
letter. Do not confuse it with the property field, which has the same name but starts with
uppercase letter.)
The following sample BQL statement selects all data records from the Product table that
have the specified value in the ProductID column.
PXSelect<Product,
Where<Product.productID, Equal<Required<Product.productID>>>>
| Accessing Data | 168
Acumatica Framework translates the previous BQL statement to the following SQL
query. In this SQL query, [list of columns] is the list of columns of the Product
table; [parameter] is the value passed to the Select() method of the PXSelect
class, which is called when the BQL query is executed.
To Order Records
To order records in the database table to be retrieved, construct a BQL statement with
ordering by doing the following:
1. Use one of the PXSelect classes that has the OrderBy type parameter, such as
PXSelectOrderBy<Table, OrderBy> or PXSelect<Table, Where, OrderBy>. For the full
list of PXSelect classes, see PXSelect Classes.
3. To specify the field that should be used for filtering, use the class field defined in the
DAC, such as Product.productID. (The name of the field class starts with a lowercase
letter. Do not confuse it with the property field, which has the same name but starts with
uppercase letter.)
The following sample BQL statement selects all Product data records and sorts them by the
UnitPrice field in ascending order.
PXSelectOrderBy<Product, OrderBy<Asc<Product.unitPrice>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
1. Use one of the PXSelect classes that has the Join type parameter, such as
PXSelectJoin<Table, Join> or PXSelectReadonly2<Table, Join>.
2. In the Join type parameter of the PXSelect class, use one of the Join clauses—
such as InnerJoin, LeftJoin, RightJoin, FullJoin, or CrossJoin—that are directly
mapped to SQL JOIN clauses, as shown in the following sample BQL statement. For more
information on the use of Join clauses, see To Query Multiple Tables.
PXSelectJoin<SalesOrder,
InnerJoin<OrderDetail,
On<OrderDetail.orderNbr, Equal<SalesOrder.orderNbr>>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the joined tables.
To Aggregate Records
1. Use one of the PXSelect classes with the Aggregate type parameter, such as
PXSelectGroupBy<Table, Aggregate>.
2. In the Aggregate type parameter of the PXSelect class, specify the grouping conditions
and aggregation functions by using the Aggregate<Function> class, the GroupBy
clauses, and the Min, Max, Sum, Avg, and Count aggregation functions, as shown in
the following sample BQL statement. For more information on the use of the grouping
conditions and aggregation functions, see To Group and Aggregate Records.
PXSelectGroupBy<Product,
Aggregate<GroupBy<Product.categoryCD>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query.
SELECT Product.CategoryCD,
[MAX(Field) for other fields]
FROM Product
GROUP BY Product.CategoryCD
Related Links
• PXSelect Classes
| Accessing Data | 170
• PXSelect<Table, Where>
• PXSelect<Table>
• PXSelectGroupBy<Table, Aggregate>
• PXSelectJoin<Table, Join>
• PXSelectOrderBy<Table, OrderBy>
• PXSelectReadonly<Table, Where>
• PXSelectReadonly<Table>
• PXSelectReadonly2<Table, Join>
• PXSelectReadonly3<Table, OrderBy>
To Filter Records
You construct business query language (BQL) statements with filtering conditions by
using the Where clause in a PXSelect class that has the Where type parameter. (For more
| Accessing Data | 171
information on selecting the PXSelect class, see To Select Records By Using Traditional BQL.)
One Where clause can contain multiple conditions chained to one another by logical operators
(Or, And, and Not) and nested Where clauses (these nested clauses are equivalent to placing
conditions in brackets).
Typically, you construct a BQL statement with a condition to compare one field with another
field or a constant, or to check if the field value has been specified (that is, to compare the
field value with null). You can also use multiple conditions in the Where clause.
To compare one field with another field in the Where clause, do the following:
1. Select the comparison class that you need, such as NotEqual, Greater, or Less.
2. Specify the compared field in the first type parameter of the Where class and the
comparison in the second type parameter, as shown in the following sample BQL
statement.
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
1. Select the comparison class that you need, such as NotEqual, Greater, or Less.
2. Select one of the predefined constants—that is, the BQL class derived from the
Constant<Type> class (such as Boolean values True and False, integer Zero, datetime
Now, Today, and MaxDate, and string StringEmpty), or define your own constant as a
class derived from the Constant<Type> class.
3. Specify the compared field in the first type parameter of the Where class and the
comparison in the second type parameter, as shown in the following sample BQL
statement.
Acumatica Framework translates this BQL statement to the following SQL query,
where [list of columns] is the list of columns of the Product table.
To check whether a field value is specified, you compare the field value with null in one of
the following ways:
• To check that the field is null, use the Where<Operand, Comparison> class, as shown in
the following sample BQL statement.
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
• Use the Where<Operator> class and the logical operator Not, as shown in the
following sample BQL statement.
• Use the Where<Operand, Comparison class, as shown in the following sample BQL
statement..
The predefined constant Null cannot be used in the Where clause with Equal to select
records with null fields. The Null constant is used in Switch conditions.
• To specify multiple comparisons that are connected with the same logical operator, use
the Where<Operand, Comparison, NextOperator> class and specify its type parameters
as follows:
• In the second type parameter, specify the first comparison, such as NotEqual,
Greater, or Less.
• In the third type parameter, specify the logical operator, such as And, And2, Or,
or Or2. You can chain any number of comparisons to one another by using binary
operators with three type parameters, as shown in the following sample BQL
statement.
PXSelect<Product,
Where<Product.bookedQty, Greater<Product.availQty>,
Or<Product.availQty, Less<Product.minAvailQty>,
Or<Product.availQty, IsNull>>>>
• To write more complex conditional expressions with logical operators of different types,
use nested Where or Where2 clauses. For more information on writing complex BQL
statements, see To Compose a BQL Statement from an SQL Statement.
Related Links
• Where<UnaryOperator> Class
• Where<Operand,Comparison> Class
• Where<Operand,Comparison,NextOperator> Class
• Where2<UnaryOperator,NextOperator> Class
• Equal<Operand> Class
• NotEqual<Operand> Class
• Greater<Operand> Class
• GreaterEqual<Operand> Class
• Less<Operand> Class
• LessEqual<Operand> Class
• Like<Operand> Class
• NotLike<Operand> Class
• NotBetween<Operand1,Operand2> Class
• Between<Operand1,Operand2> Class
• IsNull Class
• IsNotNull Class
• In<Operand> Class
• In2<Operand> Class
• In3<Operand1,Operand2,Operand3,Operand4> Class
• NotIn<Operand> Class
• NotIn2<Operand> Class
• Constant<ConstType> Class
• Null Class
• Now Class
• Today Class
• Tomorrow Class
• True Class
• False Class
| Accessing Data | 175
• Zero Class
• StringEmpty Class
• MaxDate Class
To Order Records
You construct business query language (BQL) statements that include ordering of records
by using the OrderBy clause in one of the PXSelect classes that has the OrderBy type
parameter. (For more information on selecting the PXSelect class, see To Select Records By
Using Traditional BQL.)
By default, if the BQL statement does not specify ordering, Acumatica Framework adds
ordering by the data access class (DAC) key fields (in the order of field declaration) in
ascending order to the end of the SQL query. You can order the records by one column or
multiple columns, or by a condition.
To order records by one column in ascending or descending order, use the OrderBy class and
the Asc<Field> or Desc<Field> class, as shown in the following sample BQL statement.
PXSelectOrderBy<Product, OrderBy<Asc<Product.unitPrice>>>
In this statement, all Product data records are selected and are sorted by the UnitPrice
field in ascending order.
Acumatica Framework translates this BQL statement to the following SQL query,
where [list of columns] is the list of columns of the Product table.
To order records by multiple columns, use the OrderBy class and the Asc<Field,
NextField> or Desc<Field, NextField> class, as shown in the following sample BQL
statement.
PXSelectOrderBy<Product,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
| Accessing Data | 176
To sort data records according to a condition, put the Switch clause inside Asc or Desc in
OrderBy, as shown in the following sample BQL statement.
PXSelectOrderBy<Product,
OrderBy<Asc<
Switch<Case<Where<Product.availQty, Greater<Product.bookedQty>>, True>,
False>>>>
In this statement, the records with AvailQty values less or equal to BookedQty values are
ordered first.
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
Related Links
• OrderBy<List> Class
• Asc<Field> Class
• Asc<Field,NextField> Class
• Desc<Field> Class
• Desc<Field,NextField> Class
• Switch<Case> Class
• Switch<Case,Default> Class
• Case<Where_,Operand> Class
| Accessing Data | 177
• Case<Where_,Operand,NextCase> Class
• InnerJoin returns all records where there is at least one match in both tables.
• LeftJoin returns all records from the left table, and the matched records from the right
table. Where there are no matched records from the right table, null values are inserted.
• RightJoin returns all records from the right table, and the matched records from the left
table. Where there are no matched records from the left table, null values are inserted.
• FullJoin returns all records when there is a match in one of the tables.
To Join Two Tables (Inner Join, Left Join, Right Join, or Full Join)
To join two tables, use one of the Join clauses with two type parameters (such as
InnerJoin<Table, On>) and the On<Operand, Comparison> or On<Operator> class to
specify a conditional expression for joining, as shown in the following sample BQL statement.
PXSelectJoin<SalesOrder,
InnerJoin<OrderDetail,
On<OrderDetail.orderNbr, Equal<SalesOrder.orderNbr>>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the joined tables.
To cross-join two tables, use the CrossJoin<Table> class, as shown in the following sample
BQL statement.
PXSelectJoin<Product, CrossJoin<Supplier>>
| Accessing Data | 178
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the joined tables.
•
Use a Join clause with three type parameters (such as InnerJoin<Table, On,
NextJoin>). Each subsequent join clause is specified as the last type parameter of the
previous join clause, as shown in the following sample BQL statement.
PXSelectJoin<SalesOrder,
InnerJoin<OrderDetail,
On<OrderDetail.orderNbr, Equal<SalesOrder.orderNbr>>,
LeftJoin<Employee,
On<Employee.employeeID, Equal<SalesOrder.employeeID>>>>>
Acumatica Framework translates this BQL statement to the following SQL query,
where [list of columns] is the list of columns of the joined tables.
•
Use the On conditions to specify conditional expressions for joining, as shown in the
following sample BQL statement. In subsequent join clauses, the On conditions can refer
to fields from any joined table, and can contain any number of conditions chained by
logical operators as in filtering conditions.
PXSelectJoin<SalesOrder,
InnerJoin<OrderDetail,
On<OrderDetail.orderNbr, Equal<SalesOrder.orderNbr>>,
LeftJoin<Employee,
On<Employee.employeeID, Equal<SalesOrder.employeeID>>,
RightJoin<Product,
On<Product.productID, Equal<OrderDetail.productID>,
| Accessing Data | 179
And<Product.unitPrice, Equal<OrderDetail.unitPrice>>>>>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the joined tables.
Related Links
• InnerJoin<Table,On> Class
• InnerJoin<Table,On,NextJoin> Class
• InnerJoinSingleTable<Table,On> Class
• InnerJoinSingleTable<Table,On,NextJoin> Class
• LeftJoin<Table,On> Class
• LeftJoin<Table,On,NextJoin> Class
• LeftJoinSingleTable<Table,On> Class
• LeftJoinSingleTable<Table,On,NextJoin> Class
• RightJoin<Table,On> Class
• RightJoin<Table,On,NextJoin> Class
• RightJoinSingleTable<Table,On> Class
• RightJoinSingleTable<Table,On,NextJoin> Class
• FullJoin<Table,On> Class
• FullJoin<Table,On,NextJoin> Class
• FullJoinSingleTable<Table,On> Class
• FullJoinSingleTable<Table,On,NextJoin> Class
• CrossJoin<Table> Class
| Accessing Data | 180
• CrossJoin<Table,NextJoin> Class
• CrossJoinSingleTable<Table> Class
• CrossJoinSingleTable<Table,NextJoin> Class
1. Specify all grouping conditions (the GroupBy clause) and aggregation functions (such
as Min, Max, Sum, Avg, and Count) in the Aggregate clause, as shown in the following
sample BQL statement. Fields specified in GroupBy clauses are selected as is; an
aggregation function is applied to all other fields. The default Max function is used if
no function is specified for a field. If a data field has the PXDBScalar attribute, NULL is
inserted for that field.
PXSelectGroupBy<Product,
Aggregate<GroupBy<Product.categoryCD>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query.
SELECT Product.CategoryCD,
[MAX(Field) for other fields]
FROM Product
GROUP BY Product.CategoryCD
2. If necessary, insert another GroupBy clause or aggregation function as the second type
parameter of the previous GroupBy clause or aggregation function, as shown in the
following sample BQL statement.
PXSelectGroupBy<Product,
Aggregate<GroupBy<Product.categoryCD,
Sum<Product.availQty,
Sum<Product.bookedQty,
GroupBy<Product.stockUnit,
Min<Product.unitPrice>>>>>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query.
Related Links
• Aggregate<Function> Class
• GroupBy<Field> Class
• GroupBy<Field,NextAggregate> Class
• Min<Field> Class
• Min<Field,NextAggregate> Class
• Max<Field> Class
• Max<Field,NextAggregate> Class
• Sum<Field> Class
• Sum<Field,NextAggregate> Class
• Avg<Field> Class
• Avg<Field,NextAggregate> Class
• Count Class
• Count<Field> Class
To relate data views to one another, use the Current parameter, as shown in the following
sample code.
Acumatica Framework translates the BQL query of the second view in the sample BQL
code to the following SQL statement. In this SQL query, [parameter1] is the DocNbr
value and [parameter2] is the DocType value taken from the Current property
of the DocTransaction cache; [list of columns] is the list of columns of the
DocTransaction table.
To pass a specific value to the SQL query, use the Required parameter in the BQL statement
and specify the needed value as the Select() method argument. The value passed to
Select() must be of the same type as the type of the specified field.
The Required parameter should be used only in the BQL statements that are directly
executed in the application code. The data views that are queried from the UI will not
work if they contain Required parameters.
The code below shows the execution of a BQL statement with the Required parameter.
Acumatica Framework translates the previous BQL query to the following SQL
statement. In this SQL query, [parameter] is the value of the product.CategoryCD
variable at the moment the Select() method is invoked; [list of columns] is the
list of columns of the Category table.
To pass multiple values to the SQL query, use multiple Required parameters in the BQL
statement and specify the needed values as the Select() method arguments in the order
in which the parameters are specified in the BQL statement. The number of Required
parameters must match the number of parameters passed to the Select() function.
The Required parameters should be used in only the BQL statements that are
executed in the application code.
The following code shows an example of a BQL statement with two Required parameters.
Acumatica Framework translates the previous BQL query to the following SQL
statement, where [list of columns] is the list of columns of the Document table.
1. Add the PXSelector attribute with a substitute key to a DAC field, as shown in the
following example.
[PXSelector(typeof(Search<Product.productID>),
| Accessing Data | 184
new Type [] {
typeof(Product.productCD),
typeof(Product.productName)
},
SubstituteKey = typeof(Product.productCD))]
public virtual int? ProductID { get; set; }
In this example, PXSelector replaces the ProductID field in the user interface with
the human-readable ProductCD field. In the UI control for this field, the user enters
a ProductCD value. The PXSelector attribute implements the FieldUpdating event
handler, which replaces the ProductCD value with the corresponding ProductID value.
2. Use the Optional parameter to select records by the external presentation of the field
value, as shown in the following code for OrderDetail records.
3. In the Select() method, provide values for all Optional, Required, and Argument
parameters up to the last Required or Argument parameter in the BQL statement, as
shown in the following sample code.
Acumatica Framework translates the BQL query in the code to the following SQL
statement, where [list of columns] is the list of columns of the OrderDetail
table.
1. Define the PXSelect data view with the Argument parameter whose type parameter
specifies the data type of the expected value, as shown in the following sample BQL
query.
PXSelect<TreeViewItem,
Where<TreeViewItem.parentID, Equal<Argument<int?>>>,
OrderBy<Asc<TreeViewItem.parentID>>> GridDataSource;
Acumatica Framework translates the previous BQL query to the following SQL
statement. In this SQL query, [parameter] will contain the value received from
the UI control and passed to the Select() method; [list of columns] is the list
of columns of the TreeViewItem table.
2. Define the data view delegate with the parameters through which you can access the
values passed from the UI. (You can find more information on how to use data view
delegates in .)
When a BQL statement with the Argument parameter is executed in code, the
value must be specified in the parameters of the Select() method.
Related Links
• Current<Field> Class
| Accessing Data | 186
• Current2<Field> Class
• Required<Field> Class
• Optional<Field> Class
• Optional2<Field> Class
• Argument<ArgumentType> Class
1. Compose the expression by using arithmetic operations. For example, you can calculate
product reorder discrepancy by using the following BQL expression, where the decimal_0
constant represents the 0 decimal value. IsNull returns the first argument if it is not
null or the second argument otherwise.
Minus<
Sub<Sub<IsNull<Product.availQty, decimal_0>,
IsNull<Product.bookedQty, decimal_0>>,
Product.minAvailQty>>
Acumatica Framework translates the previous BQL statement to the following SQL
query.
1. Compose the expression by using arithmetic operations. For example, you can calculate
product reorder discrepancy by using the following BQL expression, where the decimal_0
constant represents the 0 decimal value. IsNull returns the first argument if it is not
null or the second argument otherwise.
Minus<
Sub<Sub<IsNull<Product.availQty, decimal_0>,
IsNull<Product.bookedQty, decimal_0>>,
Product.minAvailQty>>
Acumatica Framework translates the previous BQL statement to the following SQL
query.
2. Use the calculated expression in a BQL statement, as shown in the following example.
PXSelect<Product,
Where<Minus<
Sub<Sub<IsNull<Product.availQty, decimal_0>,
IsNull<Product.bookedQty, decimal_0>>,
Product.minAvailQty>>,
NotEqual<decimal_0>>>
Acumatica Framework translates the previous BQL statement to the following SQL
query, where [list of columns] is the list of columns of the Product table.
Related Links
• Add<Operand1,Operand2> Class
• Sub<Operand1,Operand2> Class
• Mult<Operand1,Operand2> Class
• Div<Operand1,Operand2> Class
| Accessing Data | 188
• Minus<Operand> Class
For example, suppose that you need to convert to BQL the following SQL statement. In
this SQL query, we use the * sign to indicate that all columns of the Product table should
be selected.
2. Replace the names of columns with the names of class fields that correspond to the
columns in data access classes (DACs). That is, change the uppercase letter in the name
of each column to the lowercase, as shown in the following sample code. In this sample
code, the changes are shown in bold type.
3. If your SQL statement contains constants, replace it with either one of the predefined
constants or your own constant. (For details on using constants, see To Compare a Field
with a Constant and the description of the Constant<ConstType> class.) If you need to
change the values of the constants at runtime, replace the constants with parameters, as
described in To Use Parameters in Traditional BQL.
4. Find the JOIN, WHERE, GROUP BY, and ORDER BY clauses that you have in the SQL
statement. Depending on the included clauses, select one of the PXSelect classes, and
replace SELECT * FROM with this class in your SQL statement. For details on selection
of the PXSelect class, see To Select Records By Using Traditional BQL. For the list of all
PXSelect classes, see PXSelect Classes.
In the sample code that has been presented in this topic, you would use the
PXSelectJoin<Table, Join, Where, OrderBy> class, and you would change the
sample code as follows. (The changes are shown in bold type.)
PXSelectJoin<Product,
INNER JOIN SupplierProduct
ON SupplierProduct.productID = Product.productID
INNER JOIN Supplier
ON Supplier.accountID = SupplierProduct.accountID,
WHERE (Product.bookedQty IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND(Product.active = 1
OR Product.active IS NULL)
AND(Product.bookedQty > Product.availQty
OR Product.availQty < Product.minAvailQty))
OR Product.availQty IS NOT NULL,
ORDER BY Product.unitPrice, Product.availQty DESC>
a. Replace the last JOIN clause with the corresponding BQL Join clause. You would
change the sample code of this topic as follows. (The changes are shown in bold
type.)
PXSelectJoin<Product,
INNER JOIN SupplierProduct
ON SupplierProduct.productID = Product.productID
InnerJoin<Supplier,
ON Supplier.accountID = SupplierProduct.accountID>,
WHERE (Product.bookedQty IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND(Product.active = 1
OR Product.active IS NULL)
AND(Product.bookedQty > Product.availQty
| Accessing Data | 190
b. Chain other JOIN clauses to one another, as described in To Query Multiple Tables. You
would change the sample code of this topic as follows. (The changes are shown in
bold type.)
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
ON SupplierProduct.productID = Product.productID,
InnerJoin<Supplier,
ON Supplier.accountID = SupplierProduct.accountID>>,
WHERE (Product.bookedQty IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND(Product.active = 1
OR Product.active IS NULL)
AND(Product.bookedQty > Product.availQty
OR Product.availQty < Product.minAvailQty))
OR Product.availQty IS NOT NULL,
ORDER BY Product.unitPrice, Product.availQty DESC>
• For a single condition or groups that start with a simple condition, replace the ON
clause with On.
• For groups that start with a group of conditions, replace the ON clause with On2.
With these replacements, the sample code used in this topic would be changed to the
following code. (The changes are shown in bold type.)
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID = Product.productID>,
InnerJoin<Supplier,
On<Supplier.accountID = SupplierProduct.accountID>>>,
WHERE (Product.bookedQty IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND(Product.active = 1
OR Product.active IS NULL)
AND(Product.bookedQty > Product.availQty
OR Product.availQty < Product.minAvailQty))
OR Product.availQty IS NOT NULL,
ORDER BY Product.unitPrice, Product.availQty DESC>
6. If your SQL statement includes a WHERE clause, replace the WHERE clause and each pair
of parentheses that encloses each group of conditions in the WHERE clause with a Where,
Where2, Not, or Not2 clause, as follows:
| Accessing Data | 191
• Not is used for groups that start with a simple condition but are preceded with the
logical NOT.
• Not2 is used for groups that start with a group of conditions but preceded with the
logical NOT.
With these replacements, the sample code used in this topic would be changed to the
following code. (The changes are shown in bold type.)
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID = Product.productID>,
InnerJoin<Supplier,
On<Supplier.accountID = SupplierProduct.accountID>>>,
Where2<Where<Product.bookedQty, IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND Where<Product.active = 1,
OR Product.active IS NULL>
AND Where<Product.bookedQty > Product.availQty,
OR Product.availQty < Product.minAvailQty>>,
OR Product.availQty IS NOT NULL>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
7. In each BQL Where or On clause, replace the logical operators (either AND or OR) to And,
Or, And2, or Or2, as follows:
a. Replace the last AND or OR in each BQL Where or On clause with the And or Or
operator, respectively, as shown in the following code. (The changes are shown in
bold type.)
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID = Product.productID>,
InnerJoin<Supplier,
On<Supplier.accountID = SupplierProduct.accountID>>>,
Where2<Where<Product.bookedQty, IS NOT NULL
AND Product.availQty IS NOT NULL
AND Product.minAvailQty IS NOT NULL
AND Where<Product.active = 1,
Or<Product.active IS NULL>>
And<Where<Product.bookedQty > Product.availQty,
Or<Product.availQty < Product.minAvailQty>>>>,
Or<Product.availQty IS NOT NULL>>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
| Accessing Data | 192
b. In each BQL Where or On clause, if the AND or OR is located before a simple condition,
replace it with And or Or, respectively. If the condition is preceded by NOT, wrap it in
Not. With these replacements, the sample code used in this topic would be changed
to the following code. (The changes are shown in bold type.)
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID = Product.productID>,
InnerJoin<Supplier,
On<Supplier.accountID = SupplierProduct.accountID>>>,
Where2<Where<Product.bookedQty, IS NOT NULL,
And<Product.availQty IS NOT NULL,
And<Product.minAvailQty IS NOT NULL,
AND Where<Product.active = 1,
Or<Product.active IS NULL>>
And<Where<Product.bookedQty > Product.availQty,
Or<Product.availQty < Product.minAvailQty>>>>>>,
Or<Product.availQty IS NOT NULL>>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID = Product.productID>,
InnerJoin<Supplier,
On<Supplier.accountID = SupplierProduct.accountID>>>,
Where2<Where<Product.bookedQty, IS NOT NULL,
And<Product.availQty IS NOT NULL,
And<Product.minAvailQty IS NOT NULL,
And2<Where<Product.active = 1,
Or<Product.active IS NULL>>,
And<Where<Product.bookedQty > Product.availQty,
Or<Product.availQty < Product.minAvailQty>>>>>>>,
Or<Product.availQty IS NOT NULL>>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
8. In each Where or On clause, replace the groups that use arithmetic operations with the
corresponding BQL operators, as described in To Use Arithmetic Operations.
9. In each Where or On clause, replace each comparison with the corresponding comparison
operator, such as Equal, Greater, or IsNull. For more information on constructing
comparisons, see To Filter Records.
The following sample code includes these changes (shown in bold type).
| Accessing Data | 193
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID, Equal<Product.productID>>,
InnerJoin<Supplier,
On<Supplier.accountID, Equal<SupplierProduct.accountID>>>>,
Where2<Where<Product.bookedQty, IsNotNull,
And<Product.availQty, IsNotNull,
And<Product.minAvailQty, IsNotNull,
And2<Where<Product.active, Equal<True>,
Or<Product.active, IsNull>>,
And<Where<Product.bookedQty, Greater<Product.availQty>,
Or<Product.availQty, Less<Product.minAvailQty>>>>>>>>,
Or<Product.availQty, IsNotNull>>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
10.Align logical operators of the same level so that they have the same indentation and so
that each simple condition is placed on a separate line. Do not add line breaks before
nested Where clauses.
11.If your SQL statement includes the GROUP BY clause, do the following:
b. Chain the GroupBy clause and aggregation functions (such as Min, Max, Sum, Avg, and
Count) to one another as described in To Group and Aggregate Records.
12.If your SQL statement includes the ORDER BY clause, do the following:
a. Replace the ORDER BY clause with the OrderBy clause. The following sample code
shows this change (with changes shown in bold type).
PXSelectJoin<Product,
InnerJoin<SupplierProduct,
On<SupplierProduct.productID, Equal<Product.productID>>,
InnerJoin<Supplier,
On<Supplier.accountID, Equal<SupplierProduct.accountID>>>>,
Where2<Where<Product.bookedQty, IsNotNull,
And<Product.availQty, IsNotNull,
And<Product.minAvailQty, IsNotNull,
And2<Where<Product.active, Equal<True>,
Or<Product.active, IsNull>>,
And<Where<Product.bookedQty, Greater<Product.availQty>,
Or<Product.availQty, Less<Product.minAvailQty>>>>>>>>,
Or<Product.availQty, IsNotNull>>,
OrderBy<Product.unitPrice, Product.availQty DESC>>
b. Chain the Asc and Desc operators to one another, as described in To Order Records.
The following sample code shows this change (with changes shown in bold type).
PXSelectJoin<Product,
| Accessing Data | 194
InnerJoin<SupplierProduct,
On<SupplierProduct.productID, Equal<Product.productID>>,
InnerJoin<Supplier,
On<Supplier.accountID, Equal<SupplierProduct.accountID>>>>,
Where2<Where<Product.bookedQty, IsNotNull,
And<Product.availQty, IsNotNull,
And<Product.minAvailQty, IsNotNull,
And2<Where<Product.active, Equal<True>,
Or<Product.active, IsNull>>,
And<Where<Product.bookedQty, Greater<Product.availQty>,
Or<Product.availQty, Less<Product.minAvailQty>>>>>>>>,
Or<Product.availQty, IsNotNull>>,
OrderBy<Asc<Product.unitPrice, Desc<Product.availQty>>>>
• Check that all lines except the last line of the BQL statement end with a comma.
• Ensure that the number of closing angle brackets equals the number of opening angle
brackets.
Related Links
In this chapter, you can find information on how to create LINQ queries. For the general
information about data querying, see Querying Data in Acumatica Framework.
In This Chapter
For details about BQL, see Creating Fluent BQL Queries and Creating Traditional BQL Queries. For
more information about the differences between LINQ and BQL, see Comparison of Fluent BQL,
Traditional BQL, and LINQ.
In LINQ expressions, to access data from the database tables, you use data access classes
(DACs). For details on DACs, see Data Access Classes.
You use property fields of DACs when you need to specify table columns in LINQ
expressions. (The name of the property field starts with an uppercase letter. Do not confuse
it with the class field, which has the same name but starts with lowercase letter.)
Query Syntax
To configure a LINQ query, you can use the following variants of syntax:
• Query expressions, which use standard query operators from the System.Linq
namespace (such as where or orderby) or Acumatica Framework-specific operators
from the PX.Data.SQLTree namespace (such as SQL.BinaryLen, which is shown in the
following example of this syntax).
• Explicit (method-based) syntax. The arguments of the methods used in this syntax are
lambda expressions. In these expressions, you can use the standard C# operators and
Acumatica Framework-specific operators from the PX.Data.SQLTree namespace (such as
SQL.BinaryLen, which is shown in the following code). The code below is equivalent to
the query expression shown above.
For details about composing LINQ queries, see To Select Records by Using LINQ. In the code
examples of this guide, we use explicit syntax.
//BQL statement
var Products = new PXSelect<Product,
Where<Product.productCD, Like<string_D>>>(graph);
//Use of LINQ for the result of the BQL query
var goods = Products.Select()
.Where(p => p.GetItem<Product>().StockUnit == "item");
//Execution of the query
foreach (var good in goods) {
var prod = good.GetItem<Product>();
}
However, you cannot work with the query defined with LINQ by using BQL.
For details about how to use LINQ and BQL simultaneously, see To Append LINQ Expressions to
BQL Statements.
Related Links
• Call the ToList or ToArray method for the query, as shown in the following code.
• Iterate the query by using the foreach statement, as shown in the following code.
The system merges with PXCache the records the system has retrieved from the database by
using the LINQ queries, as described in Merge of the Records with PXCache.
You may need to explicitly merge the records retrieved from the database with a particular
PXCache object. To merge the records with PXCache explicitly, you use the Merge method, as
shown in the following code example.
You also can specify that the query should not be merged with PXCache by using the
ReadOnly method, as shown in the following code example.
Related Links
The following code shows an example of a situation when the system falls back to LINQ
to Objects mode. In this example, the system selects from the database all records
from the CRCase table, and then, in memory, orders the retrieved records by the
Date column and selects the records that satisfy the condition specified by using the
MyHelpers.IsHighPriority function.
If the system falls back to LINQ to Objects, only the results of the base PXSelectBase
query are merged with PXCache as described in Merge of the Records with PXCache. The
Merge and ReadOnly methods do not affect the merge of records with PXCache for the
queries that caused fallback.
The LINQ fallback is supported in Acumatica Framework for compatibility with previous
versions. The system writes to the trace log about all situations in which the system falls
back to LINQ to Objects mode. Therefore, we strongly recommend that you investigate the
trace log for such issues and fix the issues in one of the following ways:
• Remove the custom C# functions that cause fallback so that the full query is executed in
the database.
• Append the AsEnumerable() method to the part of the query that can be converted
to SQL, and add after it the conditions that include custom C# functions. In this case,
the system does not waste resources trying to build the SQL query tree for the whole
query. Instead, the system builds the SQL query tree for the part of the query that has
AsEnumerable() appended and performs the corresponding request to the database,
while the custom C# conditions of the query are processed in memory. For example, the
code example above can be modified as follows.
The system executes the following SQL query for the code above, where [list of
columns] is the list of columns of the CRCase table.
Related Links
After you have composed a LINQ expression, to execute the query defined by this
expression, you have to call the ToList or ToArray method for the query, or iterate
the query by using the foreach statement. For example, the following code executes
the query defined by a LINQ expression.
For details about the execution of LINQ expressions, see Deferred LINQ Query Execution.
using PX.Data;
using PX.Data.SQLTree;
using System.Linq;
• Make sure that the application database has the database tables from which you are
going to request data, and that the application defines the data access classes (DACs) for
these tables. For more information on defining DACs, see Data Access Classes.
| Accessing Data | 200
To Filter Records
To filter records in the database table to be retrieved, construct the LINQ expression by
using the Where LINQ method and the needed conditions. In the conditions, use the property
field defined in the DAC, such as Product.ProductID.
The following LINQ expression uses the C# logical operators (||, &&, and !) to define
multiple conditions.
This LINQ expression is equivalent to the following SQL query. In this SQL query,
[list of columns] is the list of columns of the Product table.
To Order Records
To order records to be retrieved from the database table, construct the LINQ expression by
using the OrderBy or OrderByDescending LINQ method and the needed property fields of
the DAC, such as Product.ProductID.
The following sample LINQ expression selects all Product data records and sorts them by
the UnitPrice field in ascending order.
This LINQ expression is equivalent to the following SQL query, where [list of
columns] is the list of columns of the Product table.
To join multiple tables, construct the LINQ expression by using the Join, LeftJoin,
GroupJoin, and FullJoin LINQ methods and the needed property fields of DACs, such as
SalesOrder.OrderNbr.
The following sample LINQ expression performs an inner join of the SalesOrder and
OrderDetail DACs by the OrderNbr field.
| Accessing Data | 201
This LINQ expression is equivalent to the following SQL query, where [list of
columns] is the list of columns of the Product table.
1. Construct the LINQ expression by using the GroupBy LINQ method and the needed
property fields of the DAC, such as Product.CategoryCD. (The name of the property
field starts with an uppercase letter. Do not confuse it with the class field, which has the
same name but starts with lowercase letter.)
You have to use the Select method after GroupBy to eliminate the number of
requests that the system performs to the database.
The following sample LINQ expression groups the records of the Product table by the
CategoryCD field.
SELECT Product.CategoryCD
FROM Product
GROUP BY Product.CategoryCD
To select particular columns, specify the corresponding property fields of DACs in the Select
clause.
The following example selects the values of the ProductCD and AvailQty fields for all
records of the Product table.
The system executes the following SQL query for the code above.
Related Links
using PX.Data;
using PX.Data.SQLTree;
using System.Linq;
• Make sure that the application database has the database tables from which you are
going to request data, and that the application defines the data access classes (DACs) for
these tables. For more information on defining DACs, see Data Access Classes.
1. Configure a BQL query derived from PXSelectBase either in fluent BQL or in traditional
BQL.
2. Call the Select() method of PXSelectBase, and append the LINQ query to the result.
Because the result of the Select() method call is a PXResultset<> object, you need
to cast it to a DAC type by using the PXResult.GetItem<DacType>() method or direct
casting.
The following example appends LINQ joining and filtering to a BQL query.
using PX.Data;
using PX.Data.SQLTree;
using System.Linq;
using PX.Data.BQL.Fluent;
The system executes the following SQL query for the code above. In this SQL query,
[list of columns] is the list of columns of the Product and SupplierProduct
tables.
In This Chapter
PXDBDefault
The PXDBDefault attribute specifies the default value for a data field. You should use this
attribute to insert the default value, which is the foreign key to the master DAC.
PXDBDefault works similarly to PXDefault and obtains its value from the Current
property of the PXCache object that holds data records of the specified class. However, the
PXDBDefault attribute is specially intended to insert the default value that is the key to the
parent record. Unlike PXDefault, the PXDBDefault attribute supports the identity key field
of the master DAC and inserts the actual default value after the parent record is saved to the
database.
If you implement a master-detail relationship, you should use the PXDBDefault attribute to
bind the detail data record fields (foreign key fields) to the master data record key fields.
If the master data record is new and uses the identity key generated by the database, its
key field will be set to a real value only when the master record is saved to the database.
So if a detail data record is created before the master data record is saved for the first time,
the detail data record field will be set to the temporary value of the master identity field.
However, the PXDBDefault attribute will replace the temporary value with the actual one
when the detail data record is saved to the database.
//SupplierProduct.SupplierID
...
[PXDBDefault(typeof(Supplier.supplierID))]
public virtual int? SupplierID
{...
}
PXParent
We recommend that you add the PXParent attribute to the first foreign key field of the child
DAC (although it is possible to add the attribute to any field). Because the attribute specifies
the master-detail relationship between classes, it enables cascading deletion of the child
records once a parent record is deleted.
The parent data record is defined by the BQL Select<> statement specified in the attribute.
Typically, the query includes a Where<> clause that adds conditions for the parent’s key fields
to equal the child’s key fields. In this case, to specify the values of the key fields of the child
data record, you use the Current parameter.
| Accessing Data | 205
In the following code example, PXParent specifies the current Supplier record as
the parent for the SupplierProduct record. The PXParent attribute is added on the
SupplierProduct.SupplierID field in the SupplierProduct DAC.
//SupplierProduct.SupplierID
...
[PXParent(
typeof(SelectFrom<Supplier>.
Where<Supplier.supplierID.IsEqual<SupplierProduct.supplierID.FromCurrent>>))]
public virtual int? SupplierID
{...
}
To select master-detail data, you define two data views in the graph (see the code below).
In this code example, the master data view selects data records of the Supplier class, while
the detail data view selects records of the SupplierProduct class. To select the details for
a particular master record, you specify the master key field in the Current parameter of the
data view type. In the Current parameter, Acumatica Framework inserts the value from the
Current property of the PXCache object that works with the specified DAC.
The order of data views in the graph defines the order of the insertion, update, and deletion
of data records in the database. The framework inserts and updates data records in the
order in which the data views are defined, and deletes the records in the reverse order.
Thus, you have to define the master data view before the detail data view to enabledetails
to be saved and deleted correctly. (The master data record is the first to be inserted in the
database and the detail data records are the first to be deleted from the database.)
In the code of an Acumatica Framework-based application, you can define the relationship
between two tables as follows:
| Accessing Data | 206
• To define a primary key of a table, for the set of key fields of the data access class (DAC)
that corresponds to the table, you set the IsKey property of the data type attribute to
true.
• To define a foreign key of a table, in the DAC that corresponds to the table, you
mark the field that contains the foreign key with one of the following attributes:
PXForeignReference, PXSelector, or PXParent.
To select a record from the database by its primary or foreign key, you can use a Select
statement in business query language (BQL) or use the methods of the attributes mentioned
above.
Another way to define a relationship between two tables is to use the PrimaryKeyOf and
ForeignKeyOf classes that are specially designed for the definition of primary and foreign
keys.
This approach, which is described in this topic, provides the following advantages:
• These classes provide static information that a compiler can use to identify errors in the
code.
• You can use runtime information about primary keys to select records by their keys.
• These classes and methods have no other meanings and use cases; conversely, the
PXForeignReference, PXSelector, and PXParent attributes can be used for other
purposes.
• These classes and methods are optimized for the selection of records from the database;
therefore, using them improves database access performance on record selection.
using PX.Data.ReferentialIntegrity.Attributes;
You can define a foreign key based on the primary key of the referenced table, as shown in
the following code.
If a primary key is defined for a DAC, you can select a record by using the values of the key
fields of the record, as shown in the following example.
If a foreign key is defined for a DAC, you can select the parent and child records, as shown
in the following code.
You can use static foreign keys, defined as described in Definition of a Foreign Key, for the
configuration of the PXForeignReference and PXParent attributes, as shown in the following
example.
[...]
[PXParent(typeof(SOOrderFK))]
public virtual String OrderNbr { get; set; }
public abstract class orderNbr : PX.Data.IBqlField { }
[...]
[PXForeignReference(typeof(InventoryFK))]
public virtual Int32? InventoryID { get; set; }
public abstract class inventoryID : PX.Data.IBqlField { }
}
Because all primary keys, which are defined as described in this topic, implement the
IPrimaryKey interface, you can use primary and foreign keys in the scope of custom
attributes, as shown in the following example.
Related Links
BQL includes an operand that you can use to insert the values of the Current data record
into a BQL query. In the code below, the Current property is used in data views to select
and retrieve linked data. The Suppliers and SelectedSupplier data views retrieve
records from the same PXCache object, because they have the same main DAC. The
SupplierProducts data view retrieves records of the SupplierProduct class that have
the specified SupplierID. The SupplierID is retrieved from the Current property of the
PXCache object for Supplier.
The framework automatically assigns the Current property to the following records:
• Each data record retrieved from the database and displayed in the UI (or requested by
the web services APIs)
• The last modified data record that has been inserted or updated from the UI, after the
updated data is posted to the server
| Accessing Data | 210
• The last modified data record that has been inserted or updated from code, after you
invoke the Insert() or Update() method on the cache object
The Current property returns the object of the main DAC type of the data view. The
Current property of the data view and the Current property of the Cache object of the data
view return the same record (see the following code).
After you create a graph instance in the code, the Current property of all cache
objects of the graph returns null.
You can get a value from the current data record and specify it as a BQL parameter in a data
view type or in an attribute of a DAC field. In the following code, a DAC field is specified in
the Current parameter of the BQL statement in a data view.
You can set the Current property of a data record of the type the PXCache object works
with. When you assign the Current property of a PXCache<> object, you should select a data
record from the database by using DAC key field values. If a data record with these key field
values exists in the database, the Current property is assigned to the retrieved record. If
no such data record exists in the database, the property is set to null. Setting the Current
property gives you the ability to do the following:
• Open a form displaying the specified data record when you redirect to the form from
another one
We don't recommend that you set the Current property in other cases, such as in event
handlers, because doing this may cause the application to work incorrectly.
The assignment of the Current property raises the RowSelected event for the current
data record.
You must use the Search<>() generic method of a data view object to retrieve a record from
the database and assign the retrieved record to the Current property, as the following code
shows.
//The data view works with the PXCache object that holds SalesOrder data records
public SelectFrom<SalesOrder>.OrderBy<SalesOrder.orderNbr.Asc>.View Orders;
| Accessing Data | 211
...
//To search for the record in the database,
//you can use the generic Search<> method of the data view
graph.Orders.Current = graph.Orders.Search<SalesOrder.orderNbr>(order.OrderNbr);
using PX.Data.ReferentialIntegrity.Attributes;
2. Use the primary key to select a record, as shown in the following code.
using PX.Data.ReferentialIntegrity.Attributes;
2. Use the compound primary key to select a record, as shown in the following code.
Related Links
1. In the DAC of the parent table, define the primary key, as described in To Define a Primary
Key. The following code shows an example of the definition of the primary key that is
used in the following code examples in this instruction.
2. In the DAC of the child table, define the foreign key based on the primary key of the
parent table, as shown below.
3. Use the foreign key to select the parent record or child records, as shown in the following
code.
Related Links
In This Chapter
• Session
• Update(object)
• Delete(object)
For Insert(object), Acumatica Framework checks whether a data record with the specified
key values already exists in the cache object. If the record exists, nothing is inserted into
the cache, and the existing record isn't modified in the cache; that is, the Insert(...)
method returns null. If the record does not exist, the new record is inserted into the cache
object; the method returns the new data record.
On Update(object), the framework checks whether a data record with such key values
already exists in the cache object. If the record exists, it is updated. If the record doesn't
| Accessing Data | 214
exist in the cache, the framework retrieves the record with such keys from the database and
places it into the cache. If there is no such record in the database, the framework invokes
Insert() for the record.
On Delete(object), the framework sets the Deleted status for the record if it exists in the
cache object. It does this in three steps. First, the framework checks whether a data record
with the key values of the provided object already exists in the cache. If the record doesn't
exist in the cache, the framework retrieves the record with these keys from the database.
If the record exists in the cache or in the database, the framework sets its status within the
cache to Deleted. The record isn't removed from the cache object.
The modified records remain in the cache object until the Persist() method of the graph
is invoked, or until the Cache.Clear() method of the data view is invoked, which removes
all records from the cache object. You can remove all records from all cache objects of the
graph by invoking the Clear() method of the graph object.
For each of the data modification methods, the framework raises the corresponding
sequence of events and changes the status of the record in the cache object as shown in the
diagram below. Once it is retrieved from the database, a record has the Notchanged status
until it is modified. An inserted record maintains the Inserted status even if it is updated.
When the inserted record is deleted, it is assigned the specific InsertedDeleted status. If
you want to make sure that a record has been marked as deleted within the current session,
you have to check the record for both the Deleted status and the InsertedDeleted status.
To obtain the status of the record, invoke the GetStatus() method of the cache object
for the needed DAC object. The status of the record is one of the following values of the
PXEntryStatus enumeration:
• Notchanged
• Updated
• Inserted
• Deleted
• InsertedDeleted
| Accessing Data | 215
You can obtain the modified records from the collections of the PXCache object that
correspond to the type of modification, as follows:
• Cache.Inserted, which retrieves data records that have the Inserted status
• Cache.Updated, which retrieves data records that have the Updated status
• Cache.Deleted, which retrieves data records that have the Deleted status.
The Cache.Dirty collection is used to retrieve all data records that have the
Inserted, Updated, or Deleted status.
Records that have other statuses (such as InsertedDeleted) aren't retrieved by these
collections. For more information on these statuses, see PXCache<TNode> Class in the API
Reference.
| Accessing Data | 216
We don't recommend that you iterate through the Cached collection. For performance
optimization, this collection may not retrieve all available data records.
The framework raises the applicable events and updates the status of the record in the
following cases as described:
• When you invoke the Insert(), Update(), or Delete() method of a PXCache object,
the framework raises field-level events for each field when the Insert() or Update()
method is invoked, and for only key fields when the Delete() method is invoked; the
framework then raises all row-level events for the data record.
document.DocNbr = lastNumber;
Documents.Update(document);
• When you invoke the SetValueExt<Field>() method of a cache object, the framework
raises field-level events for the specified field only. The framework doesn't invoke row-
level events and doesn't update the status of the record. You can update the status of
the record manually by invoking the SetStatus() method of the cache. However, you
should be careful with skipped field-level and row-level events because changing the
status manually may cause missing logic and incorrect data update.
view.Cache.SetValueExt<Document.docNbr>(row, lastNumber);
The framework neither raises any event nor updates the status of the record in the following
cases:
document.DocNbr = lastNumber;
• If you assign a new value to a field by invoking the SetValue<Field>() method of the
cache object
Searching for a data record in the cache is helpful when you want to check whether the
data record has already been modified during the current user session. To search for a data
record, you can use the Locate() method, which returns the data record if it exists in the
PXCache object. The Locate() method searches for the record by key values. If the record
doesn't exist, the method returns null. No query is executed to the database in the Locate()
method.
You can invoke data modification methods on a PXCache object through a data view. The
methods are invoked on the PXCache object of the first DAC specified in the data view
type (main DAC of the data view). The following code shows equivalent invocations of the
Insert() method on the PXCache object that stores ShipmentLine data records in a graph.
Session
The Acumatica ERP server creates a separate session for each browser tab or window that
opens an Acumatica ERP form.
The server creates the first session for a user after the user authorization when the starting
form is loading. Then the server does the following:
• Saves the user authorization data (.ASPXAUTH) and the session ID (ASP.NET_SessionID)
in the browser cookies for the website URL
• Creates the shared session data to be used for the Acumatica ERP forms opened in new
browser tabs and windows
• Saves the shared session data in the storage that is specified in the website configuration
When the user opens a form of Acumatica ERP in a new browser tab, the server creates
a new session that is based on the previous session data. To access the shared data, the
server uses the session ID from the cookies, which are added to the request by the browser.
The following diagram shows how the server of Acumatica ERP manages the shared session
data that is used for multiple sessions of a single user.
| Accessing Data | 218
If the session data has been changed during the processing of a request, the server
updates the data in the shared session data store. For example, if the user clicks Copy on
a form toolbar to copy the form data, the data is stored in the shared session, so that it is
accessible for the Paste action in another session of the same user.
To distinguish different sessions that have the same ASP.NET_SessionID, the server adds
to each new session a unique identifier that consists of the W character and a number value
wrapped in parentheses. In the browser, you can see such an identifier in the site URL, as
with the bolded part in the following example: http://localhost/MySite/(W(3))/Main.aspx?
ScreenId=AR301000.
The following diagram shows difference in storing session data on a stand-alone server and
in a cluster. On a stand-alone Acumatica ERP server, session data is stored in the server
memory. In a cluster of application servers, session data must be serialized and stored in a
high-performance remote server, such as Redis or Microsoft SQL.
The cost of serialization and the amount of data that need to be shared between application
servers is often the main challenge to scaling complex business applications horizontally.
• Objects on the application server are created on each request and disposed after
the request execution. The application state is preserved in the session through the
serialization mechanism.
| Accessing Data | 220
• Data serialized into the session is minimized to store only modified data (inserted,
deleted, held and modified records). (Serialization and retrieval times are directly
proportional to the size of the serialized data.)
• The rest of the data is extracted from the database on demand and built around the
session data. (A custom algorithm that extracts only the data required for the current
request execution from the database is implemented.)
• Hash tables, constraints, relations, and indexes concerned with the execution of business
logic are created strictly on demand. This technique allows the user to avoid execution
of these operations on each request if not needed. (Creation of indexes, constrains, hash
tables, and relations consumes a significant amount of CPU and runtime memory.)
The system commits the changes to the database in the following cases:
A graph instance exists on the server only while a user request is being processed, and it
is destroyed right after this processing. The following diagram shows that a graph instance
is created to process a user request on the Acumatica ERP server and destroyed once
processing is completed.
| Accessing Data | 221
On a stand-alone Acumatica ERP server, session data is stored in the server memory.
In a cluster of Acumatica ERP servers, session data must be serialized and stored in
external high-performance session state storage. (For more information on storing
session data in a cluster, see Session Sharing Between Application Servers.)
For a user request on an Acumatica ERP form, the following operations are executed in the
system:
1. The application server creates a graph instance that is specified in the TypeName property
of the PXDataSource control of the form. (For more information about the initialization
of graph views, caches, actions, and event handlers, see Initialization of an Event Handler
Collection.)
2. If the user session contains graph data that has been stored during a previous request,
the system loads the graph state and the cache data from the session.
3. The graph instance processes the requested data on the data view that is specified in
the ASPX code in the DataMember property of the control container for the data to be
processed. To process the data, the system invokes the ExecuteSelect, ExecuteInsert,
| Accessing Data | 222
ExecuteUpdate, or ExecuteDelete method of the graph, based on the request type. The
invoked method implements the logic of the appropriate scenario to add the request data
to the cache and to execute the event handlers defined for the data fields and records in
the cache. (See Data Manipulation Scenarios for details.) The cache then merges the data
retrieved from the database with the data restored from the session, and the application
accesses the data as if the entire data set had been preserved from the time of the
previous request.
4. The graph instance returns the request results to the PXDatasource control of the form.
5. The system stores in the session the graph state and the modified data of the cache.
Because the graph instance is no longer being used by the application server,
the .NET Framework garbage collector then clears the memory allocated for the
graph instance.
While a graph is instantiated, all the cached data of the graph is saved in the appropriate
PXCache objects that are created in the graph instance based on the data access class
(DAC) declarations. To preserve the modified entity data between user requests, the cache
controller saves the Updated, Inserted, Deleted, and Held collections of each PXCache
object in the session.
The following diagram shows how the graph state and cache data are stored in the Session
object.
• The items of the graph instance are stored in the GraphInfo collection of the Session
object as a key-value pair, where the key is equal to the full name of the graph.
• The items of a PXCache object are stored in the CacheInfo collection of the Session
object as a key-value pair, where the key consists of the following parts separated by the
$ symbol:
When you instantiate a graph from code, the system will not load data from the
session, because you may want to perform redirection or other processing. You
can direct the system to load this data by using the PXPreserveScope class, as the
following code snippet shows.
A slot provided by the PXContext class exists in the memory of the application server only
during the current HTTP request. Therefore, you can use these slots for quick data exchange
between different server modules while the server processes a single request.
A slot of the PXDatabase class is stored in the server memory until you clear the slot.
Therefore, you can use such a slot to cache a data object for a long time—for example, to
read the cached data during a future HTTP request.
If a PXDatabase slot is used to cache the data that is obtained from the database tables, you
can use a special API to automatically update the data in the slot when any of these tables
has been changed.
For detailed information on using slots, see the sections of this topic.
If you need to keep a data object during a single HTTP request, we recommend that you
cache the object in a slot provided by the PXContext class.
You can use the following public static methods of the class to save a data object in a slot.
| Accessing Data | 224
Method Description
public static ObjectType Stores the specified data object under the key that is creat-
SetSlot<ObjectType> (Ob- ed on the base of the object type.
jectType value)
public static Object- Stores the specified data object under the key that is de-
Type SetSlot<ObjectType> fined by the first parameter.
(string key, ObjectType
value)
The following example shows how you can save the MyData object in the slot of the current
HTTP context under the key that is the same as the object type.
PXContext.SetSlot<MyDataType>(MyData);
To get a data object that is cached in the current HTTP context, you can use the following
methods of the PXContext class.
Method Description
public static ObjectType Returns the data object that is cached under the key that is
GetSlot<ObjectType>() created on the base of the object type.
public static Object- Returns the data object that is cached under the specified
Type GetSlot<ObjectType> key.
(string key)
The following example shows how you can get from the slot of the current HTTP context the
MyData object that is cached under the MyData22 key.
The following diagram illustrates how you can use a data object cached by using a slot
provided by the PXContext class.
| Accessing Data | 225
You do not need to delete the data saved in the PXContext class slots, because the system
deletes these slots from the server memory along with the data of the current HTTP context
created for the current request.
If you need to keep a data object in the server memory for a long time, we recommend that
you cache the object in a slot provided by the PXDatabase class.
You can use the following public static methods of the class to cache a data object in a slot
and to get the cached object from the slot.
Method Description
public static Object- If the PXDatabase slots contain a valid data object of the
Type GetSlot<ObjectType> specified type saved under the key defined by the first pa-
(string key, params rameter, returns this data object. Otherwise, the method
Type[] tables) creates a new object of the specified type, saves this emp-
ty object in the slot under the key defined by the first para-
meter, and returns the data object that is used by the call-
ing code to save the needed data. The list of the table types
specified in the params parameter is used to invalidate the
slot if any table of the list has been changed in the data-
base.
Method Description
able<> interface, the GetSlot<> method invokes the
Prefetch method of the object without a parameter.
public static ObjectType Is used for caching a data object of an ObjectType class in-
GetSlot<ObjectType, Pa- herited from the IPrefetchable<> interface to provide au-
rameter> (string key, tomatic update of the object in the slot. If the PXDatabase
Parameter parameter, slots contain a valid data object of the specified type saved
params Type[] tables) under the key defined by the first parameter, the method
returns this data object. Otherwise, the method does the
following:
3.Saves this object in the slot under the key defined by the
first parameter
The following example shows how you can use the GetSlot<ObjectType> (string key,
params Type[] tables) method to cache data under the MyData key in the slot of the
PXDatabase class.
...
Dictionary<string, string[]> dict =
PXDatabase.GetSlot<Dictionary<string, string[]>>(
"MyData", typeof(Table1), typeof(Table2), typeof(Table3));
lock (((System.Collections.ICollection)dict).SyncRoot)
{
...
List<string> myList = new List<string>();
...
string key = "myListKey";
dict[key] = myList.ToArray();
}
...
After the data object has been cached, you can access the object by using the following
instruction.
You can clear a slot provided by the PXDatabase class by means of the following public static
methods of the class.
Method Description
public static void Re- Sets to null the value of the slot that has the specified key.
setSlot<ObjectType>
(string key, params
Type[] tables)
public static void Re- Sets to null the value of each slot that is provided by the
setSlots() PXDatabase class.
The following example shows how you can clear the slot created in the example above.
PXDatabase.ResetSlot<MyDataType>(
"MyData", typeof(Table1), typeof(Table2), typeof(Table3));
For example, suppose that you need to develop a data provider that selects data from
multiple tables of the database and caches the data in PXDatabase slots. To do this, you can
develop the provider class based on the following code.
// Here you need to add the code for all the methods that are defined
// in the MyProvider abstract class.
// These methods can be used to manage the MyData object.
...
}
For the code above, the following diagram shows how the data object is cached and
automatically updated in the PXDatabase slot.
| Accessing Data | 229
If you have discovered that a PXDatabase slot returns legacy data, you can invoke the
SelectTimeStamp() public static method of the PXDatabase class to invalidate all the
PXDatabase slots that contain data obtained from the database tables that have been
changed. Then the GetSlot method invokes the Prefetch method and updates the
data in the slot.
| Implementing Business Logic | 230
In This Part
• Localizing Applications
By implementing event handlers, application developers can add business logic for the
manipulation of data within business logic controllers (BLCs). This business logic includes
the validation and calculation of field values, the management of data records (inserting,
updating, or deleting), the checks for duplicate records, and the implementation of the
presentation logic of the user interface.
In This Chapter
• Event Handlers
• List of Events
Event Handlers
The Acumatica Framework raises events in the context of a graph. An event handler can be
implemented in a graph, as well as in an attribute of a data field.
Graph event handlers are defined as methods in a business logic controller (BLC) class for
a particular data access class (DAC) or a particular DAC field. See the topics in the PX.Data
Delegates section in the API Reference for each event for an example of a graph event
handler declaration.
Attribute event handlers are defined as methods in attribute classes. The logic of the event
handlers is attached to all DAC objects or data fields annotated with these attributes.
The attribute in which an attribute event handler is implemented must be derived
from the PXEventSubscriberAttribute class and must implement the interface of the
IPX<EventName>Subscriber form (where <EventName> is replaced with the name of the
needed event), as shown in the following example.
PXFieldVerifyingEventArgs e)
{
...
}
Related Links
We recommend using generic event handlers because they are easier to declare, use, and
validate in Visual Studio. To refactor classic event handlers into generic event handlers, you
can use Acuminator.
For example, a classic handler for the RowSelected event of the CROpportunityProducts
DAC is defined as follows.
Specifying the DAC name as a parameter is optional because the system determines it
automatically based on the field name. You should specify the DAC name explicitly if the
field is inherited and you want to declare an event handler for the inherited field. For
example, the FieldDefaulting event handler for the CurrencyInfo.moduleCode field
looks as follows.
You can declare more than one handler for the same event by adding a custom name (for
example, a number) after the event name as follows.
The following example shows the second event handler for the FieldUpdated event of the
CROpportunityProducts.contactID field.
However, we do not recommend declaring more that one event handler because .NET
framework does not determine the call order.
All event handlers for a particular event share the same PXCache instance that has raised
this event. The system creates a PXCache instance to control the modified data records of
a particular data access class (DAC) type. The PXCache instance is always available as the
first argument in an event handler; the second argument provides the specific data that
corresponds to the event.
Once an event is raised, the order in which the associated event handlers are executed may
differ. For some events, the chain of graph event handlers is executed before the attribute
event handlers are; the attribute event handlers are executed only if the Cancel property of
the event arguments doesn't equal true after the execution of the graph event handlers.
For other events, the attribute event handlers are executed first, and the graph event
handlers are executed next. The topics in the PX.Data Delegates section in the API Reference
for each event shows the order in which the system invokes handlers for a particular event.
A business logic controller (BLC) includes the collections of graph event handlers for all
events except CacheAttached. Each of these collections holds event handlers for a particular
event and has the same name as the event. (You can find more information about how these
collections are initialized in the Initialization of an Event Handler Collection section in this topic.)
By using the methods of these collections, you can add and remove graph event handlers in
code at runtime.
A method added as an event handler must have the signature of a graph event handler, but
doesn't need to follow the naming convention for graph event handlers. If you want to add a
method as an event handler, you invoke the AddHandler<>() method on the corresponding
collection. For example, if the event is related to a row, it is invoked as follows.
RowEventName.AddHandler<DACName>(MethodName);
FieldEventName.AddHandler<DACName.fieldName>(MethodName);
When the AddHandler<>() method is invoked, event handlers are added to the collection as
follows:
• Event handlers are added to the beginning of the collection for any event whose name
ends with ing except the RowSelecting event.
• Event handlers are added to the end of the collection for any event whose name ends
with ed and for the RowSelecting event.
On each round trip, the PXGraph() constructor does the following while it initializes a graph
instance:
1. Creates the Cashes, Views, and Actions collections and other required collections. All of
these collections are initially empty.
2. If the graph instance is being created on the Acumatica ERP server for the first time:
a. Obtains the metadata of this graph from the appropriate assembly (which is
PX.Objects for most graphs in the application).
The ProcessMethods method processes the metadata of the methods that are
declared in the graph and all extensions of the graph. According to the naming
convention for event handlers, the _ symbol is a separator, so this method
tries to split the name of each processed method into segments. If the name
of the processed method has fewer than two segments or more than three
segments, the processed method is skipped.
c. Saves the graph metadata and the InitializeDelegate emitted method in the
Acumatica ERP server memory as the GraphStaticInfo static object shared for the
entire application instance.
event handler (described in the graph and graph extensions) to the FieldUpdatedEvents
collection of the PXCache<SOOrder> cache object.
…
}
To override an event handler with invocation of the base method, use code based on the
following template.
For a base method, you should provide two parameters because a base method is a
classic interceptor delegate, not a generic one. For details, see Types of Graph Event
Handlers.
For example, to override an event handler for the RowSelected event of the CurrencyInfo
class, use the following code.
The following diagram shows how different types of event handlers are invoked.
| Implementing Business Logic | 238
Figure: Use of event handlers while the basic data operations are processed
For details on how Acumatica Framework processes the basic data operations, see the
following topics:
...
ShipmentLines.Insert(line);
Keep in mind that calling the Insert() method of the data view is just a shortcut for the
Insert() method of the Cache property of the data view.
Also, in the example shown above, this method does not make the shipment line that you
insert related to the current shipment; it only inserts the shipment line into the PXCache
of shipment lines. The master-detail relationship between the new shipment line and the
current shipment is created because of the PXDBDefault attribute of the ShipmentNbr field
in the ShipmentLine DAC.
The FieldDefaulting event, which is raised for each field during the insertion of a new data
record, sets the default value for the data field.
The RowInserting and RowInserted events occur during the insertion of a data record in
the cache, after the field-related events have occurred. The RowInserting event happens
right before the new data record is actually inserted into the PXCache object, but after all
field events happen for this data record. The RowInserted event happens after the actual
insertion. If, in a RowInserting event handler, you change the data fields of the data record
that are inserted, no field events will be raised for these data fields.
You use the RowInserted event to do something after a data record is inserted into the
cache. For example, you can use a RowInserted event handler of the master data record to
add a default detail data record.
For details on the order in which events are executed during the insertion of a data record
into PXCache, see Sequence of Events: Insertion of a Data Record.
During the execution of the Update() method, the cache raises a number of events: first
the field-level events, and then the row-level events. During the update process, you have
access to the modified version of the data record through the NewRow property of event
arguments in RowUpdating event handlers or the Row property in other event handlers.
In a FieldUpdated event handler, you can modify a field of the data record passed as
e.Row. In this case, you don't have to call the Update() method; if you do, an infinite
loop will occur.
For details on the order in which events are executed during update of a data record, see
Sequence of Events: Update of a Data Record.
| Implementing Business Logic | 240
While a data record is being deleted, the cache raises a number of events. Before the
RowDeleting and RowDeleted events are triggered, the data record is placed in the cache
and assigned the Deleted status or the InsertedDeleted status (which means that the
data record has been inserted but has not yet been saved to the database). If the delete
operation is canceled, the data record will revert to the previous state and the RowDeleted
event won't be raised.
The sequence of events raised during the deletion of a data record is shown in Sequence of
Events: Deletion of a Data Record.
To save changes on the form to the database, you should use the
Actions.PressSave() method. To save changes without pressing the Save button,
you can use the Persist() method. You should not invoke the Persist() method
on the current graph instance. (You can invoke this method for only a graph instance
created in a background operation.)
The graph commits changes from all cache objects within a single database transaction. If
any row modification fails, the entire transaction is rolled back, and no changes from any
cache object are persisted.
In the Persist() method, the system starts the database transaction (see the diagram
below). Within the transaction, the graph iterates the collection of cache objects three times:
for inserted records, for updated records, and for deleted records. First, all new records from
all cache objects are inserted, and then all modified records are updated. Finally, within the
same transaction, all deleted records are removed from the database. The graph iterates
the collection of caches so that all master records are saved and then all detail records are
saved.
For new records and updated records, the graph iterates the collection of caches
in the order in which the data views are declared in the graph. That's why in the
graph, the master data view must be declared before the detail data view. For deleted
records, the graph iterates the collection of caches in the reverse order. The graph
| Implementing Business Logic | 241
access the caches through the Views.Caches collection, which is formed from the
main DACs of all data views declared in the graph.
In each iteration, the framework raises the RowPersisting event for a data record, executes
the appropriate SQL command for this data record within the transaction, and then raises
the RowPersisted event with the Open transaction status.
When the SQL commands have been executed successfully for all modified (inserted,
updated, or deleted) data records, the graph commits the transaction. Regardless of the
result of the transaction, the graph raises the RowPersisted event, in which you can check
the status of the transaction. A successful transaction has the Completed status. If an error
or exception has occurred during the transaction, the transaction returns the Aborted status.
If the transaction fails, the modified data remains in cache objects. On the RowPersisted
event for an aborted transaction, you can revert changed records to the default values.
| Implementing Business Logic | 242
| Implementing Business Logic | 243
When the RowPersisting event is raised, you can cancel the saving of a particular row to
the database by setting the e.Cancel property to true in the graph handler for the event.
For the sequence of events raised for each modified row within the Persist() method of the
graph, see Sequence of Events: Saving of Changes to the Database.
The system inserts a data record as an instance of a data access class (DAC) when a user
creates a new data record in the user interface, a request to insert a record is sent to the
web services API, or the Insert() method of a data view is called in code. The data record
is actually inserted into the PXCache object that corresponds to the DAC of the data record.
The inserted data record has the Inserted status and is available through the Inserted and
Dirty collections of the PXCache object.
When a data record is inserted, data field events are raised for each data field in the
following order:
1. FieldDefaulting
3. FieldVerifying
4. FieldUpdated
1. RowInserting (If the e.Cancel property is true, no further events are raised.)
2. RowSelected
3. RowInserted
The instance of the inserted data record is available in the e.Row property of event
arguments.
A data record is updated when a user modifies the data record on the user interface, the
request is sent through the Web Service API, or the Update() method is invoked on the data
view. Updated data records, which the system gives the Updated status, are later available
through the Updated and Dirty collections of the appropriate PXCache object.
The RowUpdating event is fired before the update happens, while the RowUpdated event is
fired after the update. The developer can handle these events and has access to the updated
data record and the previous version of the data record that is kept in the PXCache object.
The actual update happens between these two events when the data record is copied to the
PXCache object.
When a data record is updated, the following data field events are raised for each updated
data field:
1. FieldUpdating
2. FieldVerifying
3. FieldUpdated
Next, data record events are raised as follows:
1. RowUpdating is raised. At this moment, in the e variable, which represents event data,
e.Row holds the data record version from the cache, while e.NewRow holds the updated
data record. You can still stop the update by throwing a PXException instance.
a. RowSelected is raised. Only the updated data record can be accessed through e.Row.
b. RowUpdated is raised. e.Row now holds the updated instance, while e.OldRow holds a
copy of the old data record with the previous values.
A data record is deleted when a user deletes the record on the user interface, the deletion
request is sent through the Web Service API, or the Delete() method of a data view is
invoked in code. As a result of the deletion, the data record gets the Deleted status if it
already exists in the database, or the InsertedDeleted status if the record has just been
inserted into the PXCache object and deletion from the database is not required. The data
record is later available through the Deleted and Dirty collections of the PXCache object.
If the deletion has been initiated by a user on the UI or through the web services API, the
following field events are raised for each key data field before any other events are raised:
1. FieldUpdating
2. FieldUpdated
Next, regardless of how the deletion was initiated, data record events are raised as follows:
1. RowDeleting is raised. At this point, the developer can still stop the deletion by throwing
a PXException instance. In the e variable representing event data, e.Row holds the data
record being deleted.
While a user is inserting, updating, or deleting a data record, no changes are committed
to the database. The system stores the modified data records in the session, and you can
access them through the appropriate PXCache object. The system commits the changes
to the database when the user clicks Save in the user interface, the save request is sent
through the Web Service API, or Actions.PressSave() is invoked on the business logic
| Implementing Business Logic | 252
controller (BLC) instance. In both cases, the Persist() method of the graph is invoked. The
Actions.PressSave() method additionally checks that the Save action exists in the graph
and is enabled. The Save action then invokes the Persist() method.
When changes are saved to the database, events are raised as follows:
a. RowPersisted is raised. The commit operation for the current data record (available
through e.Row in the handler) is completed, but the transaction is still open:
e.TranStatus equals Open.
b. RowPersisted is raised one more time, either with e.TranStatus equal to Completed
(if all changes have been saved successfully) or with e.TranStatus equal to Aborted
if an error has occurred and all changes have been canceled.
Related Links
• Persist() Method
• PXRowPersisting Delegate
• PXRowPersisted Delegate
List of Events
In this topic, you can find the list of all events by category. You can get more information
about any of these events by navigating to the applicable topic in the API Reference.
• PXFieldDefaulting
• PXFieldVerifying
• PXFieldUpdating
• PXFieldUpdated
• PXFieldSelecting
• PXRowSelected
• PXRowInserting
| Implementing Business Logic | 253
• PXRowInserted
• PXRowUpdating
• PXRowUpdated
• PXRowDeleting
• PXRowDeleted
Database-Related Events
• PXCommandPreparing
• PXRowSelecting
• PXRowPersisting
• PXRowPersisted
Exception-Handling Event
• PXExceptionHandling
• CacheAttached
• For events whose names end with ing, graph event handlers are called first. You can
prevent the execution of attribute event handlers by setting e.Cancel to true.
• For events whose names end with ed, attribute event handlers are called first.
For example, if you need to change the default value set by the PXDefault attribute for a
field, you can define a FieldDefaulting event handler in the graph. In this graph event
handler, you can assign a different default value and set e.Cancel to true to prevent the
execution of the FieldDefaulting event handler defined in the PXDefault attribute (see the
following code).
{
e.NewValue = true;
e.Cancel = true;
}
}
Generally, if validation fails, you can cancel the update of the field and restore the
control to its value before the user changed it by throwing an exception of the
PXSetPropertyException type, as is shown in the following example.
if ((decimal)e.NewValue < 0)
{
throw new PXSetPropertyException("The quantity cannot be negative.");
}
You could instead correct the entered value to a valid value as shown in the following
example.
If you correct the entered value, you shouldn't throw an exception; instead, you should
let the method finish normally. However, we recommend that you display a warning
indicating that the value has been corrected automatically. To do this, you can invoke
the RaiseExceptionHandling<>() method of the PXCache<> type, as the following code
example shows. This method displays a warning for the validated data field but doesn't raise
an exception, so the method finishes normally and e.NewValue is set.
sender.RaiseExceptionHandling<ShipmentLine.lineQty>(
line, e.NewValue,
new PXSetPropertyException(
"The quantity has been corrected to the minimum possible value.",
PXErrorLevel.Warning));
You still have to initialize an instance of the PXSetPropertyException type. But this
time you do not throw an exception; you provide this instance as a parameter to the
RaiseExceptionHandling<>() method. You should specify the error level if you want the
message attached to the control to not be displayed with the default error sign. To attach
a warning to the control, specify PXErrorLevel.Warning in the PXSetPropertyException
constructor.
| Implementing Business Logic | 255
If validation depends on other fields of the same data record, you should implement the
validation in the RowUpdating event handler. The RowUpdating event happens during the
update of a data record, after all field-related events have occurred. At the moment when
the RowUpdating event is triggered, the modifications haven't been applied to the data
record stored in the cache yet; if needed, you can cancel the update process.
The event arguments give you access to the following data records:
• e.NewRow: The modified version of the data record, which contains all changes made by
field-related events
• e.Row: The copy of the original data record stored in the cache
You can use the ObjectsEqual<>() method of the cache to compare these two data records
to find out if any of the fields specified in the type parameters of the method has changed.
For example, the event handler in the following code uses the ObjectsEqual<>() method (in
bold type) compares the new and original data records (also in bold type) for the values of
three fields.
if (!sender.ObjectsEqual<ShipmentLine.shipmentTime,
ShipmentLine.shipmentMinTime,
ShipmentLine.shipmentMaxTime>(line, originalLine))
{
...
}
...
}
In this example, the ObjectsEqual<>() method returns false if any of the following values
has changed: ShipmentTime, ShipmentMinTime, and ShipmentMaxTime.
To cancel the update process, you set the Cancel property of the event arguments to true.
We recommend that you generate an error message for any field whose value doesn't pass
validation. You do this by calling the RaiseExceptionHandling<>() method of the cache, as
shown in the following code example.
line, line.ShipmentTime,
new PXSetPropertyException("The delivery time is too early."));
e.Cancel = true;
}
If the RowUpdating event handler finishes with the e.Cancel property equal to true, the
data record is not updated in the cache.
If the validation of a field depends on the field that is defined before the validated field in the
data access class (DAC), you can use the FieldVerifying event handler. The field-related
events are raised for fields in the order in which the fields are defined in the DAC. So in this
case, the FieldVerifying event handler is called for the validated field after all field-related
events have been raised for the field the validated field depends on.
In the example shown in the following code, the validation of the DeliveryDate
field depends on the ShipmentDate field. But because DeliveryDate is defined after
ShipmentDate in the Shipment DAC, it is correct to use the FieldVerifying event handler
to validate DeliveryDate.
You should primarily use the FieldUpdated event to modify only the data record itself,
because the update (or insertion) of the data record can still be canceled in row-level events
(RowUpdating or RowInserting). If you modify other data records in the FieldUpdated
event and the update is canceled, your changes to the other data records won't be reverted.
To modify field values in a FieldUpdated event handler, follow the rules below:
• To update a field that is defined after the current field in the data access class, use the
properties of the e.Row data record as shown in the following code example.
...
line.Description = product.ProductName;
Direct assignment of a value sets it to the given instance of the data record; no field-
level events are raised at this point.
• To update a field that is defined before the current field in the data access class, use one
of the following methods:
• SetValueExt<>(): You use this method of the cache to assign a specific value to a
field. The method raises the same field-level events for the data field as the events
raised when a data record is updated. For details about the update of a data record,
see Update of a Data Record.
• SetDefaultExt<>(): You use this method of the cache to assign the default value to
a field. The method raises the same field-level events for the data field as the events
raised when a data record is inserted. For details about the insertion of a data record,
see Insertion of a Data Record.
sender.SetValueExt<ShipmentLine.ProductID>(e.Row, GiftCardID);
The following parameters define the value presentation within the FieldSelecting event:
• e.ReturnValue defines the current field value, which is the internal value that is held in
the DAC field or the external value that is displayed in the control.
• The field state (a PXFieldState object) that provides the set of UI parameters for
the control. The UI control obtains all rendering parameters from the field state.
• The button state (a PXButtonState object) that provides UI parameters for the
action. The button state is constructed in the FieldSelecting event handlers of
the following attributes: the PXButton attribute, its successors, and the PXUIField
attribute defined for the action.
Every type attribute adds the event handlers for these two events, which by default
transform the field value between the internal presentation and the external one.
However, in the graph, you can define specific event handlers for the FieldSelecting and
FieldUpdating events and cancel the execution of handlers defined in attributes. Many
attributes—such as PXSelector, PXDefault, and PXUIField—handle events in which they
add specific information to the field state.
| Implementing Business Logic | 258
The FieldSelecting event is raised when the client requests the DAC field state to display
the field value in the UI.
The FieldUpdating event is raised every time the UI posts the updated external value to
the server.
Attributes implement business logic by subscribing to events. Each attribute class directly
or indirectly derives from the PXEventSubscriberAttribute class. In addition, an attribute
class derives from the interfaces that correspond to the event handlers it implements.
For example, the PXDefault attribute derives from the IPXFieldDefaultingSubscriber,
IPXRowPersistingSubscriber, and IPXFieldSelectingSubscriber interfaces,
which means that it implements its logic in the FieldDefaulting, RowSelecting, and
FieldSelecting event handler methods.
Attributes can be added to a data access class (DAC) definition, a data view declaration in a
business logic controller (BLC), and the BLC definition itself.
In This Chapter
• Mandatory Attributes
• Use of Attributes
• UI Field Configuration
• Default Values
• Referential Integrity
• Audit Fields
• Data Projection
• Access Control
• Notes
• Report Optimization
• Attributes on DACs
• Action Attributes
• Custom Attributes
This logic can be used in multiple forms of the application, and therefore can be moved into
an Attribute class. The attribute is used to annotate a data field in the data access class.
Then it can be reused anywhere in the code, as in the example below.
| Implementing Business Logic | 260
In this example, the logic of updating the receipt total on an update of the transaction is
implemented inside the DeltaMultiply attribute. This logic is triggered after each update,
delete, or insert operation on the DocTransaction data access class instance and updates
totals on the receipt level in the appropriate Document data access class instance.
Acumatica Framework provides a wide range of predefined attributes that can be used for
defining data types, database mapping, referential integrity, data format validation, and
default values for the field. The following code shows an example of how you can implement
the logic from the above example by using the predefined PXFormula attribute, which is
used for implementing calculations of data fields.
Because the data access classes are shared within an application, formatting, custom
logic, and any constraints implemented in attributes are reused in each business logic
controller that utilizes each data access class. By reusing code through attributes, you can
move shared application functionality into attributes and avoid code duplication, while still
enforcing application integrity.
| Implementing Business Logic | 261
Mandatory Attributes
In this topic, you can learn about the mandatory attributes of data access class (DAC) fields
and actions.
For each field defined in a DAC, you must specify the following attributes:
• A data type attribute, which is either a bound field data type attribute that binds the field
to a database column of a particular data type, or an unbound field data type attribute
that indicates that the field is unbound. For lists of these attributes, see Bound Field Data
Types and Unbound Field Data Types.
• The PXUIField attribute, which is mandatory for all fields that are displayed in the user
interface. For details on the PXUIField attribute, see UI Field Configuration.
The example below demonstrates a declaration of a DAC field bound to a database column
and displayed in the user interface.
[PXButton]
public virtual IEnumerable viewDocument(PXAdapter adapter)
{
...
}
Related Links
• UI Field Configuration
• PXUIFieldAttribute
• PXButtonAttribute
Use of Attributes
To apply the attribute business logic to an entity, you should place the attribute on the entity
declaration. At runtime, you can call the static methods of a particular attribute to adjust the
attribute's behavior.
Here the PXDefault attribute is created with the constructor that takes a Boolean-type
parameter (set to false). Additionally, the PersistingCheck property is specified.
You should call static methods defined in the attribute class to change the properties at
runtime. The static methods can affect a single attribute instance or multiple attribute
instances related to a specific data record or all data records in a particular cache object.
The following example shows an invocation of a static method.
When calling such a method, you typically specify the cache object, a data record related to
this cache object, and the data access class (DAC) field. The method affects the attribute
instance created for this field and the specified data record. If you pass null as the data
| Implementing Business Logic | 263
record, the method affects attribute instances related to all data records in the specified
cache object.
Acumatica Framework also includes other attributes that are used in special cases to bind a
DAC field to database columns.
UI Field Configuration
By using the PXUIField attribute, you can configure the layout of input controls and
buttons. The attribute is mandatory for all data access class (DAC) fields displayed in the
user interface.
• To a DAC field declaration to configure the field input control, as shown in the following
example
[PXDBDate()]
[PXUIField(DisplayName = "Pay Date")]
public virtual DateTime? PayDate { get; set; }
• To the declaration of a method that implements an action to configure the action button,
as shown in the following sample code
The attribute's properties determine the control layout in the user interface. You can specify
the display name, specify whether the control is visible and available, set the error marker,
and specify the access rights to view and use the control.
| Implementing Business Logic | 266
You can use the static methods (such as SetEnabled and SetRequired) of the
PXUIFieldAttribute class to set the properties of a control at runtime. The
PXUIFieldAttribute static methods can be called in the graph constructor or the
RowSelected event handlers.
The RowSelected event handler is raised when the user interface controls are
prepared to be displayed. This happens each time the form sends a request to the
server.
If you want to modify the Visible, Enabled, and Required properties for all detail rows in
a grid, you use the RowSelected event handler of the primary view DAC. If you want to set
the Enabled property of a field in particular row in a grid, you use the RowSelected event
handler of the DAC that includes this field.
If the grid column layout is configured at runtime, you set the data parameter of the
corresponding method to null. This indicates that the property should be set for all data
records shown in the grid. If a specific data record is passed to the method rather than null,
the method invocation has no effect.
Related Links
• PXUIFieldAttribute Class
Default Values
You can set the default values to data access class (DAC) fields by using the following
attributes:
• PXDefault: This attribute sets the default value and validates the field value when the
value is saved to the database. The following attributes are derived from the PXDefault
attribute:
• PXUnboundDefault behaves in the same way as PXDefault does, but the default
value is assigned to the field when a data record is retrieved from the database.
• PXDefaultValidate.
• PXDBDefault: This attribute sets the default value by using the value of some source
field, and updates the value if the source field value changes in the database before the
data record is saved.
PXDefault Attribute
The PXDefault attribute provides the default value for a DAC field. The default value is
assigned to the field when the cache raises the FieldDefaulting event. This happens when
a new row is inserted in code or through the user interface.
| Implementing Business Logic | 267
A value specified as a default can be a constant or the result of a BQL query. If you provide
a BQL query, the attribute executes it on the FieldDefaulting event. You can specify both
a constant and a BQL query; the attribute first executes the BQL query and then uses the
constant if the BQL query returns an empty set. If you provide a DAC field as the BQL query,
the attribute retrieves the value of this field from the Current property of the cache object.
The attribute uses the cache object of the DAC type in which the field is defined.
The PXDefault attribute also checks that the field value is not null before saving a record
to the database. You can adjust this behavior by using the PersistingCheck property. Its
value indicates whether the attribute should verify that the value is not null, verify that the
value is not null or a blank string, or not perform any verification.
The attribute can redirect the error that happened on the field to another field if you set the
MapErrorTo property.
You can use the static methods of the attribute to change the attribute properties for a
particular data record in the cache or for all data records in the cache.
Differences
You usually set the default value to a DAC field by using the PXDefault attribute. You
can set a constant as the default value or provide a BQL query to obtain a value from the
database or data records from the cache. The default value is assigned to the field when a
data record that includes this field is inserted into the cache.
You can use the PXDefault attribute just to make the field mandatory for input by using the
attribute without parameters.
The PXDefault attribute is not suitable when the default value is retrieved from a field that
can be auto-generated by the database (such as the identity field). In this case, you should
use the PXDBDefault attribute. It updates the value assigned to the field as the default with
the value generated by the database.
For example, if you implement a master-detail relationship, you should use the PXDBDefault
attribute to bind the detail data record fields to the master data record key fields. If the
master data record is new, its identity field is set to a real value by the database when the
master record is saved. So if a detail data record is created before the master data record is
saved, the detail data record field is set to the temporary value of the master identity field.
However, the PXDBDefault attribute replaces the temporary value with the real value when
the detail data record is saved to the database.
You can use the PXUnboundDefault attribute to set the default value to an unbound
field. The value is assigned when a data record is retrieved from the database (on the
RowSelecting event).
Related Links
• PXDBDefaultAttribute Class
• PXDefaultAttribute Class
• PXUnboundDefaultAttribute Class
| Implementing Business Logic | 268
Drop-Down Lists
You can use the following attributes to configure a drop-down list that represents a data
access class (DAC) field in the user interface:
• PXStringList: Configures a drop-down list from which a user can select from a fixed set
of strings.
• PXIntList: Configures a drop-down list where a user can select from a fixed set
of values. The control displays strings, while the field is assigned the integer value
corresponding to the selected string.
• PXDecimalList: Configures a drop-down list where a user can select from a fixed set of
strings converted to decimal values.
• PXImagesList: Configures a drop-down list where a user can select from a fixed set of
images.
• PXDBIntList: Configures a drop-down control for an integer field. The values and labels
for the drop-down control are retrieved from the specified database table.
• PXDBStringList: Configures a drop-down control for a string field. The values and labels
for the drop-down control are retrieved from the specified database table.
Lookup Controls
You can use the following attributes to configure a lookup control that represents a field in
the user interface:
• PXSelector: Defines a lookup control for a DAC field that references a data record from
a particular table by holding its key.
• PXRestrictor: Adds a restriction to a BQL command that selects data for a lookup
control, and displays an error message when the value entered does not fit the
restriction. The attribute works only with PXSelector and cannot be used with
PXCustomSelector.
A segmented key value is a string value that identifies a data record in the system and
consists of one segment or multiple segments. A segmented key is an entity that is identified
by a string (referred to as a dimension) and associated with segments. For each segment,
you can define the list of possible values. You can create a new segment when the data
records identified by the segmented key already exist in the database.
| Implementing Business Logic | 269
You can use the following attributes to configure a control to input a segmented key value in
the user interface:
• PXDimension: Defines an input control that formats the input as a segmented key value
and displays the list of allowed values for each key segment.
Referential Integrity
You can use the following attributes to implement referential integrity at runtime:
• PXParent: Creates a reference to a parent data record. By default, when the parent
data record is deleted, all child data records that reference it are also deleted. (You can
change this behavior by using the LeaveChildren property.)
• PXLineNbr: Generates unique line numbers that identify child data records in the parent-
child relationship.
Note that all the attributes in the list above add server-side logic used at runtime. The
referential integrity is implemented on the server side.
The attributes implement the RowInserted, RowUpdated, and RowDeleted event handlers to
calculate aggregates. Also, the attributes use the FieldUpdated event handler for dependent
fields. The PXFormula attribute defines the RowSelecting event handler to calculate values
for unbound DAC fields. You only need to mark a field in the DAC with an attribute; you
don't have to define any event handlers.
• To calculate the value of a field from other fields of the same data record, you add the
PXFormula attribute with one parameter to this field, as shown below.
[PXFormula(
| Implementing Business Logic | 270
typeof(Mult<DocTransaction.tranQty,DocTransaction.unitPrice>)]
public virtual decimal? ExtPrice
...
• To calculate the value of a field from other fields of the same data record and calculate
the aggregated value from these values, you add the PXFormula attribute with two
parameters to this field. You should also add the PXParent attribute to identify the
master data record. (See the following code.)
[PXFormula(
typeof(Mult<DocTransaction.tranQty, DocTransaction.unitPrice>),
typeof(SumCalc<Document.totalCost>))]
public virtual decimal? ExtPrice
...
[PXParent(typeof(Select<Document,
Where<Document.docType, Equal<Current<DocTransaction.docType>>,
And<Document.docNbr, Equal<Current<DocTransaction.docNbr>>>>>))]
public virtual string DocType
...
This code sets ExtPrice to the product of TranQty by UnitPrice, summarizes ExtPrice
over all detail data records, and assigns the result to the TotalCost field of the parent
document.
• To calculate an aggregated value by using the value of the field, you add the PXFormula
attribute to this field with the first parameter set to null, as shown below. The PXParent
attribute is required.
[PXFormula(
null,
typeof(SumCalc<Document.totalCost>))]
public virtual decimal? ExtPrice
...
This code sets the TotalCost field of the parent document to the sum of the ExtPrice
values.
• To calculate an expression for each detail data record and aggregate the resulting values
in a field of the master data record, you add the PXUnboundFormula attribute to any
field of the detail data record, as shown in the following code. The field marked with the
attribute is not set by the attribute. The PXParent attribute is required.
[PXUnboundFormula(
typeof(Mult<DocTransaction.tranQty, DocTransaction.unitPrice>),
typeof(SumCalc<Document.totalCost>))]
public virtual decimal? ExtPrice
...
| Implementing Business Logic | 271
This code sets the TotalCost field of the parent document to the sum of the products of
the TranQty and UnitPrice values. The attribute doesn't set the ExtPrice field.
In the first parameter of the PXFormula and PXUnboundFormula attributes, you can specify
an expression built of data fields, BQL constants, and the following BQL functions:
In the second parameter of the PXFormula and attribute, you can specify one of the
following aggregation functions with the parent's field to hold the result as the type
parameter:
• PXDBCalced: Defines an SQL expression that calculates an unbound field from the fields
of the same DAC, whose values are taken from the database.
• PXDBScalar: Defines an SQL subrequest that retrieves a value for an unbound DAC field.
The subrequest can retrieve data from any bound field in any DAC.
The attributes add the provided expression and the subrequest into the SQL query that
selects data records of the given DAC.
| Implementing Business Logic | 272
PXDBScalar
The PXDBScalar attribute defines a subquery that selects the value assigned to the field on
which the attribute is specified. In the code example below, PXDBScalar selects the value
from the ProductQty.AvailQty data field and inserts it into the ProductReorder.AvailQty
field.
The BQL expressions specified in the PXDBScalar attribute above adds the following
subqueries (shown in bold type) to the SQL query that selects ProductReorder
records.
SELECT ...,
(SELECT TOP (1) ( productqty.availqty )
FROM productqty ProductQty
WHERE ( productqty.productid = ProductReorder.productid )
ORDER BY productqty.availqty),
...
FROM ...
PXDBCalced
The PXDBCalced attribute defines an expression that is translated into SQL. This expression
calculates the field value from other fields of the same data record. An example of the
PXDBCalced attribute is shown in the following code.
[PXDecimal(2)]
[PXUIField(DisplayName = "Discrepancy")]
[PXDBCalced(
typeof(Minus<
Sub<IsNull<ProductReorder.availQty, decimal_0>,
ProductReorder.minAvailQty>>)
typeof(Decimal))]
public virtual decimal? Discrepancy { get; set; }
The BQL expression specified in PXDBCalced in the previous code adds the following
calculation expression to the SQL query that selects ProductReorder records.
SELECT ...,
(( -( Isnull((SELECT TOP (1) ( productqty.availqty )
FROM productqty ProductQty
WHERE ( productqty.productid = ProductReorder.productid )
ORDER BY productqty.availqty), .0)
- ProductReorder.minavailqty ) )),
| Implementing Business Logic | 273
...
FROM ...
Unlike PXDBCalced, the PXFormula attribute can be added to either an unbound or bound
data field. Also, the PXFormula attribute provides calculation of master DAC fields from child
DAC fields. For more information on PXFormula, see Calculation of Field Values.
Audit Fields
The following attributes are placed on data access class (DAC) fields used for data audit:
• PXDBCreatedByID: Maps a DAC field to a database column and automatically sets the
field value to the ID of the user who created the data record.
The framework binds the DAC fields to database columns and automatically assigns field
values.
Data Projection
The following attributes implement the projection of data from one table or multiple tables
into a single data access class (DAC):
• PXProjection: Binds the DAC to a random data set. The attribute thus defines a named
view, but is implemented by the server side rather than by the database.
| Implementing Business Logic | 274
•
PXExtraKey: Indicates that the field implements a relationship between two tables. The
use of this attribute enables the update of the referenced table when the projection is
updated.
Access Control
The group mask value indicates the access rights a user should have in order to use a
data record. To be able to set access rights for particular data records, you should use the
PXDBGroupMask attribute to mark the data access class (DAC) field that holds the group
mask value.
On a substitute form, to define the inheritance of access rights for an action that is
implemented in the corresponding entry form, you can use the PXEntryScreenRights
attribute.
Related Links
• PXDBGroupMaskAttribute Class
• PXEntryScreenRightsAttribute Class
Notes
By using the PXNote attribute, you can give users the ability to attach text notes, files, and
activity items to data records, and to search for an entity by using the full-text search index.
You should use the PXNote attribute in the data access class of these data records to mark
the field that stores the identifier of a note in the Note table. Notes are used to attach text
to a data record. This text is stored in the note data record in the Note table. Additionally,
you can attach files or other entities to a data record through a note. This feature is
implemented through additional tables that store the identifiers of a note and the attached
entity.
The PXNote attribute can also be configured to save the specified table fields in a note. In
this case, by using the Acumatica Framework application website search, the user will be
able to search the data records by the values saved in the note.
Related Links
• PXNoteAttribute Class
Report Optimization
The value of an unbound data access class (DAC) field can be calculated in the property
getter. The calculation can involve other fields of the same DAC. However, when the value
| Implementing Business Logic | 275
of the DAC field is requested, other fields are not guaranteed to be calculated or assigned
their values. These situations are normal when the integration services are used, copy-paste
functionality is used, or the field is used in reports.
To ensure that the fields referenced in the property getter have values when it is executed,
you should use the PXDependsOnFields attribute.
Related Links
• PXDependsOnFieldsAttribute Class
• Display of Reports
Attributes on DACs
You can place the following attributes on the data access class (DAC) declaration:
• PXPrimaryGraph: Specifies the graph that is used by default to edit a data record.
• PXCacheName: Specifies the user-friendly name of the DAC. The name is displayed in the
user interface.
• PXTable: Binds a DAC that derives from another DAC to the table having the name of
the derived table. Without the attribute, the derived DAC will be bound to the same table
as the DAC that starts the inheritance hierarchy.
• PXAccumulator: Updates the values of a data record in the database according to the
policies specified in the attribute parameters.
• PXHidden: Gives the developer the ability to hide a DAC, graph, or view from the
selectors of DACs and graphs, and from generic inquiries, reports, and the web services
API.
The PXProjection and PXTable attributes can also mark a DAC. See Data Projection for more
details.
Action Attributes
You use the following attributes to configure a button that will represent an action in the
user interface:
• PXButton: Serves as the base attribute for all other attributes, which give you the ability
to configure buttons. The successor attributes only set the base class properties to
specific values.
• PXSaveButton
• PXSaveCloseButton
• PXCancelButton
• PXCancelCloseButton
| Implementing Business Logic | 276
• PXInsertButton
• PXDeleteButton
• PXFirstButton
• PXPreviousButton
• PXNextButton
• PXLastButton
• PXSendMailButton
• PXReplyMailButton
• PXForwardMailButton
• PXTemplateMailButton
• PXLookupButton
• PXProcessButton
• PXFilterable: Adds the control that makes it possible for a user to create filters and
save them in the database. The control is added to the grid that uses the data view to
retrieve data.
• PXImport: Adds the grid toobar button that a user clicks to load data from the file to the
grid. The attribute is placed on the data view the grid uses to retrieve the data.
• PXHidden: Hides the data view from the selectors of data access classes (DACs) and
graphs, and from the web service API clients.
• PXCopyPasteHiddenFields: Indicates that the specific fields of the primary DAC of the
data view are not copied when the copy-paste feature is used on the form.
| Implementing Business Logic | 277
The system stores the attributes from the DAC and graphs (domain-level attributes). On
each round trip, the system copies the appropriate domain-level attributes to the cache
object, creating cache-level attributes. Attribute constructors are not invoked; instead, the
system invokes the CacheAttached(PXCache) method for each copy of an attribute. If you
modify an attribute's properties in code for a particular data record, the cache object creates
a data-record copy of the attribute.
Attributes specified in a data access class apply to this class in every graph of the application
unless a graph replaces them with other attributes. In some graphs, you may need
attributes that differ from what is declared in the DAC. To replace attributes within a
particular graph, add new attributes to the CacheAttached event handler for the particular
field defined in the graph, as the following code shows. When you replace attributes, you
have to redefine all attributes, including the type attribute.
In a graph, each PXCache object stores attributes for the corresponding DAC (see the
diagram below). When a PXCache object is created, the framework searches for attributes of
CacheAttached event handlers in the graph using the DAC field name. Attributes specified
for these event handlers are copied to PXCache objects. If there is no CacheAttached
event handler defined for a DAC field, the attributes are copied from the data access class.
Attributes of CacheAttached event handlers are instantiated once, on the first initialization
of the graph in which you define this event handler. In the data access class, the attributes
are instantiated when the application domain starts.
When you add an attribute to a DAC field, the attribute subscribes to certain events of
Acumatica Framework. In event handlers, the attribute processes the field value based
on the purpose of the attribute. For instance, the PXDefault attribute subscribes to the
FieldDefaulting event and sets the default value every time the event is raised for the
field. Multiple attributes can subscribe to the same event.
| Implementing Business Logic | 278
Custom Attributes
Typically, you use custom attributes for DAC fields to reuse the same behavior or business
logic in multiple places of the application.
If you want to add new functionality that can be used in multiple places of the application
(for instance, automatic numbering of documents), you can create a custom attribute from
scratch. To implement the new logic, you need to handle the events on which the logic
should be performed. Acumatica Framework automatically subscribes attribute methods that
implement interfaces to the corresponding events; you don't have to manually subscribe
these methods to events. In many cases, you can also add static methods to the attribute
class to provide the dynamic API of the attribute. By using these methods, you can change
the attribute parameters of DAC fields in a graph at runtime.
To create a custom attribute from scratch, you do the following (see the sample code
below):
3. Implement the CacheAttached method (if you are reading parameters or data from the
database).
IPXFieldVerifyingSubscriber
{
// Create internal objects here.
// Do not read dynamic parameters or data from the database.
public MyFieldAttribute()
{
// Code
}
// Process input parameters here; read dynamic parameters and data from
// the database so that the data is current every time the cache is
// initialized.
public override void CacheAttached(PXCache sender)
{
// Code
}
In the constructor, you usually validate input parameters and save them to internal objects
of the attribute. However, to read data from the database or conditionally configure the
attribute, you typically use the CacheAttached() method of the attribute. When you create
a custom attribute, you should take into account that the attribute constructor is invoked
only during domain startup and on the initialization of a graph with the CacheAttached()
event handler (see Replacement of Attributes for DAC Fields in CacheAttached).
We recommend that you read the data from the database in the graph execution
context. To do this, read the data and set the actual parameters for the attribute in the
CacheAttached() method that you override in your custom attribute. From the sender
input parameter of the CacheAttached() method, you can get the reference to the graph
in whose context the attribute is used. Every time the framework initializes a cache within
a graph, the CacheAttached() method implemented in the attribute is invoked for each
attribute on a DAC field for that particular graph. The graph is created on every round trip,
so if you read data in CacheAttached(), the attribute retrieves the actual parameters every
time. Also, you might need to read data in event handlers just before you use the data in
the attribute. For instance, in Example in this topic, you retrieve the last assigned number in
the RowPersisting() method just before you calculate the new number and insert it into a
document.
Do not confuse the CacheAttached() method that you implement in attributes with
the CacheAttached() event handler that you define in a graph. The CacheAttached()
event handler of the graph has no implementation and is used as a marker to
substitute attributes on the specified DAC field in that particular graph. The
CacheAttached() method implemented in attributes is invoked on attributes that are
current for the graph when the appropriate cache is initialized.
To implement a static method that provides dynamic configuration of the attribute, you can
define a static method, taking into account the following considerations:
1. You should pass a PXCache object and a data record as input parameters of the method.
2. To be able to get the attribute object on a particular field of a record, you should define
the generic method with the <Field> type parameter. To get the collection of attributes
on the field, invoke the GetAttributes<Field>(record) method of the PXCache object
that stores the record.
3. To find the needed attribute, you should iterate the collection of attributes to find the
object of the needed attribute type.
// The static method of the AutoNumberAttribute attribute dynamically sets the number
prefix
public static void SetPrefix<Field>(
PXCache sender, object row, string prefix) where Field : IBqlField
| Implementing Business Logic | 281
{
foreach (PXEventSubscriberAttribute attribute in sender.GetAttributes<Field>(row))
{
if (attribute is AutoNumberAttribute)
((AutoNumberAttribute)attribute).Prefix = prefix;
}
}
// Assigning the number prefix depending on the document type
public virtual void Document_RowPersisting(PXCache cache, PXRowPersistingEventArgs e)
{
Document doc = (Document)e.Row;
if (doc != null)
{
switch (doc.DocType)
{
case DocType.R: // no prefix
AutoNumberAttribute.SetPrefix<Document.docNbr>(cache, doc, null);
break;
case DocType.N:
AutoNumberAttribute.SetPrefix<Document.docNbr>(cache, doc, "RET");
break;
}
}
}
Event handlers implemented in graphs are stored in separate collections in the graph for
each event. The framework determines which attribute handlers an attribute defines based
on the interfaces the attribute implements.
The attribute and graph handlers that handle the same event are executed in the defined
order. In -ing events, such as FieldDefaulting, the attribute handlers are invoked after the
handlers of the graph. So if you generate the default value in a graph handler and don't want
attributes to change it, you can cancel the execution of attribute handlers for this field by
setting the e.Cancel property to true in the graph handler (see the diagram in Sequence of
Events: Insertion of a Data Record).
In the -ed events, such as RowUpdated, attribute handlers are executed before handlers
implemented in the graph. In -ed events, you typically implement some additional logic, and
you cannot cancel attribute handlers.
Example
In this section, you can find an example of the creation of a custom attribute that provides
automatic numbering of documents.
Instead of creating a custom attribute, you can use the predefined auto-numbering
attributes of Acumatica ERP, such as PX.Objects.CS.AutoNumberAttribute.
In this example, the following event handlers are implemented for the attribute:
| Implementing Business Logic | 282
• RowPersisting(), which assigns the new document number before the record is saved
to the database and updates the last assigned number stored in the configuration.
• RowPersisted(), which checks the database transaction status and resets the default
number if the transaction fails.
As the runtime API of the attribute, the following static methods, which enable dynamic
setup of the last number field and add a prefix to the number, are implemented:
The declared interfaces correspond to events that you handle in the attribute.
2. In the AutoNumber attribute, define the listed fields for the internal objects (the flag
that enables or disables automatic numbering, and the command that retrieves the last
number) and two properties (the last number field and the number prefix). See the
following code.
3. Define two attribute constructors, one with one Type parameter and another with
two Type parameters, and define the CreateLastNumberCommand() method. (See the
following code.)
if (LastNumberField != null)
{
if (typeof(IBqlSearch).IsAssignableFrom(LastNumberField))
_LastNumberCommand = BqlCommand.CreateInstance(LastNumberField);
else if (typeof(IBqlField).IsAssignableFrom(LastNumberField) &&
LastNumberField.IsNested)
_LastNumberCommand = BqlCommand.CreateInstance(
typeof(Search<>), LastNumberField);
}
if (_LastNumberCommand == null)
throw new PXArgumentException("lastNumberField");
}
In the constructor, you validate input parameters and ensure that each of them is a
valid DAC field type or a BQL Search<> type. The first parameter specifies the auto-
numbering flag field that enables or disables automatic numbering of documents. The
second parameter specifies the field from which the attribute will take the last assigned
number to calculate the next number for a new document. In the _LastNumberCommand
field, you construct the BQL Search<> type that selects the last number field from the
database. You will use this command to retrieve the last assigned number from the setup
parameters in the RowPersisting event handler.
{
BqlCommand command = null;
Type autoNumberingField = null;
// Create the BqlCommand from Search<>
if (typeof(IBqlSearch).IsAssignableFrom(_AutoNumberingField))
{
command = BqlCommand.CreateInstance(_AutoNumberingField);
autoNumberingField = ((IBqlSearch)command).GetField();
}
// Otherwise, create the Bql command from the field.
else
{
command = BqlCommand.CreateInstance(
typeof(Search<>), _AutoNumberingField);
autoNumberingField = _AutoNumberingField;
}
// In CacheAttached, get the reference to the graph.
PXView view = new PXView(sender.Graph, true, command);
object row = view.SelectSingle();
if (row != null)
{
_AutoNumbering = (bool)view.Cache.GetValue(
row, autoNumberingField.Name);
}
}
In the CacheAttached() method, you retrieve the auto-numbering flag from the
database. To retrieve the flag, you create a BqlCommand object that is passed to a PXView
object. The BqlCommand object represents a BQL statement that is translated into an SQL
SELECT query and executed in the database by the PXView object.
To select the data record, you invoke the SelectSingle() method of the PXView object,
which retrieves the data from the database by using the specified command. You use
the SelectSingle() method, which generates the SQL statement with TOP 1 records to
return, which executes faster. You can select a single row because only one record in the
setup table has the configuration of the current company. If the retrieved flag is enabled,
you set the _AutoNumbering field of the attribute to true.
For details about PXView, see PXView Type and Views Collection.
On the FieldDefaulting event, you insert the <NEW> placeholder into the number
field for a new document. On the FieldVerifying event, you revert the number to
the placeholder if a user has entered a nonexistent document number. If the user has
entered the number of a document that exists, the document opens on the form.
6. Add the RowPersisting() method and the auxiliary GetNewNumber() method, which is
called from RowPersisting(), as shown in the following code.
setupCache.Update(row);
setupCache.PersistUpdated(row);
On the RowPersisting event occurs, you retrieve the last number from the setup record,
increment the number, insert the new value into the document, and update the last
assigned number in the setup record. The RowPersisting event is the last event raised
before Acumatica Framework commits changed data from cache objects to the database.
To avoid duplicate numbers, you calculate the new value just before the data record is
saved to the database.
}
}
The RowPersisted event, in which e.TranStatus may have the Completed or Aborted
value, is raised after all changes from cache objects of the graph are committed to the
database. If any error occurs during the transaction, you remove the new document
number, which hasn't been saved for a document, from the PXCache object for the
setupType DAC. For details on the process of saving changes to the database, see Saving
of Changes to the Database.
9. Define the static SetPrefix<Field>() method of the attribute, as the following code
shows.
The SetPrefix<Field>() method sets the prefix that is added to the number generated
by the AutoNumber attribute.
Now the AutoNumber attribute is ready, and you can use it for the numbering of documents
and sales orders. To use the attribute, add it to the document number field in the DAC, as
shown in the code below.
• To update a field or multiple fields of a data record without checking for the data record
version in the database. (In an ordinary update, the framework generates the SQL
statement that checks the timestamp column, if this column exists in the table.)
• To define a specific update policy for a column—for instance, to calculate the sum of
values in a column on every update. You can also specify restrictions for a column that
will be checked by the database during update.
The base PXAccumulator attribute has two parameters in the constructor. This attribute
applies the Summarize policy to the specified decimal or double fields (on each update, the
new value is added to the database value) and the Initialize policy to other fields. To
override this policy or to set restrictions, you derive a custom attribute from PXAccumulator,
in which you override the PrepareInsert() method.
In the overridden PrepareInsert() method, you first have to invoke the base
PrepareInsert() method to initialize the collection of columns. If the base
PrepareInsert() method returns true, the collection of columns is initialized. Then in the
overridden method, you can set restrictions and update policies for specific columns.
| Implementing Business Logic | 289
If you set any restrictions, you have to override the PersistInserted() method. For details,
see Implementing an Update With Restrictions below.
2. columns.Restrict(): Sets the value restriction for the column. The restriction triggers
the PXLockViolationException exception, which you should handle in the overridden
PersistInserted() method of the attribute.
The methods listed above and all the methods listed below can be used in default mode
(when SingleRecord = false):
1. columns.RestrictPast()
2. columns.RestrictFuture()
3. columns.InitializeFrom()
4. columns.UpdateFuture()
For reference information on methods and properties that you can use in the
PXAccumulator attribute, see PXAccumulatorAttribute Class in the API Reference.
If no new values that you assign with the attribute depend on the existing ones or add
restrictions to future values, you can enable single-record mode for the attribute. In this
case, you can use only the Update() and Restrict() methods of the columns collection. In
single-record mode, the framework generates the specific SQL statement that updates an
independent record in the database. To enable single-record mode, set the _SingleRecord
field of the attribute (or the SingleRecord property in the attribute constructor) to true. By
default, single-record mode is disabled.
You can use one of the following update policies for columns (values defined in the
PXDataFieldAssign.AssignBehavior enumeration):
1. Replace: Assigns the specified value to the data field on every update. For this policy,
set the new value to the field in your code.
2. Summarize: Adds the specified delta (with the appropriate sign) to the data field on every
update. For this policy, set the delta to the field in your code.
3. Maximize: Assigns the maximum value in the column to the data field on every update.
For this policy, it isn't necessary to set the new value to the field in your code.
| Implementing Business Logic | 290
4. Minimize: Assigns the minimum value in the column to the data field on every update.
For this policy, it isn't necessary to set the new value to the field in your code.
5. Initialize: Assigns the specified value to the data field if the old value is null. For this
policy, set the new value to the field in your code.
When you use an accumulator attribute, you might need to check some restrictions when
the data is being updated in the database. You can specify the restriction conditions in
the accumulator attribute. The attribute adds the conditions to the WHERE clause of the
SQL query. When the framework executes the query, the record is not updated in the
database if the condition is false. In this case, the database returns no affected rows, and
the framework throws the PXLockViolationException exception.
If you update a column with a restriction, you have to override the PersistInserted()
method in the attribute and handle PXLockViolationException in it. In the overridden
| Implementing Business Logic | 291
PersistInserted() method, you first have to invoke the base method and then have
to catch the PXLockViolationException exception that can be thrown in the base
method. If the exception is thrown, you have to check the new values for restriction
conditions to compose the appropriate error message for the UI. The framework raises
the PXLockViolationException exception in a general case if the database returns no
rows affected by the UPDATE command. When you catch the PXLockViolationException
exception, you have to check whether the restriction conditions take place. If you
have found that a restriction condition is false, throw the PXRowPersistingException
exception to return the appropriate error message to the UI. Otherwise, rethrow the
PXLockViolationException exception, because it was caused by another reason. On the
PXRowPersistingException exception, the transaction is rolled back and no record is
updated in the database. If no exceptions occur in PersistInserted(), the new value is
saved to the database.
For an ordinary data update without accumulators, you can check value restrictions
at different times before the data is updated in the database. In particular, on the
FieldVerifying and RowUpdating events, you can check the values for restrictions
at the model level, before the changes are saved to the PXCache object. On these
events, you typically implement validation of the values entered by the user. If you
need to check a value immediately before it is saved to the database, you can do this
on the RowPersisting event.
An accumulator attribute triggers only for data records that have the Inserted status in
the cache object. Thus, you have to insert a data record into the cache to trigger an update
via the accumulator attribute. Once inserted, the data record gets the Inserted status
and maintains this status until it is saved to the database, even if you update the record in
the cache. When a user saves changes to the database, despite of that the record has the
Inserted status in the cache object, the framework generates the correct SQL statement
that updates the record or inserts it into the database. By default, if there is no record with
such a key in the database, a new one is inserted. If a record with these key values already
exists, it is correctly updated in the database because of the accumulator attribute.
When you invoke Update() on an Inserted record (inserted into the cache), the
record remains Inserted. When you invoke Update() on an unchanged record that
is retrieved from the database, the record becomes Updated, and the accumulator
won't trigger on this record. For more information on the statuses of data records, see
Modification of Data in a PXCache Object.
You can insert a data record with unique key values into the cache only once. The
framework won't insert the duplicate record. After you have inserted the record, use
Update() to modify its values in the cache within the current unsaved session.
In an accumulator attribute, you can specify which database operations are allowed. By
default, the accumulator attribute inserts a new record if it doesn't exist in the database
and updates an existing record, if any. To allow only insert or update operations, set the
InsertOnly or UpdateOnly property of the columns collection in the PrepareInsert()
method before invocation of columns.Update().
| Implementing Business Logic | 292
During the database transaction that applies changes from cache objects to the database,
the graph invokes the PersistInserted() method of each PXCache object. The
PersistInserted() method that is executed for the Inserted records of the cache object
checks whether an accumulator attribute is specified on the DAC. If the accumulator exists,
the PersistInserted() method of the accumulator is invoked. If there is no accumulator on
the DAC, the method executes an ordinary INSERT (see the diagram below).
The following code example adds a restriction to the ProductQty.AvailQty field by using
the Restrict() method.
The restriction set with the Restrict() method doesn't work on insertion of the ProductQty
data record. In the code above, you compare the existing value with -newQty.AvailQty.
The following code example adds the same restriction to the ProductQty.AvailQty field by
using the AppendException() method.
The restriction set with the AppendException() method works on both insertion and update
of the ProductQty data record. In the code above, you compare the resulting value, -
newQty.AvailQty plus the existing value, with 0m.
In This Chapter
For more information about how a user can attach files and notes to forms, see To Attach
a File to a Record and To Attach a Note to a Record. For information about activities, see Tasks,
Events, and Activities.
The extensions of the files that can be uploaded to a form must be registered on the
File Upload Preferences (SM202550) form. If the required file types are not registered
on this form, you have to add them and save your changes. On this form, you can
also define the maximum size of an uploaded file (in kilobytes).
1. In the data access class (DAC) that provides data for the form, add the NoteID field, as
the following code shows.
#region NoteID
public abstract class noteID : PX.Data.IBqlField { }
[PXNote]
public virtual Guid? NoteID { get; set; }
#endregion
The database table that corresponds to the DAC must contain the NoteID column
with the uniqueidentifier data type.
Once you have added the NoteID field to the DAC and rebuilt the project, the following
buttons appear on the title bar of the form:
3. Optional: To change whether each of these elements is displayed on the title bar, in the
ASPX code of the form, specify the values of the following properties of the PXFormView
control:
| Implementing Business Logic | 295
• NoteIndicator: Indicates (if set to True) that the Notes button is displayed on the
title bar.
• FilesIndicator: Indicates (if set to True) that the Files button is displayed on the
title bar.
• ActivityIndicator: Indicates (if set to True) that the Activities button is displayed
on the title bar. This button, which users click to attach activities to the form, is not
displayed by default.
The LinkIndicator property, which controlled whether the Help > Get Link
element was displayed on the title bar, is now obsolete. Help > Get Link is
always displayed on the title bar.
1. In the data access class (DAC) that provides data for the table rows, add the NoteID
field, as the following code shows.
#region NoteID
public abstract class noteID : PX.Data.IBqlField { }
[PXNote]
public virtual Guid? NoteID { get; set; }
#endregion
The database table that corresponds to the DAC must contain the NoteID column
with the uniqueidentifier data type.
Once you have added the NoteID field to the DAC and rebuilt the project, the following
columns appear in the table:
• : The Notes column, which users click to attach notes to the form
• : The Files column, which users click to attach files to the form
3. Optional: To change whether each of these columns is displayed in the table, in the ASPX
code of the form, specify the values of the following properties of the PXGrid control that
corresponds to the table:
• NoteIndicator: Indicates (if set to True) that the Notes column is displayed in the
table
• FilesIndicator: Indicates (if set to True) that the Files column is displayed in the
table
| Implementing Business Logic | 296
1. Add the NoteID field and the field that stores the path to the image to the data access
class (DAC) that provides data for the form on which you want to display the image, as
shown in the following code.
#region NoteID
public abstract class noteID : PX.Data.IBqlField { }
[PXNote]
public virtual Guid? NoteID { get; set; }
#endregion
#region ImageUrl
public abstract class imageUrl : PX.Data.IBqlField { }
[PXDBString(255)]
[PXUIField(DisplayName = "Image")]
public virtual string ImageUrl { get; set; }
#endregion
The database table that provides data for the form on which you want to display
the image must contain the following columns:
• The field (in this example, ImageUrl) with the varchar(255) data type, to
store the internal path to the attached image
2. In the ASPX code of the form that works with this DAC, add the PXImageUploader
control linked to the ImageUrl data field, as shown in the following code.
In This Chapter
• Configuration of Actions
• Redirection to Webpages
The two data views defined in the following code example have the same main DAC and
work with the same PXCache object, which stores the Product data records.
The two data views in this example can be used as data members for only UI containers
that display the same data record at the same time. In this definition of data views, the
| Implementing Business Logic | 298
first one is used to display brief information of a product on a form, and the second one is
used to display the detail information of the same product on a tab. For both data views, the
Current property of the PXCache cache object returns the same Product data record. If a
user selects a data record in one UI container, the same data record appears in the second
container.
To bind a container control with a data view, you specify the data view in the DataMember
property of the container control. Each container control should be bound to a data view. You
can bind any number of container controls to the same data view, unless the data view is
specified in the PrimaryView property of the datasource control.
To disable, hide, or change a caption of a data field on the form, you use the static methods
of the PXUIFieldAttribute type, as shown in the following example.
PXUIFieldAttribute.SetEnabled<Shipment.deliveryDate>(
sender, row, row.ShipmentType == ShipmentTypes.Single);
You specify the field as the type parameter. It is mandatory to specify the PXCache object
(sender in the example above). You can provide a specific data record (row) or specify null
in the second parameter to apply the method to all data records in the cache. If you omit the
type parameter, the method will be applied to all fields that are displayed in the UI, as shown
in the following example.
You can specify default UI options for a field in the PXUIField attribute in the data access
class (DAC) and change them if necessary in a RowSelected event handler through the
static methods of the attribute.
You shouldn't read anything from the database or write to the database in a
RowSelected event handler.
Always implement two states of a UI control (such as enabled and disabled) when
you set up the UI. Thus, when you make a data field disabled by some condition, you
have to enable the data field in the corresponding opposite condition.
For related data records (in a one-to-many relationship), you place the entire configuration
of the UI presentation in the RowSelected event handler of the master data record. You can
hide columns of the details grid only in the RowSelected handler for the master data record.
In the RowSelected event handler of the detail record displayed in a grid, you can enable or
disable data fields, but you cannot modify their visibility.
| Implementing Business Logic | 299
The common UI presentation logic, which doesn't depend on particular values of the data
record, can be implemented in the constructor of the graph. In the constructor, you cannot
check the values of the currently selected data record, because no data records have been
loaded yet. To implement the UI presentation specific to a data record, you should use the
RowSelected event handler of the data record.
Acumatica Framework initializes attributes during the startup of the domain or the
first initialization of a graph that defines a CacheAttached event handler. During cache
initialization, the appropriate attributes are copied to the cache level. The system can also
create attributes of a data record level by copying cache-level attributes.
To specify the properties of a cache-level attribute, you pass null as the second argument
to the static methods of the attribute, as the following code example shows. The change to a
property will affect all data records of the corresponding cache.
PXUIFieldAttribute.SetVisible<ShipmentLine.shipmentDate>(
ShipmentLines.Cache, null, false);
To specify the properties of a record-level attribute, you pass a data record as the second
argument to the static methods of the attribute (see the following code example). The
change will affect only the data record that you pass to the method.
PXUIFieldAttribute.SetEnabled<Shipment.deliveryDate>(
sender, row, row.ShipmentType == ShipmentTypes.Single);
When you use record-level attributes for the first time during a round trip, the system
creates the record-level attributes by copying cache-level attributes. If some record-level
attributes already exist and you pass null to an attribute's method, the system applies the
change to both cache-level attributes and record-level attributes.
Every toolbar button corresponds to an action declared in the graph. To add standard
buttons to the toolbar, you can use explicit or implicit declaration of these actions.
In any declaration, the DAC that you specify for actions must be the same as the main DAC
of the primary view of the form. The form toolbar manipulates data records of the main
DAC of the primary view. If the DAC specified for an action differs from the main DAC of the
primary view, the button will not appear on the toolbar.
| Implementing Business Logic | 300
Suppose that you are developing the Countries form, where users work with a table of
records and need only the Cancel and Save toolbar buttons. (The users work with a table of
records on the form and don't need the other standard buttons, such as Previous and Next
for navigation.) In the example of the CountryMaint graph (shown in the following code),
we explicitly declare two actions that correspond to the Cancel and Save standard toolbar
buttons.
To implicitly declare standard buttons, you need to specify the second type parameter in the
base PXGraph class, as the following code shows in bold type.
The following code shows two equivalent declarations of actions for standard buttons that
work with data records of the Customer DAC. Based on these declarations, the system
automatically adds to the form toolbar the standard buttons for manipulating the Customer
records if the main DAC of the primary view specified in the datasource control on the ASPX
page is also Customer.
Configuration of Actions
Actions are methods that can be invoked from outside of the graph, from the UI, or
through the web service API. In the user interface, actions correspond to buttons or menu
commands.
Action Declaration
In the PXAction<> type parameter, you should specify the main DAC of the primary data
view. Otherwise, the action cannot be displayed on the form toolbar.
• A method that implements the action; this method has the PXButton and PXUIField
attributes. This method has the following forms of declaration:
• Without parameters and returning void: This is the usual form of declaration (shown
in the following code example).
[PXButton(CommitChanges = true)]
[PXUIField(DisplayName = "Cancel Shipment")]
protected virtual void cancelShipment()
{
...
}
This type of declaration is used for actions that are executed synchronously and are
not called from processing forms.
[PXButton]
[PXUIField(DisplayName = "Release")]
protected virtual IEnumerable release(PXAdapter adapter)
{
...
return adapter.Get();
}
This type of declaration should be used when the action initiates a background
operation or is called from processing forms.
The field and the method should have the same name, differing only in the capitalization of
the first letter.
| Implementing Business Logic | 302
In the same way as a graph includes methods that invoke actions, the webpage's
datasource includes callback commands for all actions defined in the graph. For
details, see Configuration of Callback Commands.
A graph includes the Actions collection of all PXAction<> objects defined in the
graph.
When a user invokes an action through the UI, the page sends a request to the server side
of the application (that is, it executes the callback). If you need the form to send the recent
changes made on the form, set the CommitChanges property of the PXButton attribute to
true as follows.
[PXButton(CommitChanges = true)]
You should always set the CommitChanges property to true for the actions that cause
changes to be saved to the database.
Types of Actions
• To modify or validate data records and save changes to the database. Set the
CommitChanges property of the PXButton attribute to true for these actions.
You can adjust the appearance of an action in the UI at runtime (for example, to disable or
hide the corresponding button on the form). To do this, you should use the methods of the
PXAction<> class, as the following code example shows.
You don't use the static methods of the PXUIField attribute, because these methods work
only with the attribute copies stored in cache objects.
For example, if you need the user to confirm that deletion should occur, you should invoke
the Ask() method in the RowDeleting event handler for the data record that the user is
trying to delete.
| Implementing Business Logic | 303
if (ShipmentLines.Ask("Confirm Delete",
"Are you sure?",
MessageButtons.YesNo) != WebDialogResult.Yes)
{
e.Cancel = true;
}
For details on the deletion process, see Sequence of Events: Deletion of a Data Record.
You can use the Ask() method of a data view everywhere (that is, not only in RowDeleting)
to display the dialog box requesting the user's confirmation to continue. If the Ask() method
is used in an event handler, the event handler will be called twice. The first time, the event
handler is interrupted on Ask() invocation, and the dialog box is displayed. The second time,
after the dialog box is closed, the Ask() method indicates which button has been clicked,
and execution continues.
The ExternalCall property returns true if the deletion has been initialized in the UI
or through the web services APIs. If you don't need to invoke particular logic for data
modifications made in code (such as the removal of a record), you can exit the method if the
ExternalCall property is false as follows.
if (!e.ExternalCall) return;
Redirection to Webpages
You may need to redirect a user to an another webpage. Acumatica Framework supports the
following types of redirection:
You can add redirection from a selector control declaratively, by adding the attributes to the
data access class and to the selector control. Such redirection is often used for opening the
data entry form from which the user can edit the record selected in the selector control. In
other cases, the code invokes the redirection by throwing one of the exceptions provided by
Acumatica Framework.
Once an exception is thrown, it interrupts the current context and propagates up the call
stack until it is handled by Acumatica Framework, which performs the redirection. You don't
| Implementing Business Logic | 304
need to implement the handling of the exceptions that are used for redirection. Also, this
mechanism doesn't affect the performance of the application.
To configure a drop-down list, you use the PXStringList or PXIntList attribute in the
definition of the data field in the data access class (DAC), as shown in bold type in the
following example.
[PXDBString(1)]
[PXDefault(ShipmentStatus.OnHold)]
[PXUIField(DisplayName = "Status")]
[PXStringList(
new string[]
{
ShipmentStatus.OnHold, ShipmentStatus.Shipping,
ShipmentStatus.Cancelled, ShipmentStatus.Delivered
},
new string[]
{
"On Hold", "Shipping", "Cancelled", "Delivered"
})]
public virtual string Status
{
get;
set;
}
| Implementing Business Logic | 305
You use PXStringList when the values that are assigned to the field are strings, and
you use PXIntList when the values are integers.
• The array of values assigned to the field and saved to the database with the data record
You can modify a drop-down list at runtime by using the SetList<>() static method of
the PXStringList attribute. You can do this in the RowSelected event handler or graph
constructor.
The following code example shows the use of the SetList<>() method.
PXStringListAttribute.SetList<Shipment.status>(
sender, row,
new string[]
{
ShipmentStatus.OnHold,
ShipmentStatus.Shipping,
},
new string[]
{
"On Hold",
"Shipping",
});
This code sets a new list of values and labels for the Status field.
In the type parameter, you specify the data field associated with the control. You also
provide the cache object, the data record that will be affected by the method, the list of
values, and the list of labels.
If the list of possible values of a drop-down control is changed dynamically at runtime, you
should use the RowSelected event handler to manage the list. Otherwise, we recommend
that you create the list in the graph constructor.
| Implementing Business Logic | 306
If a drop-down list is configured with the PXStringList attribute, you can allow a user to
enter values that are not options in the list. You do this by setting the AllowEdit property
of the PXDropDown control to True on the ASPX page (see the setting in bold type in the
following code).
By default, a user can select one value from a drop-down list. The user will be able to select
multiple values if you do all of the following:
•
Set the AllowMultiSelect property of the PXDropDown control to True on the ASPX page
(see the setting in bold type in the following code).
• Set the MultiSelect property of the PXStringList attribute to true, as shown in the
following code.
[PXString(20)]
[PXUIField(DisplayName = "Priority")]
[PXStringList(
new string[]
{
WorkOrderPriorityConstants.High,
WorkOrderPriorityConstants.Medium,
WorkOrderPriorityConstants.Low
},
new string[]
{
Messages.High,
Messages.Medium,
Messages.Low
},
MultiSelect = true)]
public virtual string Priority { get; set; }
To configure a selector control, you use the PXSelector attribute in the definition of the data
field in the data access class (DAC), as shown in bold type in the following example.
[PXDBInt(IsKey = true)]
[PXDefault]
[PXUIField(DisplayName = "Product ID")]
[PXSelector(typeof(Search<Product.productID>),
typeof(Product.productCD),
typeof(Product.productName),
typeof(Product.unitPrice),
SubstituteKey = typeof(Product.productCD))]
public virtual int? ProductID
...
In the first parameter, you specify a Search<> BQL query to select data records for the
control. The Search<> command has the same syntax as the Select<> command, except
that you specify the data field of the main DAC. In the Search<> command, you can specify
conditions and join data from other DACs. When a user selects a data record in the control,
the control assigns the value of the specified field to the data field.
You can omit Search<> in the first parameter of PXSelector, if you specify
only a DAC field without a complex expression that may contain WHERE, JOIN,
ORDER BY, or GROUP BY conditions. Thus, in the example above, you can specify
typeof(Product.productID) instead of typeof(Search<Product.productID>) in
the first parameter.
You configure the columns that should be shown in the control by providing the types of the
fields after the Search<> command; see the code in bold type in the following example.
[PXSelector(typeof(Search<Product.productID>),
typeof(Product.productCD),
typeof(Product.productName),
typeof(Product.unitPrice),
SubstituteKey = typeof(Product.productCD))]
public virtual int? ProductID
...
The code above defines three columns (see the following screenshot).
| Implementing Business Logic | 308
You can join multiple DACs in the Search<> command and specify fields from the joined
DACs as columns. The following code example shows in bold type the LeftJoin clause and
the fields from the joined DAC added to the selector control as columns.
If you don't specify any columns, the control will display all columns
that have the Visibility property of the PXUIField attribute set to
PXUIVisibility.SelectorVisible.
The SubstituteKey property specifies the field whose value should be shown in the control
in the UI instead of the field that is specified in the Search<> command.
The SubstituteKey property is shown in the bold type in the following code.
| Implementing Business Logic | 309
[PXSelector(typeof(Search<Product.productID>),
typeof(Product.productCD),
typeof(Product.productName),
typeof(Product.minAvailQty),
SubstituteKey = typeof(Product.productCD))]
public virtual int? ProductID
...
In the example above, the ProductID field of a shipment line stores the ProductID
value of the selected product, while in the UI. the control shows the ProductCD value.
Conversion between the ProductID and ProductCD values happens in the FieldUpdating
and FieldSelecting event handlers, which are implemented within the PXSelector
attribute.
• The PXBranchSelector control on the ASPX page, which renders the tree structure of
the companies and branches
• The OrganizationTree attribute in the data access class, which retrieves the data for
the control
In this topic, you can find information about how to work with the Company/Branch
selection menu in the code of an inquiry or processing form.
To add the Company/Branch selection menu to an inquiry or processing form, you do the
following:
1. Optional: In the filter DAC, add separate fields to hold the company and branch, which
the system can fill with the company and branch identifiers when a value is selected in
the Company/Branch selection menu. The following code shows an example of the field
definitions.
If the filter DAC does not contain these fields, you can omit the corresponding
parameters in the constructor of the OrganizationTree attribute, which you will
add in the next step.
using PX.Objects.GL.Attributes;
#region OrganizationID
| Implementing Business Logic | 310
[BranchOfOrganization(typeof(ARHistoryFilter.organizationID), false)]
public int? BranchID { get; set; }
2. In the filter DAC, create an integer field, and add the OrganizationTree attribute to this
field, as shown in the following code example. In the constructor of the attribute, specify
the following values:
• sourceOrganizationID: The type of the field in this DAC that stores the identifier of
the company. The type can be null.
• sourceBranchID: The type of the field in this DAC that stores the identifier of the
branch. The type can be null. The value of sourceBranchID of the attribute can be
null if a company is selected in the Company/Branch selection menu.
• onlyActive: The Boolean value that (if set to true, which is the default value)
displays only the company and branch records that have the Active field set to true.
using PX.Objects.GL.Attributes;
#region OrgBAccountID
public abstract class orgBAccountID : PX.Data.BQL.BqlInt.Field<orgBAccountID> { }
[OrganizationTree(
sourceOrganizationID: typeof(organizationID),
sourceBranchID: typeof(branchID),
onlyActive: false)]
public int? OrgBAccountID { get; set; }
#endregion
Because the objects of the company and branch have different types and their
identifiers can overlap, the system identifies the entity that is selected in the
Company/Branch selection menu by the business account identifier.
3. In the ASPX code of the form, add the PXBranchSelector control, as the following code
example shows.
In a selector control on a form, to display only the records that are available for the branch
or company selected in the Company/Branch selection menu, you can use the following
business query language (BQL) functions in the selector condition:
| Implementing Business Logic | 311
• Inside<>: To make sure that the branch specified by BranchID matches the entity
specified in the Company/Branch selection menu—that is, to ensure that the branch is
the same as the one selected in the selection menu or is a part of the company selected
in the selection menu. The following code shows the use of this function.
using PX.Data.BQL.Fluent;
using using PX.Objects.GL;
[PXSelector(
typeof(SearchFor<Ledger.ledgerID>
.In<SelectFrom<Ledger>
.LeftJoin<Branch>
.On<Ledger.ledgerID.IsEqual<Branch.ledgerID>>
.Where<Ledger.balanceType.IsEqual<LedgerBalanceType.budget>
.And<Branch.branchID.IsNull
.Or<Where<Branch.branchID, Inside<Optional<orgBAccountID>>>>>>
.AggregateTo<GroupBy<Ledger.ledgerID>>>),
SubstituteKey = typeof(Ledger.ledgerCD))]
public virtual int? BudgetLedgerIDByBAccount { get; set; }
• Suit<>: To check that the company specified by OrganizationID matches the entity
specified in the Company/Branch selection menu—that is, the company is the same as
selected in Company/Branch or the branch selected in Company/Branch is a part of
this company. The use of this function is shown in the following code example.
using PX.Data.BQL.Fluent;
using PX.Objects.GL;
using PX.Objects.GL.FinPeriods;
using PX.Objects.GL.FinPeriods.TableDefinition;
[FinPeriodSelector(
typeof(Search<OrganizationFinPeriod.finPeriodID,
Where<FinPeriod.organizationID, Suit<Optional<orgBAccountID>>>>),
null)]
public string FinPeriodIDByBAccount { get; set; }
If you need to select the company or branch on an inquiry or processing form when this form
is opened during redirection from another form, you specify the value of the organization's
business account ID in the filter DAC of the target form. You do not need to specify the
values of the BranchID or OrganizationID fields, which are specified automatically by
OrganizationTreeAttribute. The following code shows an example of the code that
performs redirection to an inquiry form with the company or branch specified.
using PX.Objects.GL;
AccountHistoryBySubEnq graph =
PXGraph.CreateInstance<AccountHistoryBySubEnq>();
GLHistoryEnqFilter filter = PXCache<GLHistoryEnqFilter>.CreateCopy(
| Implementing Business Logic | 312
graph.Filter.Current);
filter.OrgBAccountID = Filter.Current.OrgBAccountID;
graph.Filter.Update(filter);
throw new PXRedirectRequiredException(graph, "Account by Subaccount");
1. In the data access class (DAC), add a new field or modify an existing string field as
follows:
a. Add one of the string attributes (PXDBString or PXString) to the property field.
b. Specify the value of the InputMask property of the attribute. Use the following
conventions to define the mask:
• >: All of the characters that follow this symbol should be in uppercase.
• <: All of the characters that follow this symbol should be in lowercase.
in the database for the corresponding record will contain the same value
(1234567890).
2. In the ASPX code of the form, add a new PXMaskEdit control or modify the control that
corresponds to the field so that it has the PXMaskEdit type, as shown in the following
code example.
To specify the input mask for a string field at runtime, use a SetInputMask method of the
PXDBString or PXString attribute. You use the same conventions to define the mask as
those described for the InputMask property in To Specify an Input Mask and a Display Mask for a
String Field.
In the following example, the input mask of the AccountMask field is changed at run time.
1. In the data access class (DAC), add a new field or modify an existing data and time field
as follows:
a. Add one of the date and time attributes (PXDate, PXDateAndTime, PXDBDate,
PXDBTime, or PXDBDateAndTime) to the property field.
b. Specify the value of the InputMask or DisplayMask property of the attribute. Use the
standard and custom date and time format strings.
The following example shows the use of the InputMask and DisplayMask properties.
2. In the ASPX code of the form, add a new PXDateTimeEdit control or modify the control
that corresponds to the field so that it has the PXDateTimeEdit type, as shown in the
following code example.
You can change how the PXDateTimeEdit control is displayed (whether the control
shows a calendar selector or a drop-down list with time values) by specifying
the value of the TimeMode property. The following example causes the system to
display the list of time values.
1. In the graph that corresponds to the form, add the action and the delegate for the
button that opens the dialog box, as shown in the following example.
In this example, the MainDAC DAC is the main DAC of the primary view of the graph;
therefore, the action is added to the toolbar of the form by default.
2. If you want to specify additional properties of the action (such as CommitChanges), in the
ASPX code of the form, add the PXDSCallbackCommand tag for the action and specify the
necessary properties of the tag, as shown in the following example.
3. In the graph that corresponds to the form, add the data view for the dialog box, as
shown in the following code.
| Implementing Business Logic | 315
[Serializable]
public class DialogBoxParameters : IBqlTable
{
public abstract class parameter1 : IBqlField { }
[PXString(10, IsUnicode = true)]
public virtual string Parameter1 { get; set; }
PXFilter<DialogBoxParameters> OpenDialogBoxView;
In this code, we add a simple DAC with two unbound fields and use a data view based on
the PXFilter class.
4. In the ASPX code of the form, add the PXSmartPanel container with the Key property
equal to the name of the data view you created for the dialog box, as shown in the
following code. In the PXPanel container inside PXSmartPanel, add the commit buttons
of the dialog box (such as OK, Cancel).
5. In the button delegate, perform a call to an AskExt method of the view specified in the
Key property of the PXSmartPanel container.
| Implementing Business Logic | 316
When the user clicks the button on the form, the execution interrupts on the AskExt
call and the dialog box is displayed. After the user clicks a button in the dialog box, the
AskExt method returns the dialog box result.
In This Chapter
A setup data access class (DAC) never has key fields. When a user navigates to a setup
form, the system displays the first retrieved data record. If no record is retrieved, the
framework creates a new record that is saved to the database when the user clicks Save
| Implementing Business Logic | 317
on the form. The next time the form is opened, the previously inserted record is retrieved.
Save and Cancel are the only buttons that are necessary on a setup form.
1. Create a Setup table in the database. (You add a prefix to the Setup name according
to Table and Column Naming Conventions.) Add to this table columns that represent
configuration parameters.
2. Define a Setup data access class with the same name as the name of the Setup
database table. In the data access class, add the PXDefault attribute to the required
configuration parameters.
You should configure the UI of the setup form so that an administrative user can insert
and edit only one record, which represents the configuration of this part or module of the
application. Define only two actions, Save and Cancel, for this page.
4. Add the PXPrimaryGraph attribute to the Setup DAC. In the attribute, specify the graph
used by the setup form.
To display the error page for missing configuration data in the UI when the user tries to
navigate to a form that depends on configuration parameters, you do the following:
| Implementing Business Logic | 318
1. In the graph where you want to use configuration parameters, define the PXSetup data
view as follows.
2. In the graph constructor, obtain the current record from the PXSetup data view, as the
following code shows.
public ReceiptEntry()
{
Setup setup = AutoNumSetup.Current;
}
Each tenant may have its own configuration parameters that are identified by the company
ID in the setup table. To support multi-tenant configuration, the setup table should contain
the CompanyID column as the primary key in the database. The CompanyID field is not
declared in DACs. This column is transparently handled by the framework, which retrieves
records by the needed CompanyID only.
The application template provides two tenants by default: the system one and the active
one. The system tenant is read-only and has a CompanyID of 1. The second tenant, which is
active, is used for all new and updated objects. The active tenant has a CompanyID of 2. The
configuration parameters are saved to the setup table with the CompanyID of 2. Each new
tenant that you add to the application gets the next sequential ID: 3, 4, and so on.
The PXFilter data view always returns one record with the current values of the filtering
parameters. The PXFilter data view works only with the UI and doesn't invoke any requests
to the database. The data view object for the filtering parameters is defined in a graph, as
the following code shows.
With the PXFilter type, you can use any data access class (DAC) that has the needed data
fields. You usually define a specific DAC for the filtering parameters because the filtering
parameters are taken from multiple DACs. Since the filter DAC isn't bound to any database
table, you define only unbound data fields in the filter DAC. Because PXFilter data views
are supposed to contain only one data record, you should not specify any key fields within a
DAC that is used to store filtering parameters.
Avoid using the PXFilter data view type with DACs that have at least one key field
defined—that is, DACs that contain fields having the IsKey=true parameter in the
type attribute.
Filter data fields are usually displayed on a form. To immediately refresh data records as
soon a user updates a filtering parameter, enable the callback for the input control that
displays the filtering parameter on the form (see To Enable Callback for a Control).
The PXFilter data view is specified in the PrimaryView property of the datasource control
of the ASPX page where the filtering parameters are used, as shown in the following code
example.
To clear the filtering parameters on the form, you define the Cancel action (which actually
clears all data on the form) for the filter DAC; see the following code example.
To select data, you should specify filtering conditions in the Where clause of the data view
type (see the following code). To pass current filter values to the query, you specify the filter
DAC fields within the Current parameter. You have to define the data view that retrieves
filtered records for the UI after the definition of PXFilter.
Equal<Current<SupplierFilter.countryCD>>>>,
And<Where<Current<SupplierFilter.minOrderQty>, IsNull,
Or<SupplierProduct.minOrderQty,
GreaterEqual<Current<SupplierFilter.minOrderQty>>>>>>,
OrderBy<Asc<SupplierProduct.productID,
Asc<SupplierProduct.supplierPrice,
Desc<SupplierProduct.lastPurchaseDate>>>>> SupplierProducts;
}
You can use a read-only type of the data view that retrieves filtered data records. For
a read-only data view, the framework automatically disables the editing of rows in the
grid.
• The grid that displays the list of records retrieved by the specific processing data view. In
the grid, you also provide the following components:
• A column with a check box, which gives users the ability to select one record or
multiple records in the grid for processing. To add the check box, define the unbound
Selected field in the main data access class (DAC) of the processing data view and
add the column for this field to the grid.
Selected is the default name for this specific check box; you can define the
data field with any name and override the default Selected name in the graph
constructor with the SetSelected() method of the PXProcessing class.
• Optionally, a redirection button or link that can be clicked to open the data entry form
for the selected record.
• A toolbar that includes the Process, Process All, and Cancel buttons.
• The form that provides selection criteria (for narrowing the records that are listed and
may be processed) or configuration parameters (or both) for the processing method.
To provide data for the grid, you have to define a processing data view of one of the
following types:
• PXProcessing: Provides data records for processing without filtering. You can use the
Where<> and OrderBy<> clauses in this data view type.
• PXProcessingJoin: Provides data records for processing without filtering. You can also
use Join<> clauses in this data view type.
• PXFilteredProcessing: Provides data records for processing with filtering. This data
type uses two DACs as type parameters, where the second DAC specifies the filter DAC.
You can use the Where<> and OrderBy<> clauses in this data view type.
| Implementing Business Logic | 321
Below is an example of the SalesOrders data view defined for the grid on the a processing
form.
You can define only one processing data view in a graph. In the graph constructor, you
have to define the processing method as the processing delegate for the data view, as the
following code shows.
public SalesOrderProcess()
{
SalesOrders.SetProcessDelegate<SalesOrderEntry>(
delegate(SalesOrderEntry graph, SalesOrder order)
{
graph.Clear();
graph.ApproveOrder(order, true);
});
}
Based on the definition of the processing data view, Acumatica Framework automatically
adds the Process, and Process All buttons to the form toolbar. These buttons invoke the
processing method specified as the processing delegate for the data view. To add the Cancel
button to the toolbar, you have to define the PXCancel action with the needed DAC (see the
following code). The Cancel button completely refreshes the data on the form.
To add the filter to the form, you have to define a PXFilter data view and set this data view
in the PrimaryView property of the datasource control on the ASPX page. In the processing
data view that provides records for the grid, you have to use the PXFilteredProcessing or
PXFilteredProcessingJoin type and specify the filter DAC in the second type parameter.
In the following code example, the Filter data view provides the Discrepancy and
ActiveProducts filtering parameters, which are used in the Records data view. The Records
data view retrieves the list of deficient products that match the filter.
For a filtered processing form, you can specify a processing method as the processing
delegate for the data view in the RowSelected event handler for the DAC used in the
PXFilter data view. This approach can be used to pass the selected filter parameters to the
processing method.
If a processing form contains a form area with a control to select a method to process the
details, the usage of the RowSelected event handler for the main DAC of the primary view is
the only way to specify the selected method as the processing delegate in the code.
• Define a non-static method that uses a single record as the input parameter. This way
can be used to process a single record independently from other records of the same
class.
| Implementing Business Logic | 323
• Define a static method that uses a list of records as the input parameter. This way can be
used to process a list of records. In this method, you can reorder the records in the list
before processing, as well as check dependencies between records during processing.
These ways are described in more detail in the sections that follow.
You don't need to create a dedicated processing form for each processing operation;
whether to create a processing form depends on the requirements to the application.
In a simple case, to process a single record, you can define a non-static processing method
in the data entry graph, as the following code shows.
To make the system invoke the method on a separate thread, you can use the
PXLongOperation.StartOperation() method. Within the method that you pass to
StartOperation(), you have to create a new instance of the graph and invoke the
processing method on that instance, as the following code shows.
The anonymous method definition (delegate()) is used to shorten the code in the
example.
According to the ProcessItemDelegate type, you can use the specified graph and item
objects within the processing method. When the processing of records is initiated, Acumatica
Framework creates a graph instance of the specified type and invokes the delegate for each
data record that should be processed. A single graph instance is used for the processing
of all records. Therefore, you have to clear the graph state by calling the Clear() method
before the invocation of the processing method within the delegate, as shown in the code
example earlier in this section.
In a general case, to process a list of records that may depend on one another, you have to
define the static processing method, as the following code shows.
{
// Process the records here
}
}
You can invoke the static processing method in the data entry graph, within the method
passed in the PXLongOperation.StartOperation() method, as shown in the following code
example.
To invoke the static method from a processing form, in the graph constructor, set the
method as the processing delegate for the data view by using the SetProcessDelegate()
method, as shown in the code that follows.
public ReorderProcess()
{
Records.SetProcessDelegate(Process);
| Implementing Business Logic | 326
}
}
Based on the delegate type, you can operate with the list of processed records of the
specified List<Table> type.
When you use a static processing method, you have to manually create a new graph object
once and reuse it throughout the whole static method (see the code below).
To display a message in the UI from a processing method, use the following static methods
of the PXProcessing class:
• SetInfo(): Displays a green check mark for the processed row in the grid, which
denotes the successful processing of the record.
• SetWarning(): Displays an exclamation mark for the processed row in the grid.
• SetError(): Displays a red X for the processed row in the grid, which denotes an error
that has occurred during processing.
| Implementing Business Logic | 327
In each of these methods, you have to specify the initial object index in the list passed to
the processing method. You can also specify the message text that appears for the row.
The same rules are applicable to the processing of errors within the delegate you pass
to PXLongOperation.StartOperation(...).
In the following example, you show the green check mark or the X icon that represents an
error in the UI for each processed data record and return the error to the UI if processing of
at least one record fails. (The code below attempts to process all records from the list.)
To add a button to the processing dialog box, you can use one of the following approaches:
• For the action that corresponds to the button, in the graph, set the value of the
VisibleOnProcessingResults property of PXButtonAttribute or its descendant to
true, as shown in the following code example.
[PXUIField(DisplayName = Messages.ShowDocuments)]
[PXButton(VisibleOnProcessingResults = true)]
public virtual IEnumerable showDocuments(PXAdapter adapter)
{
ShowOpenDocuments(SelectedItems);
return adapter.Get();
}
• In the ASPX file that corresponds to the form, set the value of the
VisibleOnProcessingResults property of PXDSCallbackCommand to True, as shown in
the following example.
To not display the processing dialog box on a processing form, you can do one of the
following:
| Implementing Business Logic | 329
• To not display the processing dialog box for a custom form, override the IsProcessing
property of the graph that corresponds to the form, as shown in the following code.
• To not display the processing dialog box for a customized Acumatica ERP form, configure
the IsProcessing property of the graph that corresponds to the form in a graph
extension as shown in the following code.
• To not display the processing dialog box for all processing forms, add the
ProcessingProgressDialog key in the appSettings section of the web.config file of
the application set to False, as shown in the following example.
In This Chapter
• Asynchronous Execution
Asynchronous Execution
An instance of a graph is created on each round trip to process a request created by the user
on the appropriate form. After the request is processed, the graph instance must be cleared
from the memory of the Acumatica ERP server. If you implement code that might require a
long time to execute an action or to process a document or data, you should execute this
code asynchronously in a separate thread.
To make the system invoke the method in a separate thread, you can use the
PXLongOperation.StartOperation method. Within the method that you pass to
| Implementing Business Logic | 330
StartOperation, you can, for example, create a new instance of a graph and invoke a
processing method on that instance. The following code snippet demonstrates how you can
execute code asynchronously as a long-running operation in a method of a graph.
In the delegate method of a long-running operation, you can store a data object in the
_CustomInfo dictionary of the long-running operation and get the list of records processed
by the method. You can add to the dictionary any data object needed for a long-running
operation by using a SetCustomInfo method.
| Implementing Business Logic | 331
The following diagram shows that each long-running operation includes the _CustomInfo
dictionary, which can contain multiple key-value pairs with custom data.
Figure: Location of custom data in the memory of the Acumatica ERP server
See New way to work with CustomInfo of PXLongOperation at for more information about the use
of the dictionary.
When a user clicks an action button on a form to start a processing operation, the data
source control of the form generates a request to the Acumatica ERP server to execute the
action delegate defined for the button. The server creates an instance of the graph, which
provides the business logic for the form and invokes the action delegate method.
If the duration of the long-running operation is longer than five seconds, the server releases
the graph instance. Then the form, which is still opened in the browser, generates requests
to the server to get the results of the long-running operation every five seconds. For such a
request, the server uses the ID of the long-running operatio to check the operation status.
If the operation has completed, the server creates an instance of the graph and restores the
graph state and the cache data to finish processing the action delegate and to return results
to the form.
The following diagram shows how the server executes an action asynchronously and how it
returns the results of the action to the form.
When the user launches a report, either from the report form or by clicking an action button
on the maintenance or entry form, the system redirects the user to the report launcher form
(ReportLauncher.aspx), which is designed to automatically run a report for the received
parameters. The ASPX page for this form contains the PXReportViewer control, whose Java
Scripts objects and functions are designed to get the report data and display the data on the
form.
| Implementing Business Logic | 333
To run the report, the report launcher creates a request to the PX.Web.UI.PXReportViewer
control on the server. To process the request, the server instantiates the
PX.Reports.Web.WebReport class and invokes its Render method, which launches the report
generation as a long-running operation in a separate thread.
After the long-running operation has completed, the PXReportViewer control gets the report
data from the dictionary and displays the report on the report launcher form. For details on
how the report is displayed and how the report data is retrieved, see Display of Reports.
The following diagram shows how the server generates a report asynchronously and how it
returns the resulting report data to the report launcher form.
| Implementing Business Logic | 334
When the user clicks an action button on a form to start a processing operation, the load
balancer forwards the request to an Acumatica ERP server to execute the action delegate
defined for the button. The server creates an instance of the graph, which provides the
business logic for the form, and invokes the action delegate method.
When the action delegate method is executed, the StartOperation method creates
an instance of the PXAsyncResult class to hold the data and state of the long-running
| Implementing Business Logic | 335
If the duration of the long-running operation is longer than five seconds, the server releases
the graph instance, stores the serialized data of the graph in the remote storage, and
continues processing the long-running operation in a separate thread. When the operation
has completed, this server sets the operation status to PXLongRunStatus.Completed and
updates the operation data in the remote session storage.
Until the form that is still opened in the browsers obtains the request results, it generates
requests to the site URL every five seconds to get these results. On every such request, the
load balancer selects a server to be used to process the request and forwards the request
to the server. The server uses the long-running operation ID, which is usually equal to the
graph UID, to check the operation's status. If the operation is completed, the server creates
an instance of the graph to finish processing the action delegate and to return results to the
form.
The following diagram shows how the data of a user session and of a long-running operation
are stored in the remote session storage of a cluster.
Localizing Applications
Acumatica Framework provides built-in localization tools that you can use to translate
the user interface and application messages to different languages. This chapter provides
guidelines on how to prepare the Acumatica Framework application for localization efforts.
To prepare an application for localization, you must prepare data access classes (DACs) and
the application code.
In This Chapter
• Localization
Related Links
• Translation Dictionaries
Localization
Applications created with Acumatica Framework can be localized on the presentation,
business logic, and database level owing to:
• All messages returned from the business logic layer can be localized through the
dictionary mechanism. For details about how to implement message localization in your
code, see To Localize Application Messages.
Acumatica Framework provides a built-in utility that makes it possible for a user to localize
the product. Once localization is entered and applied, the application does not require any
recompilation or re-installation. Also, localization can be exported, imported, and merged.
| Implementing Business Logic | 337
For more information about how to use the built-in localization mechanism, see Translation
Process in the System Administration Guide.
Related Links
• Translation Process
The system collects for localization the string constants that are specified in the following
items of the application:
• The DisplayName property of the PXUIField attribute of the fields of data access classes
(DACs)
• The DisplayName property of the PXUIField attribute of fields and actions of a business
logic controller (BLC) object, which override the attributes of the fields and actions of a
DAC
• The Values property of the classes that implement the ILocalizableValues interface
• Captions of form, grid, and panel controls and labels of input controls, which are
specified in ASPX
• public const string fields of the classes marked with the PXLocalizable attribute
You can also translate user input to multiple languages and store translations in the
database. For more information on the localization of user input, see To Work with Multi-
Language Fields.
Related Links
• Translation Dictionaries
| Implementing Business Logic | 338
To prepare each DAC for localization, you need to perform the steps that are described in
this topic.
1. Make sure the DisplayName parameter of the PXUIField attribute is specified for each
visible field in the DAC, as shown in the following example.
If you change the DisplayName value of the PXUIField attribute on the fly (by
creating your own PXFieldState), you should localize the string independently.
2. Specify the values that should be displayed in drop-down lists by using the
PXStringList attribute, as shown in the following example.
To make your application display localized messages, you need to perform the steps, which
are described in this topic.
| Implementing Business Logic | 339
1. Move all strings that should be translated to the public static Messages class and specify
the PXLocalizable attribute for this class, as shown in the following code.
The exceptions to this requirement are field descriptions and list attributes in the
data access classes, which are handled separately. For details on how to make
field descriptions and list attributes localizable, see To Prepare DACs for Localization.
using System;
using PX.Data;
using PX.Common;
[PXLocalizable()]
public static class Messages
{
public const string FieldNotFound = "The field is not found.";
public const string InvalidAddress = "The address is not valid.";
public const string AdditionalData = "Author's title: {0}; author's name: {1}.";
}
2. If the message from the Messages class is used in an error or warning message,
which is displayed when an exception of the PXException type or of a type derived
from PXException is thrown, provide a non-localized message, as shown in the
following example. The system displays the localized message automatically if there is a
translation for this message in the database.
if (field == null)
{
throw new PXException(Messages.FieldNotFound);
}
3. If you need to receive the translation of a message from the Messages class within the
application code (for example, if the message is displayed in the confirmation dialog box,
which is displayed if you use the Ask() method of a data view in the code), use one of
the following methods:
• PXLocalizer.Localize(): The method returns the translation with the given key,
which you specify in the second parameter. A string may have multiple translations;
one translation for each occurrence of the string in the application. For each of the
occurrences, a key value is created. For example, if the string is declared in a class
marked with the PXLocalizable attribute, the full qualified name of the class is the
key, as the following code shows.
For details on multi-language fields on Acumatica ERP forms, see Managing Locales and
Languages.
1. In the data access class (DAC) that you want to contain a multi-language field, define
the NoteID field with the PXNote attribute, as follows.
[PXNote]
public virtual Guid? NoteID { get; set; }
2. If you want to configure a field to have values in multiple languages, annotate this field
with the PXDBLocalizableString attribute. The following code shows an example of the
use of the PXDBLocalizableString attribute.
3. If you need to give values in multiple languages to a field with the PXDBText attribute,
replace this attribute with the PXDBLocalizableString attribute and do not specify the
length parameter, as shown in the following example.
[PXDBLocalizableString(IsUnicode = true)]
| Implementing Business Logic | 341
4. If you need to configure a field that has the PXString attribute, which is used in
conjunction with the PXDBCalced attribute, replace the PXString attribute with the
PXDBLocalizableString attribute and set the value of the NonDB parameter to true, as
shown in the following example.
If you want a multi-language field to have a default value in a specific language, use the
PXLocalizableDefault attribute instead of the PXDefault attribute and specify in its
second parameter either a BQL field or a BQL select that provides language selection.
For example, in Acumatica ERP, the SOLine line description defaulted to the appropriate
InventoryItem description based on the language that is set for a customer. The
TransactionDesr field of the SOLine DAC has the PXLocalizableDefault attribute with a
second parameter that specifies the language as follows: typeof(Customer.languageName).
See the following example of the use of the PXLocalizableDefault attribute.
[PXLocalizableDefault(typeof(Search<InventoryItem.descr,
Where<InventoryItem.inventoryID,
Equal<Current<SOLine.inventoryID>>>>),
typeof(Customer.languageName),
PersistingCheck = PXPersistingCheck.Nothing)]
If you want to obtain the value of a multi-language field in the current locale, use
the PXDatabase.SelectSingle() or PXDatabase.SelectMulti() method, and pass
to it the return value of the PXDBLocalizableStringAttribute.GetValueSelect()
static method instead of passing a new PXDataField object to it. (The
PXDBLocalizableStringAttribute.GetValueSelect() method takes three input
parameters: the table name, the field name, and a Boolean flag that indicates whether
strings should be returned as text with unlimited length.)
If you want to obtain the value of a multi-language field in a specific language, use the
PXDBLocalizableStringAttribute.GetTranslation() method. Pass to the method as
input parameters a DAC cache, a DAC instance, a field name, and the ISO code of the
language.
tran.TranDesc =
PXDBLocalizableStringAttribute.GetTranslation(
Caches[typeof(InventoryItem)], item, typeof(InventoryItem.descr).Name,
customer.Current?.LanguageName);
Whether you set up centralized storage of localization data or disable localization, you should
perform the following steps:
2. Place the assembly file with the new provider in the Bin directory of the Acumatica ERP
instance, and add the assembly to the customization project as a File element.
3. Register the new provider in the pxtranslate element of the web.config file, as
described in To Register the New Provider in Web.config in this topic.
| Implementing Business Logic | 343
The code below defines a custom translation provider that loads the localization data from an
external Acumatica ERP database by using ADO.NET tools.
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using PX.Data;
using PX.Translation;
namespace Demo.Translation
{
public class DemoTranslationProvider : PXTranslationProvider
{
private struct TranslationKey
{
public Guid id;
public string resKey;
| Implementing Business Logic | 344
StringBuilder localizationTranslationSelectBld =
new StringBuilder("Select IDlt, ResKey, Value, Locale" +
"From LocalizationTranslation");
if (!string.IsNullOrEmpty(locale))
{
localizationTranslationSelectBld.AppendFormat(
| Implementing Business Logic | 345
command.CommandText = localizationTranslationSelect;
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
TranslationKey newTranslationKey = new TranslationKey()
{
id = reader.GetGuid(0),
resKey = reader.GetString(1),
locale = reader.GetString(3)
};
localizationTranslation.Add(newTranslationKey,
reader.GetString(2));
}
}
}
}
| Implementing Business Logic | 346
if (localizationTranslation.Count != 0)
{
foreach (Guid id in localizationValue.Keys)
{
IEnumerable<TranslationKey> localizationTranslationKeys =
from translationRowKey in localizationTranslation.Keys
where translationRowKey.id == id
select translationRowKey;
foreach (TranslationKey key in localizationTranslationKeys)
{
string translationResKey = key.resKey;
string translationLocale = key.locale;
string translationValue = localizationTranslation[key];
string value = escapeStrings ?
PXLocalizer.EscapeString(translationValue) :
translationValue;
if (string.IsNullOrEmpty(translationResKey))
{
dictionary.Append(
localizationValue[id],
new PXCultureValue(translationLocale, value));
}
else
{
dictionary.AppendException(
localizationValue[id],
new PXCultureEx(translationResKey,
translationLocale, value));
}
}
}
}
return dictionary;
}
}
}
| Implementing Business Logic | 347
To Disable Localization
Once the provider class is defined, register it in the web.config file by adding a new key
to the providers collection of the pxtranslate element and specifying the new key in the
defaultProvider property of pxtranslate. Use the add element to register the provider.
Set the name attribute to the key, which can be any unique value, and specify the type of the
custom provider in the type attribute.
<pxtranslate defaultProvider="DemoTranslationProvider">
<providers>
<!--The default translation provider-->
<remove name="PXDBTranslatonProvider" />
<add name="PXDBTranslatonProvider"
type="PX.Data.PXDBTranslatonProvider, PX.Data" />
With the ability to reuse business logic in Acumatica ERP or Acumatica Framework, you can
include the main business logic of particular functionality (such as multicurrency processing)
in reusable generic graph extensions and use this logic whenever you need to. If you need to
adjust this logic for the specifics of a particular module, you can override this business logic
in the implementation of this module. For example, you can assign different names for the
UI elements that are linked to the same fields of a data access class in different modules.
In this section, you can find information about generic graph extensions.
In This Chapter
• Reusable Business Logic Implementation • To Insert Reusable Business Logic That Has Al-
ready Been Declared
• Mapped Cache Extensions and the Application
Database • To Sort Multiple Generic Graph Extensions
• Reusable Business Logic and the Application • To Implement Reusable Business Logic
Website
You encapsulate the business logic that you want to reuse in a generic graph extension,
which is a graph extension that does not relate to any particular graph and can be used with
any base graph. The generic graph extension operates with data by using the mapped cache
extensions, which are cache extensions that are not 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. The following diagram
shows in yellow rectangles the classes that you need to implement to reuse the business
logic.
| Implementing Business Logic | 349
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.)
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 a mapped cache extension
to the fields of a base DAC by using the mapping class, which is described in the following
section. The mapped cache extension can also include the fields that are not mapped to any
base DAC fields. (For details, see Mapped Cache Extensions and the Application Database.)
The declaration of a field of a mapped cache extension includes the following two required
members, which are the same as the required members of a DAC field:
You derive the class from the IBqlField interface and assign it a name that starts with a
lowercase letter.
You assign the property a name that starts with an uppercase letter. The system assigns
the PXMergeAttributes attribute with MergeMethod.Merge to each field of a mapped
cache extension automatically. If you define the PXMergeAttributes attribute for a field
of a mapped cache extension explicitly, the explicitly defined attribute overrides the
automatically defined. You can also define any other attributes for the property field of
the mapped cache extension, or not define the attributes at all.
//CuryID field
public abstract class curyID : IBqlField
{
}
protected String _CuryID;
...
}
| Implementing Business Logic | 351
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:
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.
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 is not mapped to any base DAC field, as shown in the following diagram.
| Implementing Business Logic | 352
A generic graph extension is a public abstract class that encapsulates business logic that
can be used in multiple places of an Acumatica ERP application or an Acumatica Framework-
based application. The class inherits from the PXGraphExtension<TGraph> class, as the
following code shows.
• The protected abstract methods that return the mapping classes. You have to override
these methods in the implementation class.
• The views that can have either mapping-based declaration or standard declaration. You
declare a mapping-based view by using the PXSelectExtension<Table> class.
Implementation Class
• The main DAC of the primary data view of the base graph
| Implementing Business Logic | 353
In this class, you can override the mapping defined by the mapping class, override other the
methods of the base class, and insert your own views, methods, and event handlers, as the
following code shows.
Related Links
If a mapped cache extension includes fields that are bound to database columns (with the
type attributes that are derived from the PXDBFieldAttribute class, such as PXDBString),
the database table that corresponds to the base DAC must contain these fields of the
mapped cache extension. That is, the database table must include the fields bound to a
database column that are defined both in the base DAC and the mapped cache extension, as
shown in the following diagram.
Related Links
The actions that are defined in the base graph, generic graph extension, and implementation
class are automatically added by the system on the website page, as shown in the following
diagram. The implementation class can override the actions declared in the generic graph
extension.
| Implementing Business Logic | 356
During the initialization of a graph instance that includes reusable logic, the system adds
to the collection of data views the data views that are declared in the base graph, including
those that are declared in the implementation class or the generic graph extension from
which the implementation class inherits. The mapping classes define the PXCache<Table>
objects in which the mapping-based views keep data records.
Event handlers that are declared in the base graph, implementation class, and generic graph
extension are added by the system to the collections of event handlers in the corresponding
| Implementing Business Logic | 357
PXCache object. The event handlers defined for the fields or rows of the mapped cache
extension are added to the PXCache object of the base DAC type to which the mapped cache
extension is mapped.
Multi-Currency Extension
If you need to work with multiple currencies on a form, you can insert an implementation
of the MultiCurrencyGraph<TGraph, TPrimary> abstract class in the graph that provides
business logic for the form. For more information on the use of multiple currencies in the
system, see Currency Management in the Financial Management Guide.
| Implementing Business Logic | 358
The MultiCurrencyGraph<TGraph, TPrimary> class works with the following mapped cache
extensions:
If you need to work with multiple price lists on a form, you can insert an implementation
of the SalesPriceGraph<TGraph, TPrimary> abstract class in the graph that provides
business logic for the form. For more information on sales prices, see Managing Sales Prices in
the Financial Management Guide.
The SalesPriceGraph<TGraph, TPrimary> class works with the following mapped cache
extensions:
Discount Extension
If you need to work with discounts on a form, you can insert an implementation of the
DiscountGraph<TGraph, TPrimary> abstract class in the graph that provides business
logic for the form. For more information on discounts, see Configuring Customer Discounts in
the Financial Management Guide.
The DiscountGraph<TGraph, TPrimary> class works with the following mapped cache
extensions:
If you need to apply sales taxes to amounts of a form, you can insert an implementation
of the TaxGraph<TGraph, TPrimary> abstract class in the graph that provides business
logic for the form. For more information on taxes in the system, see Taxes in the Financial
Management Guide.
The TaxGraph<TGraph, TPrimary> class works with the following mapped cache extensions:
| Implementing Business Logic | 359
Related Links
1. Review the generic graph extension that provides the business logic that you want to
reuse as follows:
a. Identify the mapped cache extensions the generic graph extension works with and
the list of their fields, and decide whether the default mapping (which is defined by
the mapping class of the generic graph extension) is suitable for the base data access
class (DAC) that you are going to use.
b. Identify the fields of the mapped cache extension that are bound to columns of a
database table, and make sure the database table that corresponds to the base DAC
includes the columns to store the data from the mapped cache extension.
2. In the code of the graph you need to add the reusable business logic to, add the public
implementation class that derives from the generic graph extension of the needed type.
Use the following types in the type parameters of the generic graph extension:
• The main DAC of the primary data view of the base graph
In the following code, the MultiCurrency class extends the OpportunityMaint
graph. The MultiCurrency class derives from public abstract class
MultiCurrencyGraph<TGraph, TPrimary> : PXGraphExtension<TGraph>.
3. In the added class, override the abstract methods of the generic graph extension as
follows:
| Implementing Business Logic | 360
• In the overridden methods of the generic graph extension that return the mapping
classes, either use the default mapping of the fields of the mapped cache extension
to the fields of the base DAC or adjust the mapping.
• In the other overridden methods, implement the required business logic. For details
on the implementation of the methods in the generic graph extension declared in
Acumatica ERP, see API Reference.
4. In the added class, adjust the reused business logic by doing any of the following:
The following code shows a sample implementation of the MultiCurrency class, which
reuses the multicurrency business logic defined in the MultiCurrencyGraph generic
graph extension.
To define the order in which the generic graph extensions are applied, add the class inherited
from the Autofac.Module class and implement the sorting of the generic class extensions,
as the following code shows.
1. In the code of your application, define the mapped cache extensions, which inherit
from the PXMappedCacheExtension abstract class. For details on the mapped cache
extensions, see Mapped Cache Extension.
//CuryID field
public abstract class curyID : IBqlField
{
}
protected String _CuryID;
...
}
2. In the code of your application, define the generic graph extension as follows:
b. In the generic graph extension, for each mapped cache extension that you defined in
the first step, declare the protected mapping class, as shown in the following code.
For details on the mapping classes, see Mapping Class.
c. In the generic graph extension, for each mapping class, declare the protected
abstract method that returns the mapping class, as shown in the following code.
d. In the generic graph extension, define the views that use the mapped cache
extensions, as the following code shows. To define each view, you use the
PXSelectExtension<Table> : PXSelectBase<Table> class, where Table is a
mapped cache extension.
In the generic graph extension, you can define standard views as well as the
views that use the mapped cache extensions.
e. In the generic graph extension, define the reusable event handlers, as the following
code shows.
f. In the generic graph extension, implement any other business logic that you want to
reuse, such as filters and actions.
Once you have defined the mapped cache extensions and the generic graph extension, you
can insert reusable business logic to any part of your application, as described in To Insert
Reusable Business Logic That Has Already Been Declared.
| Troubleshooting Acumatica Framework-Based Applications | 365
In This Part
3. In the connectionStrings section of the file, modify the connection string by specifying
the credentials to your development database as follows:
• For a locally installed Microsoft SQL Server that uses SQL Server authentication (line
breaks are for display purposes only)
<connectionStrings>
<remove name="ProjectX" />
<add name="ProjectX" providerName="System.Data.SqlClient"
connectionString="Data Source=(local);Initial Catalog=Project_Catalog;
User Id=User_ID; Password=User_Password"
</connectionStrings>
• For a locally installed Microsoft SQL Server that uses Windows authentication (line
breaks are for display purposes only)
<connectionStrings>
<remove name="ProjectX" />
<add name="ProjectX" providerName="System.Data.SqlClient"
connectionString="Data Source=(local);Initial Catalog=Project_Catalog;
Integrated Security=True"/>
</connectionStrings>
• For a remote Microsoft SQL Server that uses SQL Server authentication (line breaks
are for display purposes only)
connectionStrings>
| Troubleshooting Acumatica Framework-Based Applications | 366
4. In the system.web section of the file, set the debug attribute of the compilation
element to True, as shown in the following example.
5. In Visual Studio, right-click the Site folder of the solution, and click Set as StartUp
Project.
6. Right-click the Main.aspx file in the Site folder, and click Set as Start Page.
7. Optional: If you need to debug a server error that throws an exception, do the following:
Glossary
The following table contains definitions of the basic terms used in Acumatica Framework.
Term Definition
Action An interface for executing a specific operation with data that is
implemented in a graph. An action is represented by the corre-
sponding button on the user interface.
Acumatica Cloud xRP The platform for the development of cloud ERP applications
Platform (such as Acumatica ERP and customizations of it), the mobile
application for Acumatica ERP, and applications integrated with
Acumatica ERP by means of the web services API.
Acumatica Customiza- The part of the Acumatica Cloud xRP Platform that provides cus-
tion Platform tomization tools for the development of applications embedded
in Acumatica ERP (also called customizations of Acumatica ERP).
Acumatica Framework The part of the Acumatica Cloud xRP Platform that provides the
platform API, web controls, and other tools for building ERP ap-
plications.
Acumatica Framework A set of Visual Studio templates provided as a part of Acumati-
Templates ca Framework for creating ASPX pages and business logic con-
trollers.
Acumatica Frame- An application created by means of Acumatica Framework tools.
work-based applica-
tion
Analytical report A report created with the Analytical Report Manager. For details
on this tool, see Analytical Report Manager.
Bound field A data field that represents a column from a database table.
Compare to Unbound field.
BQL statement A generic BQL class specialization that represents a specific
query to the database. The type parameters specified in the BQL
statement are BQL operator classes and DACs.
Business logic con- See Graph.
troller (BLC)
Business query lan- A set of generic classes for querying data records from the data-
guage (BQL) base.
Cache A collection of modified data records from the same table stored
in the user session and shared between requests.
Custom report A report created on the custom report form.
Customization of A modification of the user interface, business logic, and the
Acumatica ERP database scheme without recompilation and re-installation of
| Glossary | 368