MVVM Conversion Functions can crash during BP compilation

Hello! This is a crash we ran into! The following is our analysis of the problem and a code snippet to fix the issue:

Cause:

Even though MyWidget has a reference to MyBaseClass.MyBoolField through the MyDerivedClass viewmodel, MyWidget is not recompiled when MyBaseClass is recompiled, causing it to leave in stale field references in the blueprint code for MyWidget.

Recompiling MyBaseClass does cause MyDerivedClass to get “reinstanced” during which it goes through the functions of all it’s dependants (MyWidget) to attempt to update any changed variable references.

It is during this field replacement process the invalid field memory is accessed in the blueprint of MyWidget, because MyWidget was not compiled along with MyBaseClass and MyDerivedClass before “reinstancing”.

MyBaseClass does not show in the CachedDependencies list of MyWidget, which is a requirement for MyWidget to be recompiled when MyBaseClass is compiled. It isn’t there because the generation of the binding functions for MVVM happens at the very end of the blueprint compilation step, so whatever step that picks up blueprint references and puts it into CacheDependencies is not run after the generated blueprint functions are made. The MVVM module gets around this in UMVVMWidgetBlueprintExtension_View::HandleBeginCompilation by explicitly adding the blueprints of ViewModels to the CachedDependencies. It doesn’t add the blueprints of super classes though.

Workaround:

The workaround for this is to ensure the dependency to the other blueprints exist. This can be accomplished as simple as adding an accessor to the MyDerivedClassVM viewmodel property in the blueprint graph of MyWidget. This will cause the normal blueprint compiler process to pickup the dependency and add the classes to CachedDependencies.

Fix:

Expand UMVVMWidgetBlueprintExtension_View::HandleBeginCompilation by having it add blueprints of super classes of ViewModels to the CachedDependencies of the blueprint of MyWidget.

Real fix:

Ensure whatever process that normally picks up CachedDependencies from a blueprint can be run on the generated binding functions instead of explicitly adding the cached dependencies in HandleBeginCompilation.

This turns out to be caused by the fact the functions are generated, they never stay as “nodes” in the blueprint graphs, instead they’re directly converted into functions on the generated class.

By the time we’re generating the dependency graph between blueprints, the generated graphs are either not created yet or not available anymore.

Code:

void UMVVMWidgetBlueprintExtension_View::HandleBeginCompilation(FWidgetBlueprintCompilerContext& InCreationContext)
{
	VerifyWidgetExtensions();

	for (const FMVVMBlueprintViewModelContext& AvailableViewModel : BlueprintView->GetViewModels())
	{
#if WITH_EDITORONLY_DATA
		UWidgetBlueprint* WidgetBlueprint = GetWidgetBlueprint();
		UClass* ViewModelClass = AvailableViewModel.GetViewModelClass();
		if (WidgetBlueprint && ViewModelClass)
		{
			// BEGIN FIX:
			UClass* TargetClass = ViewModelClass;
			while (UBlueprint* ViewModelBP = Cast<UBlueprint>(TargetClass ? TargetClass->ClassGeneratedBy : nullptr))
			{
				ViewModelBP->CachedDependents.Add(WidgetBlueprint);
				WidgetBlueprint->CachedDependencies.Add(ViewModelBP);
				TargetClass = TargetClass->GetSuperClass();
			}
			// END FIX 
		} ....


[Attachment Removed]

Steps to Reproduce
In UE 5.7:

  • Create a class that can have field notify fields. (MyBaseClass)
  • Add a boolean field notify field (MyBoolField)
  • Create a derived class off MyBaseClass (MyDerivedClass)
  • Create a Widget Blueprint (MyWidget)
  • Add MyDerivedClass as a ViewModel to MyWidget
  • Add a text widget to MyWidget (MyTextWidget)
  • Add a binding to MyTextWidget.Visibility of type Conversion function ToVisibility(boolean)
  • In the boolean for the conversion function pick MyDerivedClassVM.MyBoolField
  • Save everything
  • Now compile MyBaseClass over and over, observe crash

[Attachment Removed]

Hi,

This sounds a bit similar to a case here where we needed to add our VM’s dependencies as dependencies to the Widget BP:

if (UBlueprint* ViewModelBP = Cast<UBlueprint>(ViewModelClass->ClassGeneratedBy))
{
	ViewModelBP->CachedDependents.Add(WidgetBlueprint);
	WidgetBlueprint->CachedDependencies.Add(ViewModelBP);
	WidgetBlueprint->CachedDependencies.Append(ViewModelBP->CachedDependencies);
	for (TWeakObjectPtr<UBlueprint> Dependency : ViewModelBP->CachedDependencies)
	{
		Dependency->CachedDependents.Add(WidgetBlueprint);
	}
}

This never made it into the engine, but it looks like you took a similar (but perhaps more robust) approach. If you were interested in sending in a pull request with these changes, I’ll get it in front of the UI team to see if we can get that integrated. I wonder if it’s necessary to iterate up the super chain, or if adding dependencies of the derived class will be sufficient since I’d expect those derived classes to list the super as a dependency already (though I’d need to dig into the compiler more to confirm).

[Attachment Removed]

Thanks for the response! I’ve created a pull request here: https://github.com/EpicGames/UnrealEngine/pull/14822

Cheers!

[Attachment Removed]

Great, thank you so much! I’ve tagged our internal tracking for that PR to link it to this ticket, we’ll follow up via the PR once we’ve had a chance to review the changes.

Best,

Cody

[Attachment Removed]