0
votes

I need to run some address validation on Customer Location addresses using a 3rd party API to determine if the address is residential or commercial. This validation should run whenever an address field is changed. In other words, the validation should be run in the Address_RowUpdated event handler.

Because the function is calling a 3rd party API, I believe that it should be done in a separate thread, using PXLongOperation so that it does not hold up address saving and fails gracefully if the API is unavailable or returns an error.

However, I am not sure if the architecture of running a long operation within an event handler is supported or if a different approach would be better.

Here is my code.

public class CustomerLocationMaint_Extension : PXGraphExtension<CustomerLocationMaint>
{
    protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
    {
        PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
        if (row != null)
        {
            Location location = this.Base.Location.Current;
            PXCache locationCache = Base.LocationCurrent.Cache;

            PXLongOperation.StartOperation(Base, delegate
            {
                RunCheckResidential(location, locationCache); 
            });

            this.Base.LocationCurrent.Cache.IsDirty = true; 
        }
    }

    protected void RunCheckResidential(Location location, PXCache locationCache)
    {
        string messages = "";

        PX.Objects.CR.Address defAddress = PXSelect<PX.Objects.CR.Address,
            Where<PX.Objects.CR.Address.addressID, Equal<Required<Location.defAddressID>>>>.Select(Base, location.DefAddressID);

        FValidator validator = new FValidator();
        AddressValidationReply reply = validator.Validate(defAddress);
        AddressValidationResult result = reply.AddressResults[0];

        bool isResidential = location.CResedential ?? false; 
        if (result.Classification == FClassificationType.RESIDENTIAL) 
        {
            isResidential = true;
        } else if (result.Classification == FClassificationType.BUSINESS)
        {
            isResidential = false;
        } else
        {
            messages += "Residential classification is: " + result.Classification + "\r\n";
        }
        location.CResedential = isResidential;

        locationCache.Update(location);
        Base.LocationCurrent.Update(location);
        Base.Actions.PressSave();

        // Display relevant messages
        if (reply.HighestSeverity == NotificationSeverityType.SUCCESS)
            String addressCorrection = validator.AddressCompare(result.EffectiveAddress, defAddress);
            if (!string.IsNullOrEmpty(addressCorrection))
                messages += addressCorrection;
        }

        PXSetPropertyException message = new PXSetPropertyException(messages, PXErrorLevel.Warning);
        PXLongOperation.SetCustomInfo(new LocationMessageDisplay(message));
        //throw new PXOperationCompletedException(messages); // Shows message if you hover over the success checkmark, but you have to hover to see it so not ideal

    }

    public class LocationMessageDisplay : IPXCustomInfo
    {
        public void Complete(PXLongRunStatus status, PXGraph graph)
        {
            if (status == PXLongRunStatus.Completed && graph is CustomerLocationMaint)
            {
                ((CustomerLocationMaint)graph).RowSelected.AddHandler<Location>((sender, e) =>
                {
                    Location location = e.Row as Location;
                    if (location != null)
                    {
                        sender.RaiseExceptionHandling<Location.cResedential>(location, location.CResedential, _message);
                    }
                });
            }
        }

        private PXSetPropertyException _message;

        public LocationMessageDisplay(PXSetPropertyException message)
        {
            _message = message;
        }
    }
}

UPDATE - New Approach

As suggested, this code now calls the LongOperation within the Persist method.

protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
    {
        PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
        if (row != null)
        {
            Location location = Base.Location.Current;
            LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);
            locationExt.UsrResidentialValidated = false;
            Base.LocationCurrent.Cache.IsDirty = true; 
        }
    }   

public delegate void PersistDelegate();
    [PXOverride]
    public virtual void Persist(PersistDelegate baseMethod)
    {
        baseMethod();

        var location = Base.Location.Current;
        PXCache locationCache = Base.LocationCurrent.Cache;
        LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);

        if (locationExt.UsrResidentialValidated == false)
        {
            PXLongOperation.StartOperation(Base, delegate
            {
                CheckResidential(location);
            });
        }
    }

public void CheckResidential(Location location)
    {
        CustomerLocationMaint graph = PXGraph.CreateInstance<CustomerLocationMaint>();

        graph.Clear();
        graph.Location.Current = location;

        LocationExt locationExt = location.GetExtension<LocationExt>();
        locationExt.UsrResidentialValidated = true;

        try
        {
          // Residential code using API (this will change the value of the location.CResedential field)
        } catch (Exception e)
        {
            throw new PXOperationCompletedWithErrorException(e.Message);
        }

        graph.Location.Update(location);
        graph.Persist();
    }
1

1 Answers

0
votes

PXLongOperation is meant to be used in the context of a PXAction callback. This is typically initiated by a menu item or button control, including built-in actions like Save.

It is an anti-pattern to use it anytime a value changes in the web page. It should be used only when a value is persisted (by Save action) or by another PXAction event handler. You should handle long running validation when user clicks on a button or menu item not when he changes the value.

For example, the built in Validate Address feature is run only when the user clicks on the Validate Address button and if validated requests are required it is also run in a Persist event called in the context of the Save action to cancel saving if validation fails.

This is done to ensure user expectation that a simple change in a form/grid value field doesn't incur a long validation wait time that would lead the user to believe the web page is unresponsive. When the user clicks on Save or a specific Action button it is deemed more reasonable to expect a longer wait time.

That being said, it is not recommended but possible to wrap your PXLongOperation call in a dummy Action and asynchronously click on the invisible Action button to get the long operation running in the proper context from any event handler (except Initialize):

using PX.Data;
using System.Collections;

namespace PX.Objects.SO
{
    public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
  {
      public PXAction<SOOrder> TestLongOperation;

      [PXUIField(DisplayName = "Test Long Operation", Visible = false, Visibility = PXUIVisibility.Invisible)]
      [PXButton]
      public virtual IEnumerable testLongOperation(PXAdapter adapter)
      {
        PXLongOperation.StartOperation(Base, delegate () 
        { 
            System.Threading.Thread.Sleep(2000);
            Base.Document.Ask("Operation Done", MessageButtons.OK);
        });

        return adapter.Get();
      }

      public void SOOrder_OrderDesc_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
      {
        if (!PXLongOperation.Exists(Base.UID))
        {
            // Calling Action Button asynchronously so it can run in the context of a PXAction callback
            Base.Actions["TestLongOperation"].PressButton();
        }
      }
  }
}