THE FIELD GUIDE

Architecture

MVVM, project structure, and runtime boundaries.

Forma uses a layered architecture with an incremental MVVM implementation in the Builder: a C# control model, a WebView2 rendering adapter, and a browser-based designer view. The boundaries below describe the existing code and the direction for future refactoring.

Existing layers

Linear layout containers share LinearLayout : LayoutContainer so the editing service and designer use the same child ordering rules. StackPanel/HStack/VStack, WrapPanel/FlowLayoutPanel, and CenterPanel keep children in managed flow; free positions are retained for ScrollablePanel. Navigation controls reuse ChoiceControl and the selection renderer, with application scripts handling navigation events.

LayerResponsibilityExamples
CoreControl state, validation, parent/child relationships, events, rendering contractsControl, NumericControl, IRenderer, IBridge
WebView2Translate model updates into browser messages and browser interactions into model events; render controls in the DOMWebView2Renderer, WebView2Bridge, forma.js
Builder view model and editing serviceForm, selection, appearance, preview state, project identity, dirty/title notifications, document creation/application, editing rules, and history commandsBuilderViewModel, DesignerEditingService
Builder view and servicesNative window integration, bridge dispatch, rendering, inspector descriptors, and project persistenceBuilderWindow, InspectorCatalog, ProjectFile
Designer browser UIToolbox, inspector editors, drag/resize gestures, alignment guides, and preview interactiondesigner.js, generated Tailwind styles

Core must remain independent of WinForms, WebView2, DOM, CSS, and native file dialogs. Renderer interfaces provide a boundary for a different rendering implementation. Browser gestures send commands to the C# host, where authoritative control state and validation live; the browser keeps transient interaction state.

DesignerWeb/reactivity.js owns the script-side observer scopes for refs, shallow reactive state, computed values, effects, and control bindings. component-customization.js connects those scopes to the runtime get/set bridge and disposes them with their component behavior. Binding reads reflect live DOM input and projected runtime state; writes remain C#-validated bridge commands. This is a browser runtime service supporting the View, separate from the Builder's document ViewModel and the Core model. Component-local script state is not saved into the design or shared implicitly across behaviors.

The control tree follows the Composite pattern: controls contain child controls. Property notifications and control events follow the Observer pattern. The renderer and bridge form an adapter between C# models and the browser. These describe specific parts of the code, not a claim that the whole application already follows one formal pattern.

Current folder structure and MVVM roles

This is the current source layout, with representative control files shown. Build output, dependencies, and individual icon files are omitted.

Forma/
├── src/
│   ├── Forma.Core/                         Model and framework contracts
│   │   ├── Form.cs                         Root of the control tree
│   │   ├── Controls/                       One control/supporting type per file
│   │   │   ├── Control.cs                  Shared state, children, notifications
│   │   │   ├── NumericControl.cs           Shared numeric behavior
│   │   │   ├── Button.cs
│   │   │   └── TextBox.cs                  Other controls live alongside these
│   │   ├── Events/                         Framework input event arguments
│   │   └── Rendering/                      Renderer and bridge interfaces
│   ├── Forma.WebView2/                     Rendering infrastructure
│   │   ├── WebView2Renderer.cs             Model-to-browser rendering adapter
│   │   ├── WebView2Bridge.cs               Browser message transport
│   │   └── Web/                            Framework runtime view assets
│   │       ├── index.html
│   │       ├── scripts/forma.js
│   │       └── css/forma.css
│   ├── Forma.Builder/                      Designer application
│   │   ├── Program.cs                     Application entry point
│   │   ├── Models/
│   │   │   ├── Appearance.cs              Designer-specific Model data
│   │   │   ├── ComponentCustomization.cs  Portable CSS, behavior and custom values
│   │   │   └── DesignerEditResult.cs      Edit feedback for the View
│   │   ├── ViewModels/
│   │   │   └── BuilderViewModel.cs        Designer state and edit/history commands
│   │   ├── Views/
│   │   │   ├── BuilderWindow.cs           Native View and bridge integration
│   │   │   ├── PreviewWindow.cs           Runtime window and native dialogs
│   │   │   └── ComponentEditorView.cs   Built-in/external source editor View
│   │   ├── Services/
│   │   │   ├── CoalescedRefresh.cs       Merge same-turn view state requests
│   │   │   ├── DesignerEditingService.cs  Add/delete/move/resize/property/layer rules
│   │   │   ├── DesignHistory.cs           Undo/redo support service
│   │   │   ├── ProjectFile.cs             Persistence service and file DTOs
│   │   │   ├── ComponentEditorService.cs  Boilerplate and source file round trips
│   │   │   ├── ComponentSaveSession.cs    Save commits and duplicate suppression
│   │   │   ├── PreviewSession.cs          Isolated runtime tree and script setters
│   │   │   ├── PreviewProject.cs          Multi-window project snapshot/shared values
│   │   │   ├── ControlCatalog.cs          Builder control definitions and defaults
│   │   │   ├── DesignerLayout.cs          Geometry and layout arrangement
│   │   │   ├── RecoveryStore.cs           Session autosaves and recovery records
│   │   │   └── InspectorCatalog.cs        Inspector property descriptors
│   │   ├── DesignerWeb/                   Browser portion of the View
│   │   │   ├── index.html
│   │   │   ├── designer.js
│   │   │   ├── component-customization.js Scoped styles and preview behavior adapter
│   │   │   ├── preview.html               Runtime-only browser View
│   │   │   ├── designer.css               Generated stylesheet
│   │   │   └── icons/                     Local SVG assets
│   │   └── Frontend/                      View asset build tooling
│   │       ├── input.css                  Tailwind source
│   │       ├── controls.css               Shared control presentation
│   │       ├── runtime.css                Runtime stylesheet entry
│   │       ├── build.cjs
│   │       └── package.json
│   └── Forma.Demo/                         Example consumer of the framework
├── tests/
│   ├── Forma.Tests/                       Model, ViewModel, service/renderer tests
│   └── WebRuntime/                        Browser runtime and designer DOM tests
└── docs/                                  Architecture, behavior, and roadmap

Model: Forma.Core contains reusable control state and rules. Builder-specific data such as Appearance stays in Forma.Builder, because the framework does not need to know how the designer stores styling. Rendering/ contains infrastructure contracts, so not every file in Core is an MVVM Model.

ViewModel: BuilderViewModel.cs exposes the designer's state, notifications, and history command methods. It coordinates model data without referencing the native window or browser. Future inspector and toolbox view models belong to the Builder application too.

View: BuilderWindow.cs and DesignerWeb/ together present the designer. The window hosts WebView2 and native dialogs; browser assets display controls and capture gestures. Forma.WebView2/Web/ supplies reusable control rendering beneath that designer View. Frontend/ builds the View's assets and contains no application state.

Services and infrastructure: DesignerEditingService, DesignHistory, ProjectFile, InspectorCatalog, and the WebView2 adapter support these roles. The editing service owns control creation, bounds, property validation, deletion, and layer ordering; the view model wraps editing batches in history tracking. MVVM does not require every helper or project to be classified as Model, View, or ViewModel.

How an interaction crosses these folders

flowchart LR
    Browser[View: DesignerWeb] -->|gesture or command| Window[View: BuilderWindow]
    Window -->|command methods| VM[ViewModel: BuilderViewModel]
    VM -->|editing rules| Editing[Service: DesignerEditingService]
    Editing -->|read or change state| Model[Model: Core controls and Appearance]
    VM -->|PropertyChanged| Window
    Window -->|designer state through bridge| Browser
    Model -->|control notifications| Renderer[Infrastructure: Forma.WebView2]
    Renderer -->|render messages| Browser

For example, moving a button travels from designer.js through BuilderWindow to BuilderViewModel.ExecuteEdit(). The view model captures history, calls the editing service to validate and clamp the position, and records the result. The window then projects the updated state to the browser. Preview and lock guards are enforced by the service as well as by the browser.

Dragging a control into a panel changes its parent in the control tree, rather than only placing it visually over the panel. The browser hit-tests containers beneath the dragged control and sends the destination parent ID with coordinates relative to its content area. The editing service validates the destination, prevents cycles, and preserves the control and its descendants. Children move with their panel because their coordinates are relative to that parent. Dragging back onto the form removes the control from the panel. Parent changes are saved in project files and recorded as one undoable move.

Undo travels through the same View to BuilderViewModel.Undo(). The view model supplies a history snapshot; the window currently restores it through ProjectFile, applies the document to the view model, and updates the renderer and browser state. This explicit bridge serves the binding role that XAML would normally provide.

Builder folder conventions

The Builder now uses these folders. Place future extractions alongside the existing responsibilities:

Folder under Forma.Builder/Files or responsibilities
Models/Appearance.cs and other designer data
ViewModels/BuilderViewModel.cs, future inspector/toolbox view models
Views/BuilderWindow.cs and native presentation adapters
Services/History, persistence, inspector metadata, and application command services
DesignerWeb/Existing browser View assets
Frontend/Existing asset build tooling

Program.cs stays at the project root as the composition entry point. Public C# namespaces remain Forma.Builder; source links in the test project follow the folder paths. Dependencies matter more than folder names: view models and editing services must stay independent of Views, while Views may depend on view models.

Preview uses Services/PreviewSession.cs to clone the form and appearance into an independent runtime tree. Views/PreviewWindow.cs hosts that tree with the same WebView2 renderer and shared appearance presentation used by the designer. DesignerWeb/preview.html contains only the running form, without editor chrome. Runtime edits and timers belong to the preview session; closing the native window disposes them. The Builder remains in Design mode. This is an in-process runtime preview; generated C# projects and distributable executables are future export work. The native Preview window supports resize, maximize and restore. Its form surface fills larger viewports; controls retain their designed coordinates. Smaller viewports scroll rather than shrink the designed canvas.

Control source organization

Each public control, shared base class, event argument type, and document record has its own named file in src/Forma.Core/Controls. Namespaces and public APIs remain unchanged. A tiny derived control can have a tiny file; this gives it a clear home when its behavior grows.

Keep state, validation, and model events with the control. Share behavior through focused bases such as NumericControl and ChoiceControl, or small helpers when duplication warrants them. Do not place DOM rendering, inspector editors, native dialogs, or persistence logic inside controls. Category folders can be introduced later without changing public namespaces.

INonvisualControl marks controls that belong in the component tray and do not participate in form geometry. It allows a context menu to inherit shared command behavior while remaining nonvisual. Component implements the same marker for timers, workers and dialogs. Temporary context-menu popups and modal dialog elements belong to the browser View; their model configuration remains in Core.

Splitting files does not by itself separate responsibilities. The runtime and Builder still contain large switches and orchestration methods; those require their own incremental refactors.

MVVM in the Builder

The Model is the Core control tree and appearance data. BuilderViewModel implements INotifyPropertyChanged and owns designer session state. BuilderWindow and the browser workspace are the View. The window binds its title to view-model notifications; browser state is explicitly projected through the existing bridge rather than XAML bindings. History command methods are invoked by bridge dispatch, so an ICommand wrapper is not required for this view technology.

The view model has no WinForms, WebView2, DOM, or dialog dependency and can be tested without opening a window. Native dialogs and renderer lifecycle belong to the view/infrastructure. The bridge refreshes computed dirty state after each mutation batch, including changes to appearance metadata.

Designer editing now goes through BuilderViewModel.ExecuteEdit() and DesignerEditingService, including add, delete, move, resize, property changes, image source changes, and layer ordering. The detailed inspector state projection and native save/open orchestration still live in BuilderWindow; they can move to focused view-model/application services as the migration continues. Core and renderer layers do not need to become view models.

Next architectural refactors

  1. Build on the extracted BuilderViewModel designer session, keeping it distinct from serialized project DTOs. Extract an inspector view model when its editing responsibilities grow.
  2. Extract the inspector state projection and project lifecycle orchestration. Keep native file dialogs and WebView2 lifecycle in the View, while application services coordinate document operations. Editing commands already use the shared validation/history boundary.
  3. Extend ControlCatalog when adding Builder control kinds. Construction, defaults, containment capabilities and persistence type lookup now share that registry; the browser toolbox/renderers and inspector metadata still need appropriate entries. Keep project loading restricted to the explicit supported types.
  4. Separate browser rendering implementations by control family or control as they grow, with a small dispatcher and shared DOM helpers. Preserve the current browser message contract and tests during this change.

Use these extractions when expanding the roadmap rather than doing a wholesale framework rewrite. Keep browser bindings explicit and view models independent of their rendering technology.

Recent structural changes

ControlCatalog centralizes the Builder's control kind/type mapping, construction defaults, appearance defaults, and containment flags. Core Control exposes GetRenderState with a virtual WriteRenderState extension point. Individual controls add their own state and process events through OnRuntimeEvent; RuntimeEvent provides typed JSON accessors. The WebView2 renderer dispatches through those contracts instead of keeping a growing switch for every control.

DesignerEditingService.Properties.cs and .Selection.cs separate property and selection edits, while DesignerLayout owns geometry and arrangement. BuilderWindow is split into partial files for messages, state, editors, Explorer, projects, recovery, and startup. These partial files remain one View class; the split improves organization without creating new MVVM layers.

PreviewProject snapshots the complete project for one run and owns the shared serializable values between its windows. PreviewSession owns an individual form's cloned controls; PreviewWindow coordinates native windows and result messages. main.js, imported module instances, providers, and refs execute within each window, while JSON-compatible forma.shared assignments are broadcast through the project bridge.

Frontend/controls.css supplies common control appearance used by the designer and runtime stylesheet builds. Frontend/input.css handles the Builder workspace, and Frontend/runtime.css handles the framework runtime. Generated CSS is build output. FormaPaths centralizes the configurable local data directory used by recent projects and recovery.

Persistence and verification

Project files contain versioned data, not live renderer objects, native resources, or .NET event-handler delegates. Optional component customization stores portable CSS/JavaScript source strings and custom JSON values in Appearance. Styles apply in Design and Preview; JavaScript is initialized only in the runtime window. ComponentEditorService creates editing files, while the native editor View opens installed editors. Save and Ctrl+S apply immediately; a debounced file watcher applies external saves while the editor is open. ComponentSaveSession suppresses duplicate notifications and retains the last successful commit after validation or application failures. BuilderViewModel.ExecuteEdit("customize") validates and commits each changed save as one undoable operation. The browser behavior adapter sends typed property changes to PreviewSession, which owns the runtime copy. Core control classes do not depend on the source editor or JavaScript. ProjectFile validates and restores models; history uses document snapshots. Preserve existing .forma compatibility when reorganizing code.

Use Core tests for control invariants and events, renderer tests for message contracts, project/history tests for restoration and undo/redo, and DOM tests for browser behavior. A source-only split should leave existing tests and public declarations unchanged.

Modern controls and MVVM

Each modern control has its own model file under src/Forma.Core/Controls. Card extends LayoutContainer for ownership; Avatar extends Image for image selection/persistence; Toast extends Component for its nonvisual lifecycle. DesignerEditingService and InspectorCatalog implement edit validation and property metadata. BuilderViewModel owns the design and history; PreviewSession owns an independent runtime copy. WebView2Renderer translates model changes/events to the browser. Web/scripts/modern-controls.js is View code for rendering, positioning, animation and notification/overlay cleanup. Preview scripts issue commands through the bridge; PreviewWindow dispatches Toast commands and PreviewSession changes Active on Spinner/LoadingOverlay. DOM elements never become the saved model.

Docked workspace and code assistance

Views/ComponentEditorView.cs is a native UserControl hosting the code WebView. BuilderWindow owns its lifecycle and connects saves to BuilderViewModel commands. DesignerWeb/workspace.js is View code: the activity rail, property search, divider and dock rectangle. It sends bounds after layout; the native editor initializes once those bounds are available. DesignerWeb stores no committed source state.

Frontend/code-editor.mjs owns CodeMirror views, format/save commands and bridge messages. code-assistance.mjs supplies syntax diagnostics and completions from current component names and the runtime API contract. Sources remain ComponentCustomization model data; ComponentSaveSession and ComponentEditorService still validate, save and watch external files. Editor diagnostics are authoring feedback, not an execution engine or a generated C# application.

The Events inspector is View code in Frontend/event-inspector.mjs. Frontend/event-handlers.mjs parses saved/draft registrations without executing them and generates static handler methods in a component's event.js module. script.js imports and subscribes those methods. Both sources are Model data in ComponentCustomization and pass through the existing save/history pipeline. The Preview module loader gives event modules their owning component context while keeping project provider modules shared within a session.

The runtime event wrapper in component-customization.js preserves browser events and adds identity, scoped component handles, input metadata and PropertyChanged. Handles use the same runtime property API and own their subscriptions through the calling script's cleanup scope. PreviewSession publishes accepted write origins with coalesced state; JS deduplicates observed property values against live input and confirmed snapshots. Portable event logging emits descriptors, not DOM handles. C# handler compilation and transaction tracing are separate future features. See the event contract.

Responsive and section containers

Controls/ResponsiveLayout.cs is the shared Model for AppShell and ResponsivePanel, with a validated Breakpoint. Sidebar reuses LinearLayout; Accordion extends TabControl with Expanded and section child slots. Editing, Undo/Redo, serialization and PreviewSession remain the ViewModel boundary. The browser View uses one ResizeObserver per responsive container, lays out children using flex, and disconnects observers when a subtree is removed. Accordion renders native section buttons and sends selection/expanded commands through the bridge. Browser geometry and transient selection styling do not become additional persisted models.

Dock and Anchor belong to the Builder Appearance model. LayoutGeometry holds pure docking/anchoring calculations; DesignerEditingService applies them within the current edit before history capture. Parent resizing carries child anchors through nested containers. layout-properties.js is the browser View counterpart: it observes free content hosts, applies runtime geometry, and releases observers on removal/reset. It does not replace stack/table/AppShell layout rules or store DOM coordinates as project data. Script edits change PreviewSession appearance, then the refreshed runtime snapshot updates the View.

RuntimePropertyService extends the typed Preview API using the explicit DesignerWeb/runtime-script-properties.json catalog. The same catalog is embedded in Builder/tests and imported by code-assistance.mjs. InspectorCatalog supplies type, range and option validation; only registered members are reflected. Runtime snapshots advertise available extension fields, and the browser getter reads their projected values, cloning structured arrays before returning them. Core setters still validate TreeView, PropertyGrid and rich document structures. PreviewSession adjusts nested anchors when scripts change container dimensions.

Project global source is held in the root Appearance.GlobalScript model and validated by the global-script editing command. Existing snapshot/restore paths therefore preserve it in Save/Open and history. Preview copies the source into its own runtime snapshot. The editor uses script.js and main.js names; legacy Behavior serialization and behavior.js fallback remain compatible. component-customization.js initializes one shared reactive scope and module registry before component scripts, using provide/use and shared access. Consumer effects are still owned by their component scopes; the project scope is disposed after consumers on Preview reset/close. Runtime values are not saved to disk.

JavaScript authoring uses a bundled TypeScript language service in Frontend/javascript-service.mjs, hosted by javascript-worker.mjs. The worker has an in-memory filesystem containing standard library declarations, generated Forma declarations, the component draft and the saved global script. Static exports from forma.provide supply shared-module shapes; no user code executes for inference. javascript-completion.mjs correlates worker requests and cleans up on tab deactivation. CodeMirror shows completions, type/documentation hover and signature help. The editor uses a WebView2 virtual host so its worker and assets share a local HTTPS origin; the main designer/runtime bridge is unchanged.

BuilderViewModel now owns a project-level list of forms and appearance entries, with Form identifying the active designer form. AddForm records a project history edit; SelectForm changes the workspace selection without changing the saved design. ProjectFile supports legacy single-root version 1 and multi-form version 2, validating IDs and project limits across all trees. History snapshots carry the active form ID separately from persisted project content. PreviewSession clones only the selected form and injects the project global script. The native view changes the rendered form while retaining other models and open editor drafts.

ProjectExplorerService projects the in-memory project into a lightweight logical tree of all forms, nested controls, source documents and image references. The browser's solution-explorer.js filters and navigates that tree while preserving expansion and focus. Stable IDs route selection and source commands to the owning form in BuilderWindow. Explorer navigation never reads arbitrary folders or changes the saved design. Image contents and source bodies are excluded from the tree payload; unchanged structural state reuses its DOM nodes.

Custom project resources use ProjectEntry records persisted by project format version 3. ProjectEntryService validates names, sibling uniqueness, content limits and parent relationships. BuilderViewModel wraps edits in project history. ComponentEditorView's single-file mode reuses the docked editor for project JS, CSS and arbitrary JSON while keeping component customization's JSON-object contract. Explorer menu commands resolve stable project IDs in the native host; file names and logical folder paths never grant arbitrary filesystem access.

Project JavaScript modules

main.js is the startup ViewModel script shared by the project's forms. The legacy Appearance.GlobalScript storage key stays compatible with existing projects. ProjectEntryService maps logical file/folder IDs into portable module paths; PreviewSession snapshots paths and source content so running code cannot edit the designer or be changed by a later save. The browser module runtime transforms JavaScript imports/exports with locally bundled Sucrase and resolves relative JavaScript/JSON imports from the project. Each Preview has its own module cache; imported providers belong to the startup reactive scope and are cleaned up with that scope. Component scripts keep their own event/cleanup scope. The JavaScript language-service worker receives the same file paths and source snapshots to infer imported exports without executing user code.

Search components, properties, and guides.

↑↓ navigate Enter open Esc close