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
- First read the documentation and quabqi’s UOD video sharing, which is currently the only information on the Internet.
- Take a look at the mature giant hard solution WPF MVVM
- The rest is just hacking the code
M – VM – V
- Model: original data
- ViewModel: data required by the view
- 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