Skip to main content

IVariable

Interface for actor variables that store typed values with persistence, change tracking, and scope management. Variables are the primary mechanism for storing actor state, configuration parameters, and runtime data.

Namespace: ControlBee.Interfaces

Extends: IActorItem, INotifyValueChanged

Overview

Variables in ControlBee provide strongly-typed, scope-aware state management with several key features:

  • Type Safety - Generic Variable<T> ensures compile-time type checking
  • Scoped Persistence - Global, Local, and Temporary scopes control save/load behavior
  • Change Tracking - Automatic notifications when values change
  • Audit Trail - Track who modified values and when
  • Recipe Management - Save/load variable sets for different products
  • UI Integration - Automatically appear in configuration interfaces

Variables are typically declared as public fields in actor classes:

public class StageActor : Actor
{
// Global - shared across all recipes, auto-saved
public Variable<double> HomeSpeed = new(VariableScope.Global, 10.0);
public Variable<double> SafetyMargin = new(VariableScope.Global, 5.0);

// Local - different value per recipe, auto-saved
public Variable<double> Speed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPos = new(VariableScope.Local);

// Temporary - runtime state, not auto-saved
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<int> CurrentIndex = new(VariableScope.Temporary);
}

Properties

Id

int? Id { get; }

The database ID of this variable, assigned when the variable is persisted. null for variables that haven't been saved yet or for Local/Temporary scoped variables.

Usage:

  • Internal use by the variable manager
  • Tracking persistence status
  • Database operations

ValueObject

object? ValueObject { get; set; }

The current value of the variable as an untyped object. For type-safe access, use the generic Variable<T>.Value property instead.

Usage Example:

// Type-safe access (preferred)
Variable<double> speed = new(VariableScope.Local, 100.0);
double currentSpeed = speed.Value;
speed.Value = 150.0;

// Untyped access via interface (for generic code)
IVariable variable = speed;
object? value = variable.ValueObject; // Returns 150.0 as object
variable.ValueObject = 200.0; // Requires casting

Use Cases:

  • Generic variable handling code
  • Recipe loading/saving
  • UI data binding
  • Reflection-based operations

OldValueObject

object? OldValueObject { get; }

The previous value before the last change. Used for change detection, validation, and undo operations.

Usage Example:

public class StageActor : Actor
{
public Variable<double> Speed = new(VariableScope.Local, 100.0);

public StageActor(ActorConfig config) : base(config)
{
Speed.ValueChanged += (s, e) =>
{
double oldSpeed = (double)e.OldValue!;
double newSpeed = (double)e.NewValue!;

Console.WriteLine($"Speed changed from {oldSpeed} to {newSpeed}");

if (newSpeed > 200)
{
// Revert to old value
Speed.Value = oldSpeed;
Console.WriteLine("Speed limit exceeded, reverted");
}
};
}
}

Use Cases:

  • Change validation
  • Undo functionality
  • Audit logging
  • Change comparison

Scope

VariableScope Scope { get; }

Defines the persistence and lifecycle behavior of the variable:

  • VariableScope.Local - Saved to database with no recipe distinction (same value for all recipes). Used for system-wide settings that don't change between products.
  • VariableScope.Local - Saved to database per recipe (different values for different recipes). Used for product-specific configurations.
  • VariableScope.Temporary - Runtime state, not automatically saved. Can be manually saved with SaveTemporaryVariables(), stored globally (no recipe distinction).

Choosing the Right Scope:

// Global - System settings shared across all recipes
public Variable<double> HomeSpeed = new(VariableScope.Local, 10.0);
public Variable<double> SafetyOffset = new(VariableScope.Local, 5.0);
public Variable<int> DebounceMs = new(VariableScope.Local, 50);

// Local - Product-specific settings that change per recipe
public Variable<double> ProcessSpeed = new(VariableScope.Local, 100.0);
public Variable<Position1D> LoadPos = new(VariableScope.Local);
public Variable<int> GridRows = new(VariableScope.Local, 5);

// Temporary - Runtime state that changes frequently
public Variable<bool> Busy = new(VariableScope.Temporary);
public Variable<int> CurrentIndex = new(VariableScope.Temporary);
public Variable<GridContainer> WorkTray = new(VariableScope.Temporary);

See Also:

ActorName

string ActorName { get; }

The name of the actor that owns this variable. Used for:

  • Database storage - organizing variables by actor
  • Change history - tracking which actor's variables changed
  • UI organization - grouping variables by actor
  • Diagnostics - identifying variable ownership

Example:

var variable = actor.GetItem("Speed") as IVariable;
Console.WriteLine($"Variable owned by: {variable.ActorName}");
// Output: Variable owned by: Stage0

Dirty

bool Dirty { get; set; }

Indicates whether the variable has unsaved changes. Set to true when the value changes, cleared when saved to the database.

Usage Example:

// Check if any variables have unsaved changes
foreach (var actor in actorRegistry.GetAll())
{
var items = actor.GetItems();
foreach (var (path, type) in items)
{
if (actor.GetItem(path) is IVariable variable && variable.Dirty)
{
Console.WriteLine($"{actor.Name}.{path} has unsaved changes");
}
}
}

// Save all dirty variables
variableManager.Save(); // Clears Dirty flag for all saved variables

Automatic Tracking:

  • For Global and Local scope variables, Dirty is automatically set to true when the value changes
  • For Temporary scope variables, Dirty is not automatically set (manual save only)

Use Cases:

  • Determining when to save
  • Warning about unsaved changes
  • Change indicators in UI
  • Audit trail triggers

UserInfo

IUserInfo? UserInfo { get; set; }

Information about the user who last modified this variable. Used for audit trails and change tracking.

Properties:

  • UserName - The user's login name
  • AuthorityLevel - The user's permission level
  • Timestamp - When the change was made

Usage Example:

public class StageActor : Actor
{
public Variable<Position1D> LoadPos = new(VariableScope.Local);

public void TeachLoadPosition()
{
LoadPos.Value.TeachCurrent();

// UserInfo automatically set by the system
if (LoadPos.UserInfo != null)
{
Console.WriteLine($"Position taught by: {LoadPos.UserInfo.UserName}");
Console.WriteLine($"At: {LoadPos.UserInfo.Timestamp}");
}
}
}

// Query change history
var changes = variableManager.ReadVariableChanges(new QueryOptions
{
Filter = "ActorName = 'Stage0' AND ItemPath = 'LoadPos'",
OrderBy = "Timestamp DESC",
Limit = 10
});

foreach (var change in changes)
{
Console.WriteLine($"{change.Timestamp}: {change.UserName} changed from " +
$"{change.OldValue} to {change.NewValue}");
}

Use Cases:

  • Audit logging
  • Change accountability
  • Compliance requirements
  • Debugging (who changed what)

Methods

ToJson

Serializes the variable value to a JSON string for storage or transmission.

string ToJson();

Returns: A JSON representation of the current value.

Usage Example:

// Simple types
Variable<double> speed = new(VariableScope.Local, 100.0);
string json = speed.ToJson();
Console.WriteLine(json); // Output: "100.0"

// Complex types
Variable<Position1D> pos = new(VariableScope.Local);
pos.Value.Set(100, 50, 200, 100); // position, speed, accel, decel
string json = pos.ToJson();
// Output: {"Position":100,"Speed":50,"Accel":200,"Decel":100}

// Arrays
Variable<Array2D<bool>> grid = new(VariableScope.Local, new Array2D<bool>(3, 3));
grid.Value[1, 1] = true;
string json = grid.ToJson();
// Output: [[false,false,false],[false,true,false],[false,false,false]]

Use Cases:

  • Recipe saving - persist variables to database
  • Export/import - transfer configurations between systems
  • Backup/restore - save variable states
  • Network transmission - send values to remote systems

FromJson

Deserializes the variable value from a JSON string.

void FromJson(string data);

Parameters:

  • data — The JSON string to deserialize and assign to the variable.

Usage Example:

// Restore from saved JSON
Variable<double> speed = new(VariableScope.Local, 100.0);
speed.FromJson("150.0");
Console.WriteLine(speed.Value); // Output: 150.0

// Load complex type
Variable<Position1D> pos = new(VariableScope.Local);
pos.FromJson("{\"Position\":100,\"Speed\":50,\"Accel\":200,\"Decel\":100}");
Console.WriteLine($"Position: {pos.Value.Position}"); // Output: Position: 100

// Load array
Variable<Array2D<bool>> grid = new(VariableScope.Local, new Array2D<bool>(3, 3));
grid.FromJson("[[false,false,false],[false,true,false],[false,false,false]]");
Console.WriteLine($"Center cell: {grid.Value[1, 1]}"); // Output: Center cell: True

Use Cases:

  • Recipe loading - restore variables from database
  • Import - load configurations from files
  • Network reception - receive values from remote systems
  • Undo/redo - restore previous states

Error Handling:

try
{
variable.FromJson(jsonString);
}
catch (JsonException ex)
{
Console.WriteLine($"Failed to deserialize: {ex.Message}");
// Handle error - perhaps keep current value or use default
}

Change Notifications

Variables implement INotifyValueChanged, allowing subscribers to be notified when values change:

public class StageActor : Actor
{
public Variable<int> RowCount = new(VariableScope.Local, 5);
public Variable<int> ColCount = new(VariableScope.Local, 8);
public Variable<GridContainer> Tray = new(VariableScope.Temporary);

public StageActor(ActorConfig config) : base(config)
{
// Subscribe to changes
RowCount.ValueChanged += OnLayoutChanged;
ColCount.ValueChanged += OnLayoutChanged;
}

private void OnLayoutChanged(object? sender, ValueChangedArgs e)
{
// Recreate grid when dimensions change
Tray.Value = new GridContainer(RowCount.Value, ColCount.Value);
Console.WriteLine($"Grid resized to {RowCount.Value}x{ColCount.Value}");
}
}

See Also:

Common Patterns

1. Validation on Change

public Variable<double> Speed = new(VariableScope.Local, 100.0);

public MyActor(ActorConfig config) : base(config)
{
Speed.ValueChanged += (s, e) =>
{
double newSpeed = (double)e.NewValue!;

if (newSpeed < 0 || newSpeed > 200)
{
Speed.Value = (double)e.OldValue!; // Revert
ErrorDialog.Show("Speed must be between 0 and 200");
}
};
}

2. Cascading Updates

public Variable<bool> UseVacuum = new(VariableScope.Local, true);
public Variable<double> VacuumPressure = new(VariableScope.Local, -80.0);

public MyActor(ActorConfig config) : base(config)
{
UseVacuum.ValueChanged += (s, e) =>
{
// Hide vacuum pressure when not using vacuum
((IActorItemModifier)VacuumPressure).Visible = (bool)e.NewValue!;
};
}

3. Derived State

public Variable<int> RowCount = new(VariableScope.Local, 5);
public Variable<int> ColCount = new(VariableScope.Local, 8);
public Variable<int> TotalCells = new(VariableScope.Local, 40);

public MyActor(ActorConfig config) : base(config)
{
void UpdateTotal(object? s, ValueChangedArgs e)
{
TotalCells.Value = RowCount.Value * ColCount.Value;
}

RowCount.ValueChanged += UpdateTotal;
ColCount.ValueChanged += UpdateTotal;
}

Generic Variable Class

In practice, you'll use the generic Variable<T> class, which implements IVariable:

// Scalar types
public Variable<int> Count = new(VariableScope.Local, 0);
public Variable<double> Value = new(VariableScope.Local, 0.0);
public Variable<bool> Flag = new(VariableScope.Local, false);
public Variable<string> Name = new(VariableScope.Local, "");

// Position types
public Variable<Position1D> TargetX = new(VariableScope.Local);
public Variable<Position2D> TargetXY = new(VariableScope.Local);
public Variable<Position3D> TargetXYZ = new(VariableScope.Local);

// Array types
public Variable<Array1D<bool>> Flags = new(VariableScope.Local, new Array1D<bool>(10));
public Variable<Array2D<int>> Grid = new(VariableScope.Local, new Array2D<int>(5, 8));

// Custom types
public Variable<GridContainer> Tray = new(VariableScope.Temporary);

See Also