IPropertyHandle Validation in Detail Customisation

I have an array of elements each with an internal FInstancedStruct that contains per-element unique data. Imagine a user-defined collection of Events (On-Turn-On / On-Turn-Off / On-Take-Damage).

// Left out USTRUCT/GENERATEDBODY, basic wrapper format
TArray<FMyWrapper> Collection;

struct FMyWrapper
{
FInstancedStruct WrapperContents;
};

The issue: If the user clicks the reset-to-default button for the wrapper, the instanced struct is emptied / nulled. The array element then just becomes an empty wrapper.

In my detail customisation I hold a backup of the contents of the instanced struct on SetOnChildPropertyValuePreChange, and in SetOnChildPropertyValueChanged callback, check if the instanced struct is null and replace it with the backup. It’s a little janky but it prevents accidental voiding by the user.

This works great for INSTANCE properties - i.e. I add an element to the array on the level actor, and clicking reset-to-default on that new element, the guard detects the null and reverts it.

But it DOESN’T work for any values that are declared in the blueprint itself - ie. default values. Reset to default clears the instanced struct resulting in an empty wrapper element.

if (InstancedStructProperty.IsValid() && InstancedStructProperty->IsValidHandle())
{

TArray<void*> RawAfter;
InstancedStructProperty->AccessRawData(RawAfter);

if (RawAfter.Num() > 0)
{

// LOGGED HERE: Doesn't enter for BP default values. //

   const int32 Count = FMath::Min(Backups.Num(), RawAfter.Num());

   for (int32 i = 0; i < Count; ++i)
   {
      FInstancedStruct* IS = RawAfter[i] ? static_cast<FInstancedStruct*>(RawAfter[i]) : nullptr;
      if (!IS) continue;

      const bool bWasValid = Backups.IsValidIndex(i) ? Backups[i].IsValid() : false;
      const bool bIsNowValid = IS->IsValid();

      if (!bIsNowValid && bWasValid)
      {
         *IS = Backups[i];
         bAnyRestored = true;
      }
   }
}

For instance-only values, this works. For blueprint base values, it never enters if(RawAfter→Num() > 0)

It seems calling GetOuterObjects on a TSharedPtr IPropertyHandle belonging to a blueprint default object (in this case a custom component) returns 0 outers for that property.

TArray<UObject*> Owners;
ArrayHandle->GetOuterObjects(Owners);

However if I duplicate the blueprint default component on the level instance, this same code reports one outer (the duplicated component) and the validation code executes as expected.

This is the root of my issue. I’m not sure why property handles on blueprint default components don’t have an outer?

// Logged on MyControllerComponent (Added in the blueprint editor).
Begin. ArrayProperty=Collection, ElementProperty=Collection
Element struct: MyEventWrapper
ContentsProperty: MyEventContents
Owners.Num=0

// Logged on a duplicate of MyControllerComponent in the level instance of the actor.
Begin. ArrayProperty=Collection, ElementProperty=Collection
Element struct: MyEventWrapper
ContentsProperty: MyEventContents
Owners.Num=1
Owner[0]=MyControllerComponent /Game/Test/MAP_TestLevel01.MAP_TestLevel01:PersistentLevel.BP_CrashTestDummy_C_261.MyController1 ElemIndex=1 Type=MyEventOnTurnOn

So the Property Handle DOES have an outer when in the Blueprint Editor. Just not on the level instance for that blueprint default component’s property handles?

Trying a different approach.

In both cases (BP default component / instance-only component) InstancedStructProperty is valid (it is cached in the CustomizeHeader function). But in the BP default case, AccessRawData returns nothing.

void FMyCollectionElement_DC::OnChildPropertyValueChanged()
{
    if (bChurnGuard) return;
    bChurnGuard = true;
    
    bool bAnyRestored = false;
    if (InstancedStructProperty.IsValid() && InstancedStructProperty->IsValidHandle())
    {
       if (ContentValidCheck.Get() != EContentValidState::All)
       {
          TArray<void*> RawAfter;
          InstancedStructProperty->AccessRawData(RawAfter);

          if (RawAfter.Num() > 0)
          {
             const int32 Count = FMath::Min(Backups.Num(), RawAfter.Num());

             for (int32 BackupIndex = 0; BackupIndex < Count; ++BackupIndex)
             {
                FInstancedStruct* Contents = RawAfter[BackupIndex] ? static_cast<FInstancedStruct*>(RawAfter[BackupIndex]) : nullptr;
                if (!Contents) continue;
                
                *Contents = Backups[BackupIndex];
                bAnyRestored = true;
             }
          }
       }
    }
    
    if (bAnyRestored)
    {
       InstancedStructProperty->NotifyPostChange(EPropertyChangeType::ValueSet);
       InstancedStructProperty->NotifyFinishedChangingProperties();

       if (TSharedPtr<IPropertyUtilities> Utils = WeakUtils.Pin())
          Utils->RequestRefresh();
    }

    bChurnGuard = false;
}