How to call Persist of PXgraph without triggering the code written in that graph

Sometimes there is a need to call the PXGraph.Persist method from a graph extension but without triggering the code written in the base graph itself. I had this issue with using the ARSalesPriceMaint graph, so will show all the examples on that graph extension.

The problem is, it’s not possible to just call it, as it is not reachable from the GraphExtension level.

There was a solution by using reflection. Something like this:

public void Persist(Intercompany.PersistDelegate baseMethod)
{
    //base.Persist();
    MethodInfo fooA = typeof(PXGraph).GetMethod("Persist",
        BindingFlags.Public | BindingFlags.Instance, nullnew Type[] { }, null);
    DynamicMethod baseBasePersist = new DynamicMethod("foo_A",
        nullnew[] { typeof(PXGraph) }, typeof(PXGraph));
    ILGenerator il = baseBasePersist.GetILGenerator();
    il.Emit(OpCodes.Ldarg, 0);
    il.EmitCall(OpCodes.Call, fooA, null);
    il.Emit(OpCodes.Ret);
    baseBasePersist.Invoke(nullnew object[] { Base });
 
}

 

The problem with this solution is that first of all, any error messages thrown from the base Persist method will not appear on the screen instead of them only the “Exception has been thrown by the target of an invocation” appears. The second and the bigger issue is – all the caches will remain not cleared. It will cause some unexpected behavior, when the already deleted wrong record won’t allow to save the screen.

Since for the PXGraph.Persist we don't need specific graph logic, instead of calling the PXGraph from ARSalesPriceMaint base graph, we can create a completely empty new custom graph, then call it from our graph extension while passing all the caches and views. Additionally, we can even add some validations to the persist at the graph level.

This is the code example:

internal class ARSalesPriceMaintPersistHelper : PXGraph<ARSalesPriceMaintPersistHelper>
{
    public override void Persist()
    {
        MyValidation();
        base.Persist();
    }
 
    protected virtual void MyValidation()
    {
        PXTrace.WriteInformation("Overriden Validation is called");
    }
}
 
public class ext : PXGraphExtension<ARSalesPriceMaint>
{
    public delegate void PersistDelegate();
    [PXOverride]
    public void Persist(PersistDelegate baseMethod)
    {
        ARSalesPriceMaintPersistHelper ph = PXGraph.CreateInstance<ARSalesPriceMaintPersistHelper>();
        ph.Views = Base.Views;
        ph.Caches = Base.Caches;
        ph.Persist();
        Base.Cancel.Press();
    }
}

 The Graph was made internal to try to avoid its appearance in different graph selector in Acumatica, but it can also be just public.

 Summary

In addressing the challenge of invoking PXGraph.Persist in Acumatica from a graph extension, there is a nuanced solution beyond the initial reflection method. The initial approach, while innovative, led to issues with error messaging and cache management. Solution involves creating a new, custom graph specifically for handling the Persist method. This method ensures functional integrity and enables custom validations, effectively bypassing the limitations of the reflection-based approach. The article concludes with a practical code example, highlighting the new graph's internal designation to restrict its visibility in Acumatica.

 

Another process has updated the record, your changes will be lost

Hello everybody,

Today a want to share with you couple approaches that can help you to fix the famous Acumatica’s error:

“Another process has updated the {Table} record. Your changes will be lost”

You can get this error when you open two the same screens with the same record from DB and then modify data on both screens and try to Save.

Or you may have custom logic that runs some functionality in PXLongOperation (that is, run in multi-threading mode) and one record from DB can be modified by different threads. Then you will get this error during persisting.

Or maybe you need to create and to release several documents while code modifies the same record or uses same inherited table. For example, you create and release invoice, and then create and release payment, both of them update the ARRegister table.

 

Generally speaking, we get this error when someone (something) tries to store in the DB a record that was modified earlier and the record now has another stage (i.e., another value). Means there is a conflict between user’s copy of data and actually stored in database.

In what way may it come? As you may know Acumatica persists rows one by one (in most cases). And before persisting to DB, Acumatica keeps data and changes in a graph’s specified cache. So, when a record is opened in several screens, the record will be kept in different caches. Thus, it might be that several different screens might have different versions of same record with different changes. That may cause concurrent updates, when a database needs to understand changes’ priority.

To handle concurrent updates Acumatica uses the special Timestamp data field. In the DAC this field has PXDBTimestamp attribute. In database this column has name TStamp with timestamp data type.

Acumatica handles the Timestamp field automatically and checks the record version every time the record is modified. When a record is updated, the Timestamp field is increased, in this way every next version of a record will have bigger Timestamp value than pervious.

Then, each time a graph needs to persist a record or changes to the DB, Acumatica verifies the version of the record to ensure that the record has not been modified by another process. And if Timestamp value in the cache differs against Timestamp value in the DB, you’ll get the error we are talking about here.

Yes, there might be many honest reasons for this mismatching. Like, another user or another code has really done changes the same time. Or record has been created with mistake, and Acumatica may not find corresponding record in DB.

 

We propose you three approaches that can help fix concurrent update error and to allow running code logic till the end.

You may find two first approaches in source code of Acumatica Framework; Acumatica often uses them.

  1. To use SelectTimeStamp() graph’s method

When you invoke this methos, a graph selects and sets the TimeStamp field with new value and then you can persist data to the DB.

Here one of Acumatica’s examples — the Persist method from the VendorMaint graph:

		public override void Persist()
		{
			using (PXTransactionScope ts = new PXTransactionScope())
			{
				bool persisted = false;
				try
				{
					BAccountRestrictionHelper.Persist();
                    persisted = (base.Persist(typeof(Vendor), PXDBOperation.Update) > 0);
				}
				catch
				{
					Caches[typeof(Vendor)].Persisted(true);
					throw;
				}
				base.Persist();
				if (persisted)
				{
					base.SelectTimeStamp();
				}
				ts.Complete();
			}
		}
And one more example:
private void PrepareForPOCreate(List<SOLine> listSO)
{
    foreach (SOLine item in listSO)
    {
        item.Qty = item.GetExtension<APSOLineExt>().UsrMasterSOQty;
 
        var inventoryItem = UpdatePOOrderExtension.CheckDefVendor(this.Base, item.InventoryID);
 
        if (inventoryItem == null)
        {
            Base.Caches<SOLine>().SetValueExt<SOLine.pOCreate>(item, false);
            Base.Transactions.Cache.Update(item);
        }
    }
 
    Base.SelectTimeStamp();
    Base.Save.Press();
}

 2. To use PXTimeStampScope.SetRecordComesFirst() method

Sometimes first approach doesn’t help and the error still occurs.

This is because, by default, Acumatica checks TimeStamp only when key fields have been modified.

Calling to the SetRecordComesFirst() method activates RecordComesFirst flag and hereupon Acumatica checks TimeStamp value for all changes in cache.

Here is an example how to create and release Payment:

private static ARPayment CreateAndReleasePayment(ARInvoice arInvoice)
{
    ARPaymentEntry paymentEntry = PXGraph.CreateInstance<ARPaymentEntry>();
 
    var arAdjust = SelectFrom<ARAdjust>.Where<ARAdjust.adjdRefNbr.IsEqual<@P.AsString>>.View.Select(paymentEntry, arInvoice.RefNbr)?.TopFirst;
 
    if (arAdjust?.AdjgRefNbr == null)
    {
        paymentEntry.CreatePayment(arInvoice, null, arInvoice.DocDate, arInvoice.FinPeriodID, false);
 
        paymentEntry.Document.Current.ExtRefNbr = arInvoice.DocDesc;
        paymentEntry.Document.Current.BranchID = arInvoice.BranchID;
        paymentEntry.Document.UpdateCurrent();
        paymentEntry.Save.Press();
    }
    else
    {
        paymentEntry.Document.Current = SelectFrom<ARPayment>.Where<ARPayment.refNbr.IsEqual<@P.AsString>>.View.Select(paymentEntry, arAdjust.AdjgRefNbr)?.TopFirst;
    }
 
    ARPayment arPayment = paymentEntry.Document.Current;
 
    paymentEntry.Clear();
 
    ARRegister doc = arPayment;
    List<ARRegister> list = new List<ARRegister>() { doc };
 
    using (new PXTimeStampScope(null))
    {
        PXTimeStampScope.SetRecordComesFirst(typeof(ARRegister), true);
 
        try
        {
            ARDocumentRelease.ReleaseDoc(list, false);
        }
        catch (PXException e)
        {
            PXTrace.WriteError(e.Message);
        }
    }
 
    return arPayment;
}

Usage by Acumatica team of this method in ARInvoiceEntry:

public void ReleaseProcess(List<ARRegister> list)
{
    PXTimeStampScope.SetRecordComesFirst(typeof(ARInvoice), true);
 
    ARDocumentRelease.ReleaseDoc(list, falsenull, (ab) => { });
}

 3. To use PXTimeStampScope.DuplicatePersisted() method

This approach is the most difficult, but fixes the error when ordinary approaches don’t work.

There are no examples in Acumatica Framework source code. The next examples are my samples to override persist methods.

public class ARDocumentReleaseExt : PXGraphExtension<ARReleaseProcess>
{
    public static bool IsActive() => true;
 
    [PXOverride]
    public virtual void Persist(Action baseMethod)
    {
        var updRegisters = Base.ARDocument.Cache.Updated.Cast<ARRegister>().ToList();
        baseMethod?.Invoke();
 
        var lazyTempGraph = new Lazy<PXGraph>(() => PXGraph.CreateInstance<PXGraph>());
 
        foreach (ARRegister updRegister in updRegisters)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARPayment)], updRegister, typeof(ARRegister));
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARInvoice)], updRegister, typeof(ARRegister));
        }
    }
}
And second example:
public class ARPaymentEntryExt : PXGraphExtension<ARPaymentEntry>
{
    public static bool IsActive() => true;
 
    [PXOverride]
    public virtual void Persist(Action baseMethod)
    {
        var updInvoices = Base.ARInvoice_DocType_RefNbr.Cache.Updated.Cast<ARInvoice>().ToList();
        var updPayments = Base.Document.Cache.Updated.Cast<ARPayment>().ToList();
 
        baseMethod?.Invoke();
 
        var lazyTempGraph = new Lazy<PXGraph>(() => PXGraph.CreateInstance<PXGraph>());
        foreach (ARInvoice updInvoice in updInvoices)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARRegister)], updInvoice, typeof(ARInvoice));
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARPayment)], updInvoice, typeof(ARInvoice));
        }
        foreach (ARPayment updPayment in updPayments)
        {
            PXTimeStampScope.DuplicatePersisted(lazyTempGraph.Value.Caches[typeof(ARRegister)], updPayment, typeof(ARPayment));
        }
    }
}

 

In the second example described the approach usage in case when you need to persists dependent documents.

In both examples is also shown lazy graph initialization. This is not necessary to use it.
I use it for performance need, when I had long and heavy processing logic on my custom processing screen.

 Summary

Now you have needed knowledge and strategies on how to fix the "Another process has updated the {Table} record. Your changes will be lost" error in Acumatica, which occurs when multiple users try to modify the same record in the database. The error can be resolved by using the Timestamp field, which Acumatica uses to handle concurrent updates. The article proposes three approaches to fixing the error, including using the SelectTimeStamp() method, calling the SetRecordComesFirst() method, and using the DuplicatePersisted() method. The article provides code examples for each approach and explains how they can be used to fix the error.