Asynchronous Operations: General Information
Each time a user action on a form triggers a request (a round trip), the system creates a new graph instance to process that request. After the request is processed, the graph instance must be cleared from the memory of the MYOB Acumatica server. If your code performs a long-running operation—such as processing a document, processing large volumes of data, or executing an action that needs to call an external API—you should run it asynchronously in a separate thread.
The MYOB Acumatica Framework provides the following approaches for executing a long-running operation asynchronously:
- The IGraphLongOperationManager interface: Derives from the ILongOperationManager interface. We recommend using this interface when you need to run an operation asynchronously from within a graph or graph extension.
- The ILongOperationManager interface: Provides a modern approach for handling execution of asynchronous operations. We recommend that you use this interface when you need to execute an operation asynchronously outside of a graph or graph extension, such as in a service where you don't have access to a graph's instance.
- The PXLongOperation class: Provides a legacy approach for asynchronous execution.
In this chapter, you’ll learn how to run an operation asynchronously by using the IGraphLongOperationManager and ILongOperationManager interfaces, as well as the PXLongOperation class.
Learning Objectives
In this chapter, you’ll learn how to do the following:
- Implement a long-running action
- Add the associated button to the table toolbar
- Set up the long-running action to run asynchronously by using the LongOperationManager property of a graph
Applicable Scenarios
You set up an operation to run asynchronously when this operation is expected to take a long time to complete.
Use of the IGraphLongOperationManager Interface
You don't need to implement the IGraphLongOperationManager interface manually because it’s already injected as a dependency-injection service into PXGraph out of the box.
You can access this interface in a graph via the graph’s LongOperationManager property. In a graph extension, you can access this property via Base.LongOperationManager. The interface has useful method overloads—which don't require an object? key parameter—for executing a long-running operation. When you use one of these overloads, the framework automatically associates the long-running operation with the current graph instance.
To execute a long-running operation asynchronously in your graph, you can use the following methods:
- void StartOperation(Action<CancellationToken>? method), which takes in a synchronous delegate as a parameter
- void StartAsyncOperation(Func<CancellationToken, Task> method), which takes in an asynchronous delegate as a parameter
The following code shows you how to use void StartOperation(Action<CancellationToken>? method) to execute a long-running operation in a graph.
public class RepairWOProcessingGraph : PXGraph<RepairWOProcessingGraph>
{
// Method that defines the long-running operation
private void ProcessRepairWorkOrdersInternal(CancellationToken token)
{
// Long-running logic
// Optional: Periodically honor cancellation
token.ThrowIfCancellationRequested();
}
// Method that kicks off the long-running operation
public void ProcessRepairWorkOrders()
{
// In a graph, the property LongOperationManager (IGraphLongOperationManager)
// is already available/injected.
LongOperationManager.StartOperation(ProcessRepairWorkOrdersInternal);
}
}
Note that the void StartOperation(Action<CancellationToken>? method) method shown in the code example above is an overload of the void StartOperation(object? key, Action<CancellationToken>? method) of the ILongOperationManager interface. Similarly, the void StartAsyncOperation(Func<CancellationToken, Task> method) method is an overload of void StartOperation(object? key,Func<CancellationToken, Task> method). We recommend that you use these overloads because they don't require you to provide an object? key parameter. When you use these overloads, the framework automatically associates the long-running operation with the current graph instance.
For the full list of methods available in the IGraphLongOperationManager interface, see API Reference.
Use of the ILongOperationManager Interface
You use the ILongOperationManager when you need to execute a long-running operation asynchronously outside of a graph or a graph extension, such as in a service where you don't have access to a graph's instance. To do this, you need to first use dependency injection to inject the ILongOperationManager service into a type. You can then use this type to execute a long-running operation asynchronously, as shown below.
LongOperationManager property instead of injecting the
ILongOperationManager service. This keeps the operation properly
associated with the graph lifecycle and UI context.public class RepairWOProcessingService
{
// Inject ILongOperationManager via DI
private readonly ILongOperationManager _longOperationManager;
public RepairWOProcessingService(ILongOperationManager longOperationManager)
{
// Initialize _longOperationManager
_longOperationManager = longOperationManager;
}
// Method that defines the long-running operation
private void ProcessRepairWorkOrdersInternal(CancellationToken token)
{
// Long-running logic
// Optional: periodically honor cancellation
token.ThrowIfCancellationRequested();
}
// Method that kicks off the long-running operation for the service
public void ProcessRepairWorkOrders()
{
// Unique ID for the long-running operation
var key = Guid.NewGuid();
// Use the StartOperation method of the ILongOperationManager interface
// to execute the long-running operation asynchronously in a separate thread.
_longOperationManager.StartOperation(key, ProcessRepairWorkOrdersInternal);
}
}
Note that in the example above, the void StartOperation(object? key, Action<CancellationToken>? method) method of the ILongOperationManager interface is used to execute a long-running operation asynchronously, which takes in a object? key and a synchronous delegate as parameters. The interface also provides the void StartOperation(PXGraph graph, Action<CancellationToken>? method) overload, which you can use to execute a long-running operation when a graph's instance is available. For this method, you pass in the graph's instance instead of a object? key.
The ILongOperationManager interface also provides the void StartAsyncOperation(object? key, Func<CancellationToken, Task> method) method, which takes in an asynchronous delegate as a parameter. It has the void StartAsyncOperation(PXGraph graph, Func<CancellationToken, Task> method) method as an overload. For details, see the API Reference.
Use of the PXLongOperation Class
You can use the PXLongOperation class when you need to execute a long-running operation asynchronously.
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 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.
PXGraph.CreateInstance<T>() method. Do not use the new
T() graph constructor because in this case, no extensions or overrides of the
graph are initialized.public class MyGraph : PXGraph
{
...
public void MyMethod()
{
...
PXLongOperation.StartOperation(this, delegate()
{
// insert the delegate method code here
...
GraphName graph = PXGraph.CreateInstance<GraphName>();
foreach (... in ...)
{
...
}
...
});
...
}
...
}
If you need to save data to the database inside a long-running operation, call the Save.Press() method of the current graph. We recommend not using the Actions.PressSave() method because it performs an external call and should be used from the UI only.
The following code shows an example of a method called InvoiceOrder that's
executed asynchronously. This method is being called within the delegate that you pass to
PXLongOperation.StartOperation() method.
PXLongOperation.StartOperation(this, delegate ()
{
InvoiceOrder(graphCopy);
});
The PXLongOperation.StartOperation() method creates a separate thread and
executes the specified delegate asynchronously on this thread. The method passed into
PXLongOperation.StartOperation() matches the following delegate type,
which has no input parameters.
delegate void PXToggleAsyncDelegate();
(delegate()) anonymous method definition
is used to shorten the code.Inside the delegate() method, you shouldn’t use members of the current
graph, because this would lead to synchronous execution of the method. Instead, use a copy
of the graph, which you can create by using the var graphCopy =
this.Clone(); statement.
Invocation of a Long-Running Operation in an Action
If you need to invoke a long-running operation in an action, the action handler for a
processing operation must return IEnumerable. If you use the
void action handler instead, the processing of the long-running operation
and its result will not be displayed in the UI.
Below you can see an example of an action definition that runs a long-running operation.
public PXAction<RSSVWorkOrder> Assign = null!; [PXButton] [PXUIField(DisplayName = "Assign", Enabled = false)] protected virtual IEnumerable assign(PXAdapter adapter) { <Invocation of a long-running operation> }
