Parameter Form Lifecycle Management

Complete lifecycle of parameter forms from creation to context synchronization.

Status: STABLE (describes main branch implementation) Module: pyqt_reactive.forms.parameter_form_manager

Note

This document describes the main branch monolithic implementation. For the refactored service-oriented architecture currently in development, see Parameter Form Service Architecture.

Overview

Parameter forms must maintain consistency between widget state, internal parameters, and thread-local context. Traditional forms lose this synchronization during operations like reset, causing placeholder bugs. The lifecycle management system ensures all three states remain synchronized.

reset_parameter() orchestrates the complete reset lifecycle. It first determines the reset value (None for lazy configs), updates the internal parameter dictionary, updates the thread-local context to match, then updates the widget display and applies placeholder logic. This four-step process ensures that form state, context state, and widget display all stay in sync.

This prevents the reset placeholder bug where forms show stale values instead of current defaults.

Sequence Number Tracking

Each ParameterFormManager instance receives a unique sequence number (_pfm_seq) for debugging and tracking:

global _PFM_SEQ = 0

class ParameterFormManager(QWidget):
    def __init__(self, ...):
        global _PFM_SEQ
        _PFM_SEQ += 1
        self._pfm_seq = _PFM_SEQ

This sequence number appears in debug logs to trace form creation and widget operations:

logger.debug(
    "[PFM_INIT] seq=%s field_id=%s target=%s is_nested=%s parent_cls=%s",
    self._pfm_seq,
    self.field_id,
    self._target_type_name,
    self._parent_manager is not None,
    type(self._parent_manager).__name__ if self._parent_manager else None,
)

Nested Form Manager Tracking

The system tracks parent-child relationships between nested form managers:

class ParameterFormManager(QWidget):
    def _on_nested_manager_complete(self, nested_manager):
        """Called when nested form manager completes widget creation."""
        logger.debug(
            "[NESTED_COMPLETE] nested_field_id=%s nested_seq=%s root_field_id=%s root_seq=%s",
            nested_manager.field_id,
            getattr(nested_manager, '_pfm_seq', None),
            self.field_id,
            getattr(self, '_pfm_seq', None),
        )

This enables debugging of complex form hierarchies with multiple nesting levels.

Widget State Management

The form manager coordinates widget updates with context behavior application.

Value Update Coordination

update_widget_value() acts as the central coordinator for widget updates. It first blocks signals to prevent infinite loops, updates the widget’s displayed value using type-specific dispatch, then applies context behavior (like placeholder text for None values). This ensures widgets show the right value without triggering cascading updates.

Context Behavior Application

_apply_context_behavior() decides whether to show placeholder text. If the value is None and we’re in a lazy dataclass context, it calls the placeholder resolution system. If the value is not None, it clears any existing placeholder state. This creates the dynamic “Pipeline default: X” behavior.

Rendering Optimizations

Several recent improvements keep typing responsive even with large forms:

  • _store_parameter_value() mirrors each edit into an in-memory cache so get_current_values() no longer rereads every widget.

  • _placeholder_candidates tracks only the parameters that currently resolve to None. Placeholder refreshes iterate over this set instead of the entire form.

  • Each widget stores a placeholder_signature (see pyqt_reactive.forms.widget_strategies) so placeholders that have not changed are skipped entirely, avoiding redundant repaints.

Parameter Change Propagation

Changes flow from widgets through internal state to context synchronization.

_emit_parameter_change() handles the flow when users change widget values. It converts the raw widget value to the correct type, updates the internal parameter dictionary, then emits a signal so other components can react. This is the normal path for user edits (as opposed to programmatic updates like reset).

Thread-Local Context Synchronization

Critical synchronization patterns ensure context reflects current form state.

Reset Context Update Pattern

reset_parameter() updates thread-local context during reset using replace() to prevent placeholder bugs.

Visual State Synchronization

Form managers synchronize visual indicators (* and _) with field state changes:

Label Styling: _update_label_styling() applies * (dirty) and _ (differs from signature) indicators to field labels.

Reset Button Styling: _update_reset_button_styling() applies the same indicators to reset buttons, showing the target field’s state.

Provenance Button Visibility: _update_provenance_button_visibility() shows/hides the ^ provenance navigation button based on whether the field inherits from an ancestor.

These updates are triggered: - During individual field reset (reset_parameter()) - After batch reset completes (reset_all_parameters()) - Via FieldChangeDispatcher for regular edits

See Field Styling and Visual Indicators Architecture for complete visual indicator semantics.

UI Component Lifecycle Patterns

Different UI components have different form lifecycle requirements.

Step Editor Lifecycle

Step editors show step configurations with isolated context. Forms are created with custom context providers that resolve against their parent pipeline configuration. Reset operations update the step-specific context without affecting other UI components.

Pipeline Editor Lifecycle

Pipeline editors (plate manager style) handle pipeline-level configuration editing. Forms use standard thread-local context resolution and coordinate with the plate manager’s save/load operations.

Pipeline Config Lifecycle

Pipeline config editing (accessed from plate manager) creates forms that resolve against the current pipeline’s thread-local context. Save operations update both the pipeline configuration and the thread-local context to maintain consistency.

Global Config Lifecycle

Global config editing (accessed from main window) creates forms that show static defaults. Reset operations restore base class default values since there’s no higher-level context to resolve against.

Cross-Window Placeholder Updates

When multiple configuration dialogs are open simultaneously, they share live values for placeholder resolution. This enables real-time preview of configuration changes across windows.

Live Context Collection

_collect_live_context_from_other_windows() gathers current user-modified values from all active form managers. When a user types in one window, other windows immediately see the updated value in their placeholders. This creates a live preview system where configuration changes are visible before saving.

Live Context Snapshots

LiveContextSnapshot wraps the collected values together with a monotonically increasing token. As long as the token remains unchanged, _build_context_stack() reuses cached GlobalPipelineConfig and PipelineConfig overlays instead of rebuilding them on every keystroke.

Async Placeholder Resolution

Once the initial load completes, _schedule_async_placeholder_refresh() offloads placeholder work to _PlaceholderRefreshTask. The worker receives the parameter snapshot, the _placeholder_candidates list, and the current LiveContextSnapshot, resolves placeholders off the UI thread, then emits the results back to the main thread for application. This keeps the UI responsive even when dozens of placeholders participate.

Active Manager Registry

_active_form_managers maintains a class-level list of all active form manager instances. When a form manager is created, it registers itself in this list. When a dialog closes, it must unregister to prevent ghost references that cause infinite refresh loops.

Signal-Based Synchronization

Form managers emit context_value_changed and context_refreshed signals when values change. Other active managers listen to these signals and refresh their placeholders accordingly. This creates a reactive system where all windows stay synchronized.

Cross-Window Debounce

_schedule_cross_window_refresh() debounces placeholder refreshes with :pyattr:`~pyqt_reactive.forms.parameter_form_manager.ParameterFormManager.CROSS_WINDOW_REFRESH_DELAY_MS` (currently 60 ms). Multiple rapid edits are coalesced into a single refresh burst without sacrificing the “live preview” feel.

External Listener Pattern

Some UI components need to react to configuration changes but are not themselves form managers (e.g., the pipeline editor’s preview labels showing “MAT”, “NAP”, “FIJI” indicators). The external listener pattern allows these components to register for cross-window notifications without participating in the full form manager lifecycle.

Registration

External listeners register via the class method register_external_listener():

# pipeline_editor.py
ParameterFormManager.register_external_listener(
    self,
    self._on_cross_window_context_changed,
    self._on_cross_window_context_refreshed
)

The registration stores a tuple (listener, value_changed_handler, refresh_handler) in the class-level _external_listeners list. Unlike form managers, external listeners:

  • Do not participate in the mesh signal topology (no bidirectional connections)

  • Do not emit signals themselves

  • Receive notifications via direct method calls, not Qt signals

  • Are not scoped (they receive all notifications regardless of scope_id)

Notification Points

External listeners are notified at three critical points:

  1. Global refresh (trigger_global_cross_window_refresh()): Called when global config changes or when all managers are unregistered (e.g., after cancel). Notifies all external listeners with refresh_handler(None, None).

  2. Reset operations (reset_all_parameters()): Called when a form is reset (either root or nested manager). Notifies external listeners with refresh_handler(object_instance, context_obj) via _notify_external_listeners_refreshed().

  3. Dialog cancel (reject() in BaseFormDialog subclasses): Called when a dialog is cancelled (Cancel button or Escape key). Must call trigger_global_cross_window_refresh() AFTER unregistration to ensure external listeners receive the refresh notification.

Critical Implementation Requirements

For external listeners to work correctly:

  1. Cancel operations (reject()) must call trigger_global_cross_window_refresh() AFTER super().reject() to ensure external listeners are notified after the window is unregistered.

  2. Reset operations must call _notify_external_listeners_refreshed() for both root and nested managers to ensure external listeners update immediately.

  3. External listeners must implement both value_changed_handler(field_path, new_value, editing_object, context_object) and refresh_handler(editing_object, context_object) to handle both incremental changes and full refreshes.

Example: Pipeline Editor Preview Labels

The pipeline editor uses external listeners to update preview labels (MAT, NAP, FIJI) when configuration changes occur in other windows:

# app/widgets/pipeline_editor.py
def setup_connections(self):
    """Setup signal connections."""
    # Register as external listener for cross-window updates
    ParameterFormManager.register_external_listener(
        self,
        self._on_cross_window_context_changed,
        self._on_cross_window_context_refreshed
    )

def _on_cross_window_context_refreshed(self, editing_object, context_object):
    """Handle cross-window context refresh (e.g., after cancel or reset)."""
    logger.info(f"🔄 Pipeline editor: Context refreshed, refreshing preview labels")
    self._update_step_list()  # Refresh all preview labels

def closeEvent(self, event):
    """Handle widget close event."""
    # Unregister external listener
    ParameterFormManager.unregister_external_listener(self)
    super().closeEvent(event)

This pattern ensures that preview labels stay synchronized with configuration changes across all open windows, including when dialogs are cancelled or reset.

Dialog Lifecycle Management

Proper dialog cleanup is critical to prevent ghost form managers that cause infinite refresh loops and runaway CPU usage.

The Ghost Manager Problem

When a dialog closes without unregistering its form manager, it remains in the _active_form_managers registry as a “ghost”. When a new dialog opens and the user types, the system collects context from the ghost manager, which triggers a refresh in the ghost, which collects context from the new manager, creating an infinite ping-pong loop.

Qt Dialog Lifecycle Quirk

Qt’s QDialog.accept() and QDialog.reject() methods do NOT trigger closeEvent() - they just hide the dialog and emit signals. This means cleanup code in closeEvent() is never called when users click Save or Cancel buttons. Dialogs must explicitly unregister in accept(), reject(), and closeEvent() to ensure cleanup happens regardless of how the dialog closes.

BaseFormDialog Pattern

BaseFormDialog solves the cleanup problem by providing a base class that automatically handles unregistration. It overrides accept(), reject(), and closeEvent() to call _unregister_all_form_managers() before closing. Subclasses implement _get_form_managers() to return their form manager instances, and the base class handles all cleanup automatically.

This pattern ensures that every dialog using ParameterFormManager properly cleans up, preventing ghost manager bugs without requiring developers to remember manual cleanup in multiple methods.

Form State Synchronization

The three-state synchronization pattern ensures consistency across all UI components.

Internal Parameter State

parameters stores the form’s internal parameter dictionary. This represents the current user edits and serves as the source of truth for widget display.

Thread-Local Context State

Thread-local context registries maintain placeholder resolution state. This must be kept synchronized with form state during operations like reset.

Widget Display State

Widget values and placeholder text reflect the combination of internal parameters and context resolution. The form manager ensures widgets always display the correct state based on current parameters and context.

Example: BaseFormDialog Usage

from pyqt_reactive.widgets.shared.base_form_dialog import BaseFormDialog
from pyqt_reactive.forms.parameter_form_manager import ParameterFormManager

class MyConfigDialog(BaseFormDialog):
    """Configuration dialog with automatic cleanup."""

    def __init__(self, config, parent=None):
        super().__init__(parent)

        # Create form manager
        self.form_manager = ParameterFormManager(
            field_id="my_config",
            dataclass_type=type(config),
            initial_values=config
        )

    def _get_form_managers(self):
        """Return form managers to unregister (required by BaseFormDialog)."""
        return [self.form_manager]

    # No need to override accept(), reject(), or closeEvent()
    # BaseFormDialog handles all cleanup automatically!

This pattern ensures proper cleanup regardless of how the dialog closes (Save button → accept(), Cancel button → reject(), X button → closeEvent()).

See Also

  • Parameter Form Service Architecture - Refactored service-oriented architecture (in development)

  • context_system - Thread-local context management patterns

  • Service Layer Architecture - Service layer integration with forms

  • code_ui_interconversion - Code/UI interconversion patterns

  • BaseFormDialog - Base class for dialog cleanup

  • ConfigWindow - Example BaseFormDialog implementation

  • DualEditorWindow - Example BaseFormDialog implementation