MVVM Conventions for UE5.7

This document describes ViewModel patterns and conventions used in our Unreal Engine 5.7 project with the MVVM plugin.

A ViewModel represents a domain entity — a meaningful concept from the game's domain model (Item, Character Health, Inventory Cell, Drag Operation) — expressed as a set of bindable properties for UI.

Key principles

  1. VM = Entity, not Widget. A ViewModel describes a domain object, not a specific UI element. One VM instance can be bound to multiple widgets simultaneously through Resolvers.

  2. VM Composition. ViewModels can contain other ViewModels as nested sub-entities. Example: UXtrAttributesViewModel contains PrimaryVM, VitalityVM, CombatVM — each representing a sub-entity of the character's attribute system.

  3. VM Ownership. A ViewModel is owned by its Model (Component, Subsystem, or parent VM). The Model populates data through protected setters; UI only reads data and sends Requests.

  4. VM Sharing. Multiple widgets can bind to the same VM instance. A Resolver returns the VM from the owning Subsystem/Component to any widget that requests it.

  5. One entity — one VM. If a domain concept has its own identity and can appear in multiple UI contexts, it deserves its own ViewModel class. Do not duplicate entity fields across unrelated ViewModels — compose them instead.

Overview

ViewModel is a contract between Model (Component/Subsystem) and UI (Widget). Model updates data, UI reads it and sends Requests.

Property Types

Type
Access
FieldNotify
Setter in UPROPERTY
Purpose

Data

BlueprintReadOnly

Yes

No

Data for UI display

Request

BlueprintReadWrite

Yes

Yes

Signals from UI to Model

Computed

— (UFUNCTION only)

Yes

Derived values

Broadcast-only

— (UFUNCTION only)

Yes

UI trigger without value

Boolean Getter Naming

Prefix
When to Use
Examples

Is*

Object state (what is it?)

IsEmpty(), IsDragging(), IsAlive()

Has*

Presence of something (does it have?)

HasTarget(), HasNotifications()

Can*

Ability to perform action (can it?)

CanInteract(), CanDrop()

Request fields always use Has*Request(): HasDropRequest(), HasCloseRequest().

Data Property

Data for display. UI only reads, Model only writes.

Rules:

  • BlueprintReadOnly — UI cannot write

  • Getter without Setter in UPROPERTY — setter is not exported to Blueprint

  • Setter in protected section — accessible only to friend class (Model)

  • meta=(AllowPrivateAccess=true) — allows access to private field

Request Property

Signal from UI to Model. UI writes, Model reads and resets.

Rules:

  • BlueprintReadWrite — UI can write

  • Both Getter and Setter in UPROPERTY — both are exported

  • Setter is BlueprintCallable in public section

  • Category contains |Request suffix for clarity

Sentinel values for Request:

  • bool: false

  • int32: INDEX_NONE (-1)

  • FVector2D: constant NoRequest = FVector2D(-1, -1)

Request Pattern (UI to Model)

UI never calls Model methods directly. Instead, the Request-field pattern is used:

  1. ViewModel contains a Request-field with a sentinel value

  2. UI (Blueprint) changes the Request-field value when user performs an action

  3. Model (Component/Subsystem) subscribes to field changes via AddFieldValueChangedDelegate

  4. Model upon notification:

    • Checks that value != sentinel (ignores reset)

    • Resets field back to sentinel immediately

    • Validates the request

    • Executes logic

Important:

  • Request-field is a one-shot signal, not state

  • Model ignores sentinel to avoid infinite loop

  • Subscription via CreateUObject creates weak reference — explicit unsubscription not needed with shared lifetime

Computed Property

Derived value calculated from other properties.

Rules:

  • UFUNCTION only, no UPROPERTY

  • FieldNotify on function — UI can subscribe to changes

  • Model calls UE_MVVM_BROADCAST_FIELD_VALUE_CHANGED(GetHealthPercent) when dependent data changes

Cascade broadcasting — when one property affects multiple computed properties:

Broadcast-only Property

Trigger for UI when "something changed". Value doesn't matter, only the broadcast itself.

When to use:

  • UI needs to know "something updated" without checking each field

  • Optimization: single binding instead of many

Friend Class Pattern

ViewModel declares Model as friend for access to protected setters:

Complete ViewModel Example

Summary

Aspect
Data Property
Request Property
Computed Property

UPROPERTY

Yes

Yes

No

UFUNCTION getter

Yes (public)

Yes (public)

Yes (public, FieldNotify)

Setter location

protected

public

N/A

BlueprintReadOnly/Write

ReadOnly

ReadWrite

N/A

Setter in UPROPERTY

No

Yes

N/A

Who writes

Model only

UI only

N/A (derived)

Who reads

UI

Model

UI

Last updated