How to debug PXCheckUnique attribute

Hi everyone,

today I want to tell you a story about PXCheckUnique attribute.

I had a task to customize PXCheckUnique, and for my disappointment for two tabs inherited attribute worked perfectly fine, but for two others it didn't. 

As one of the ways, I wanted to override method ValidateDuplicates, but soon I found that it is private, I was kind of disappointed, as it means I can't neither debug it, nor override it.

As workaround I've decided to copy/paste all it's internals with help of dnSpy, and debug it. For such a purpose following code was born:

 

public class PXCheckUniqueYZAttribute : PXEventSubscriberAttribute, IPXRowInsertingSubscriber, IPXRowUpdatingSubscriber, IPXRowPersistingSubscriber
 {
     /// <summary>
     /// The additional <tt>Where</tt> clause that filters the data records
     /// that are selected to check uniqueness of the field value among them.
     /// </summary>
     public System.Type Where;
     protected string[] _UniqueFields;
     protected PXView _View;
     private const string DefaultErrorMessage = "An attempt was made to add a duplicate entry.";
     private string _errorMessage;
     private bool callInprocess;
 
     /// <summary>Initializes a new instance of the attribute.</summary>
     /// <param name="fields">Fields. The parameter is optional.</param>
     public PXCheckUniqueYZAttribute(params System.Type[] fields)
     {
         this._UniqueFields = new string[fields.Length + 1];
         for (int index = 0; index < fields.Length; ++index)
             this._UniqueFields[index] = fields[index].Name;
     }
 
     /// <exclude />
     public bool IgnoreNulls { getset; } = true;
 
     public bool UniqueKeyIsPartOfPrimaryKey { getset; }
 
     public bool IgnoreDuplicatesOnCopyPaste { getset; }
 
     /// <summary>
     /// Gets of sets the value that indicates whether the field value
     /// is cleared when it duplicates a value in another data record.
     /// By default, the property equals <tt>true</tt>.
     /// </summary>
     public bool ClearOnDuplicate { getset; } = true;
 
     /// <summary>
     /// Gets or sets the value of custom error message.
     /// If message is not set, then default message will be shown.
     /// </summary>
     public string ErrorMessage
     {
         get => this._errorMessage ?? "An attempt was made to add a duplicate entry.";
         set => this._errorMessage = value;
     }
 
     /// <exclude />
     public override void CacheAttached(PXCache sender)
     {
         base.CacheAttached(sender);
         sender.Graph.FieldDefaulting.AddHandler(sender.GetItemType(), this._FieldName, new PXFieldDefaulting(this.OnFieldDefaulting));
         this._UniqueFields[this._UniqueFields.Length - 1] = this._FieldName;
         System.Type itemType = sender.GetItemType();
         System.Type type1 = this.Where;
         if ((object)type1 == null)
             type1 = typeof(PX.Data.Where<True, Equal<True>>);
         System.Type type2 = type1;
         for (int index = 0; index < this._UniqueFields.Length; ++index)
         {
             System.Type bqlField = sender.GetBqlField(this._UniqueFields[index]);
             type2 = BqlCommand.Compose(typeof(Where2<,>), typeof(PX.Data.Where<,,>), bqlField, typeof(IsNull), typeof(And<,,>), typeof(Current<>), bqlField, typeof(IsNull), typeof(Or<,>), bqlField, typeof(Equal<>), typeof(Current<>), bqlField, typeof(And<>), type2);
         }
         System.Type type3 = BqlCommand.Compose(typeof(Select<,>), itemType, type2);
         this._View = new PXView(sender.Graph, false, BqlCommand.CreateInstance(type3));
     }
 
     /// <exclude />
     public void RowInserting(PXCache sender, PXRowInsertingEventArgs e)
     {
         if (e.Row == null || !this.IgnoreNulls && ((IEnumerable<string>)this._UniqueFields).Any<string>((Func<stringbool>)(field => sender.GetValue(e.Row, field) == null)))
             return;
         e.Cancel = !this.ValidateDuplicates(sender, e.Row, (object)null);
     }
 
     /// <exclude />
     public void RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
     {
         this.ClearErrors(sender, e.NewRow);
         if (e.Row != null && e.NewRow != null && this.CheckUpdated(sender, e.Row, e.NewRow))
             e.Cancel = !this.ValidateDuplicates(sender, e.NewRow, e.Row);
         if (!this.ClearOnDuplicate || !PXCheckUniqueYZAttribute.CheckEquals(sender.GetValue(e.Row, this._FieldName), sender.GetValue(e.NewRow, this._FieldName)) || !e.Cancel)
             return;
         this.ClearErrors(sender, e.NewRow);
         sender.SetValue(e.NewRow, this._FieldName, (object)null);
         e.Cancel = !this.ValidateDuplicates(sender, e.NewRow, e.Row);
     }
 
     /// <exclude />
     public void RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
     {
         if (e.Row == null || e.Operation == PXDBOperation.Delete)
             return;
         e.Cancel = !this.ValidateDuplicates(sender, e.Row, (object)null);
     }
 
     private void ClearErrors(PXCache senderobject row)
     {
         foreach (string uniqueField in this._UniqueFields)
         {
             string error = PXUIFieldAttribute.GetError(sender, row, uniqueField);
             if (!string.IsNullOrEmpty(error) && this.CanClearError(error))
                 PXUIFieldAttribute.SetError(sender, row, uniqueField, (string)null);
         }
     }
 
     protected virtual bool CanClearError(string errorText) => PXMessages.Localize(this.ErrorMessage).EndsWith(errorText);
 
     protected virtual void OnFieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
     {
         if (this.callInprocess || e.Cancel)
             return;
         this.callInprocess = true;
         object newValue = (object)null;
         sender.RaiseFieldDefaulting(this._FieldName, e.Row, out newValue);
         if (newValue != null)
         {
             object copy = sender.CreateCopy(e.Row);
             sender.SetValue(copy, this._FieldName, newValue);
             e.NewValue = this.ValidateDuplicates(sender, copy, (object)null) || !this.ClearOnDuplicate ? newValue : (object)null;
             e.Cancel = true;
         }
         this.callInprocess = false;
     }
 
     private bool CheckUpdated(PXCache senderobject rowobject newRow)
     {
         foreach (string uniqueField in this._UniqueFields)
         {
             if (!PXCheckUniqueYZAttribute.CheckEquals(sender.GetValue(row, uniqueField), sender.GetValue(newRow, uniqueField)))
                 return true;
         }
         return false;
     }
 
     /// <summary>
     /// Checks whether the provided objects are equal, ignoring the case
     /// if the provided objects are strings.
     /// </summary>
     /// <param name="v1">The first object to compare.</param>
     /// <param name="v2">The second object to compare.</param>
     /// <returns></returns>
     public static bool CheckEquals(object v1object v2) => !(v1 is string) && !(v2 is string) ? object.Equals(v1, v2) : string.Compare((string)v1, (string)v2, true) == 0;
 
     private bool CheckDefaults(PXCache senderobject row)
     {
         foreach (string uniqueField in this._UniqueFields)
         {
             bool flag = false;
             foreach (PXEventSubscriberAttribute attribute in sender.GetAttributes(row, uniqueField))
             {
                 if (attribute is PXDefaultAttribute && ((PXDefaultAttribute)attribute).PersistingCheck != PXPersistingCheck.Nothing)
                 {
                     flag = sender.GetValue(row, uniqueField) == null;
                     break;
                 }
             }
             if (flag)
                 return false;
         }
         return true;
     }
 
     private bool ValidateDuplicates(PXCache senderobject rowobject oldRow)
     {
         if (!this.IgnoreNulls || this.CheckDefaults(sender, row) && sender.GetValue(row, this._FieldOrdinal) != null)
         {
             PXView view = this._View;
             object[] currents = new object[1] { row };
             object[] objArray = Array.Empty<object>();
             foreach (object obj in view.SelectMultiBound(currents, objArray))
             {
                 object sibling = obj;
                 Lazy<stringlazy = Lazy.By<string>((Func<string>)(() => this.PrepareMessage(sender, row, sibling)));
                 if (!sender.ObjectsEqual(sibling, row) || sibling != row && this.UniqueKeyIsPartOfPrimaryKey && sender.GetStatus(row) != PXEntryStatus.Inserted)
                 {
                     foreach (string uniqueField in this._UniqueFields)
                     {
                         if (oldRow == null || !PXCheckUniqueYZAttribute.CheckEquals(sender.GetValue(row, uniqueField), sender.GetValue(oldRow, uniqueField)))
                         {
                             PXFieldState valueExt = sender.GetValueExt(row, uniqueField) as PXFieldState;
                             sender.RaiseExceptionHandling(uniqueField, row, valueExt != null ? valueExt.Value : sender.GetValue(row, uniqueField), (Exception)new PXSetPropertyException(lazy.Value));
                         }
                     }
                     return this.IgnoreDuplicatesOnCopyPaste && sender.Graph.IsCopyPasteContext;
                 }
             }
         }
         return true;
     }
 
     protected virtual string PrepareMessage(PXCache cacheobject currentRowobject duplicateRow) => this.ErrorMessage;
 }

 During debugging, I payed special attention to this part:

and I found out, that

view.SelectMultiBound(currents, objArray)

  returns zero elements. It lead me to a conclusion that I have some mistakes in SQL view. After comparing two FBQL views, the one which worked fine with the one, which didn't throw exception, I was able to figure out, that I need to modify FBQL view and DAC classes attributes related to PXDefault.

After that I was able to re-use PXCheckUnique attribute.

Summary

If you will have issues with PXCheckUnique attribute, feel free to go with dnSpy into Acumatica internals, and get it into your code, and thoughtfully debug the code. I bet you'll find some issues with your defaulting attributes or with your DAC class.

 

Add comment

Loading