Unreal UMG MVVM

Unreal UMG MVVM

Article directory

  • Unreal UMG MVVM
    • background
    • M-VM-V
    • extension point
    • === Editortime ===
    • Viewmodels editor interface
    • View Bindings editor interface
    • Blueprint compilation related
    • === Runtime ===
    • Create ViewModel
    • ViewModel update

Background

  1. First read the documentation and quabqi’s UOD video sharing, which is currently the only information on the Internet.
  2. Take a look at the mature giant hard solution WPF MVVM
  3. The rest is just hacking the code

M – VM – V

  1. Model: original data
  2. ViewModel: data required by the view
  3. View: Control Blueprint WBP

Extension points

Three Extensions, note that MVVM is a plug-in, extending WBP in this way

// editortime
class MODELVIEWVIEWMODELBLUEPRINT_API UMVVMWidgetBlueprintExtension_View : public UWidgetBlueprintExtension
//runtime
class MODELVIEWVIEWMODEL_API UMVVMView : public UUserWidgetExtension
class MODELVIEWVIEWMODEL_API UMVVMViewClass : public UWidgetBlueprintGeneratedClassExtension

Add a new BP Compiler, MVVM automatically generates part of the BP code

FKismetCompilerContext::RegisterCompilerForBP(UMVVMViewModelBlueprint::StaticClass(), & amp;UMVVMViewModelBlueprint::GetCompilerForViewModelBlueprint);

=== Editortime ===

There are two editor interfaces

WidgetBlueprint.UMVVMBlueprintView, which is also hung through WBP Extension UMVVMWidgetBlueprintExtension_View

Two editors, one adds VM Class and one adds Binding, which are finally stored in UMVVMBlueprintView

//UMVVMBlueprintView
UPROPERTY(EditAnywhere, Category = "MVVM")
TArray<FMVVMBlueprintViewBinding> Bindings;

UPROPERTY(EditAnywhere, Category = "MVVM")
TArray<FMVVMBlueprintViewModelContext> AvailableViewModels;

Viewmodels editor interface

// Editor AddViewModel
if (UWidgetBlueprint* WidgetBlueprint = WidgetBlueprintEditor->GetWidgetBlueprintObj())
{<!-- -->
UMVVMEditorSubsystem* EditorSubsystem = GEditor->GetEditorSubsystem<UMVVMEditorSubsystem>();
check(EditorSubsystem);

UMVVMBlueprintView* CurrentBlueprintView = WeakBlueprintView.Get();
if (!CurrentBlueprintView)
{<!-- -->
CurrentBlueprintView = EditorSubsystem->RequestView(WidgetBlueprint);
WeakBlueprintView = CurrentBlueprintView;
ViewModelsUpdatedHandle = CurrentBlueprintView->OnViewModelsUpdated.AddSP(this, & amp;SMVVMViewModelPanel::HandleViewModelsUpdated);
}

EditorSubsystem->AddViewModel(WidgetBlueprint, SelectedClass);
}

View Bindings editor interface

Blueprint compilation related

UMVVMBlueprintView edit data is compiled into UMVVMViewClass for runtime use

// FMVVMViewBlueprintCompiler::PreCompileBindingSources
UMVVMViewClass* ViewExtension = NewObject<UMVVMViewClass>(Class);
CurrentCompilerContext->Compile(Class, BlueprintView, ViewExtension);
CurrentCompilerContext->AddExtension(Class, ViewExtension);

// FMVVMViewBlueprintCompiler::Compile
CompileSourceCreators(CompileResult.GetValue(), Class, BlueprintView, ViewExtension);
CompileBindings(CompileResult.GetValue(), Class, BlueprintView, ViewExtension);
ViewExtension->BindingLibrary = MoveTemp(CompileResult.GetValue().Library);

=== Runtime ===

Create ViewModel

Four modes, 134 are automatically created and associated, taking the default Create Instance as an example

View model creation type Description
Automatic creation The widget automatically creates its own Viewmodel instance.
Manual creation The Viewmodel is null when the widget is initialized, you need to manually create an instance and assign it.
Global view model collection refers to a globally available view model that can be used by any widget in the project. A global view model identifier is required.
Property Path On initialization, execute a function to find the Viewmodel. Viewmodel property paths use member names separated by periods.

Widget::Init -> Init Extensions -> MVVMViewClass -> MVVMView

UMVVMViewClass One copy for each WBP Class, which is the binding data saved during editing
MVVMView One copy per WBP Instance
UMVVMViewClass pulls up the MVVMView of each WBP Instance

void UMVVMViewClass::Initialize(UUserWidget* UserWidget)
{<!-- -->
ensure(UserWidget->GetExtension<UMVVMView>() == nullptr);
UMVVMView* View = UserWidget->AddExtension<UMVVMView>();
if (ensure(View))
{<!-- -->
if (!bLoaded)
{<!-- -->
BindingLibrary.Load();
bLoaded = true;
}

View->ConstructView(this);
}
}

CreateInstance, as mentioned before, will automatically create and associate ViewModel instances and WBP instances

/**
* Instance UMVVMClassExtension_View for the UUserWdiget
*/
UCLASS(Transient, DisplayName="MVVM View")
class MODELVIEWVIEWMODEL_API UMVVMView : public UUserWidgetExtension

void UMVVMView::Construct()
{<!-- -->
   //Init ViewModel instances
   for (const FMVVMViewClass_SourceCreator & amp; Item : ClassExtension->GetViewModelCreators())
   {<!-- -->
      Item.CreateInstance(ClassExtension, this, GetUserWidget());
   }
   ...
}

UObject* FMVVMViewClass_SourceCreator::CreateInstance(const UMVVMViewClass* InViewClass, UMVVMView* InView, UUserWidget* InUserWidget) const
{<!-- -->
UObject* Result = nullptr;

FObjectPropertyBase* FoundObjectProperty = FindFProperty<FObjectPropertyBase>(InUserWidget->GetClass(), PropertyName);
if (ensureAlwaysMsgf(FoundObjectProperty, TEXT("The compiler should have added the property")))
{<!-- -->
auto AssignProperty = [FoundObjectProperty, InUserWidget](UObject* NewObject)
{<!-- -->
check(NewObject);
if (ensure(NewObject->GetClass()->IsChildOf(FoundObjectProperty->PropertyClass)))
{<!-- -->
FoundObjectProperty->SetObjectPropertyValue_InContainer(InUserWidget, NewObject);
}
};

if(bCreateInstance)
{<!-- -->
//...
}
else if (GlobalViewModelInstance.IsValid())
{<!-- -->
//...
}
else if (FieldPath.IsValid())
{<!-- -->
//...
}
}

return Result;
}

ViewModel update

C++ wrapper SetXXX function to call UE_MVVM_SET_PROPERTY_VALUE macro

/** After a field value changed. Broadcast the event. */
#define UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(MemberName) \
BroadcastFieldValueChanged(ThisClass::FFieldNotificationClassDescriptor::MemberName)

/** If the property value changed then set the new value and notify. */
#define UE_MVVM_SET_PROPERTY_VALUE(MemberName, NewValue) \
SetPropertyValue(MemberName, NewValue, ThisClass::FFieldNotificationClassDescriptor::MemberName)

/** Set the new value and notify if the property value changed. */
template<typename T, typename U>
bool SetPropertyValue(T & amp; Value, const U & amp; NewValue, UE::FieldNotification::FFieldId FieldId)
{<!-- -->
if (Value == NewValue)
{<!-- -->
return false;
}

Value = NewValue;
BroadcastFieldValueChanged(FieldId);
return true;
}

Blueprint replaces SetProperty directive

DEFINE_FUNCTION(UMVVMViewModelBase::execK2_SetPropertyValue)
{<!-- -->
UMVVMViewModelBase* ViewModelContext = Cast<UMVVMViewModelBase>(Context);

bResult = TargetProperty->Identical(TargetValuePtr, SourceValuePtr);
if (!bResult)
{<!-- -->
// Set the value then notify that the value changed.
TargetProperty->SetValue_InContainer(ViewModelContext, SourceValuePtr);
ViewModelContext->BroadcastFieldValueChanged(FieldId);
}
}

In short, BroadcastFieldValueChanged is called, and ViewModel derives INotifyFieldValueChanged to monitor property changes.

But it actually encapsulates the Setter. Every time it changes, the Delegate is called to dispatch the event.

class MODELVIEWVIEWMODEL_API UMVVMViewModelBase : public UObject, public INotifyFieldValueChanged

virtual FDelegateHandle AddFieldValueChangedDelegate(UE::FieldNotification::FFieldId InFieldId, FFieldValueChangedDelegate InNewDelegate) override final;

void UMVVMViewModelBase::BroadcastFieldValueChanged(UE::FieldNotification::FFieldId InFieldId)
{<!-- -->
NotificationDelegates.BroadcastFieldValueChanged(this, InFieldId);
}

The Binding data edited by the blueprint will unbind the old one and bind the new one when WBP::SetXXXViewModel is called.

// Paste the entry function, the code will not be pasted
UMVVMView::SetViewModel
UMVVMView::EnableLibraryBinding