Understanding the State Pattern with Calculator and Blazor ATM 🧠💻
🛠 Introduction
The State Pattern allows an object to change its behavior dynamically based on its internal state. It’s a powerful design pattern for handling complex, state-dependent behavior in an organized and maintainable way.
In this article, we’ll explore the State Pattern through two practical examples:
A Console Calculator that reacts to numeric and operation inputs.
A Blazor ATM Web App that transitions through real-life ATM actions like inserting a card and authenticating a PIN.
📚 Example 1: Calculator State Pattern
1️⃣ State Interface
Defines the contract that all states must follow:
public interface ICalculatorState {
void EnterNumber(CalculatorContext context, int number);
void PerformOperation(CalculatorContext context, string operation);
}
2️⃣ Concrete States
➡️ NumberState: Handles number input
public class NumberState : ICalculatorState {
public void EnterNumber(CalculatorContext context, int number) {
context.CurrentValue = number;
Console.WriteLine($"Number entered: {number}");
}
public void PerformOperation(CalculatorContext context, string operation) {
Console.WriteLine("Operation not allowed in Number State.");
}
}
➡️ OperationState: Handles operation execution
public class OperationState : ICalculatorState {
public void EnterNumber(CalculatorContext context, int number) {
context.SecondValue = number;
Console.WriteLine($"Second number entered: {number}");
}
public void PerformOperation(CalculatorContext context, string operation) {
switch (operation) {
case "+":
context.Result = context.CurrentValue + context.SecondValue;
break;
case "-":
context.Result = context.CurrentValue - context.SecondValue;
break;
default:
Console.WriteLine("Unsupported operation.");
return;
}
Console.WriteLine($"Operation performed: {context.Result}");
context.SetState(new NumberState()); // Go back to NumberState
}
}
3️⃣ Context
Controls state transitions and stores values:
public class CalculatorContext {
private ICalculatorState _state;
public int CurrentValue { get; set; }
public int SecondValue { get; set; }
public int Result { get; set; }
public CalculatorContext() {
_state = new NumberState();
}
public void SetState(ICalculatorState state) {
_state = state;
}
public void EnterNumber(int number) {
_state.EnterNumber(this, number);
}
public void PerformOperation(string operation) {
_state.PerformOperation(this, operation);
}
}
4️⃣ Main Program
class Program {
static void Main(string[] args) {
CalculatorContext calculator = new CalculatorContext();
calculator.EnterNumber(10);
calculator.SetState(new OperationState());
calculator.EnterNumber(20);
calculator.PerformOperation("+");
}
}
💡 Output:
Number entered: 10
Second number entered: 20
Operation performed: 30
🏧 Example 2: Blazor ATM Using State Pattern
We also created a Blazor WebAssembly ATM simulator using the same pattern. Each button triggers a state-based action.
🔌 ATM Context (Core Logic)
public class ATMMachine {
private IATMState _idleState;
private IATMState _authenticationState;
private IATMState _transactionSelectionState;
private IATMState _transactionProcessingState;
private IATMState _currentState;
public void InsertCard() => _currentState.InsertCard();
public void EnterPIN(int pin) => _currentState.EnterPIN(pin);
public void SelectTransaction() => _currentState.SelectTransaction();
public void ProcessTransaction() => _currentState.ProcessTransaction();
public void EjectCard() => _currentState.EjectCard();
}
Each state class implements a shared IATMState interface and changes the machine’s behavior accordingly.
public interface IATMState
{
void InsertCard();
void EnterPIN(int pin);
void SelectTransaction();
void ProcessTransaction();
void EjectCard();
}
public class AuthenticationState : IATMState
{
private ATMMachine _atmMachine;
public AuthenticationState(ATMMachine atmMachine)
{
_atmMachine = atmMachine;
}
public void InsertCard()
{
Console.WriteLine("Card already inserted.");
}
public void EnterPIN(int pin)
{
if (pin == 1234) // Example PIN check
{
Console.WriteLine("PIN correct.");
_atmMachine.SetState(_atmMachine.GetTransactionSelectionState());
}
else
{
Console.WriteLine("PIN incorrect. Try again.");
}
}
public void SelectTransaction()
{
Console.WriteLine("Enter PIN first.");
}
public void ProcessTransaction()
{
Console.WriteLine("Enter PIN first.");
}
public void EjectCard()
{
Console.WriteLine("Card ejected.");
_atmMachine.SetState(_atmMachine.GetIdleState());
}
}
public class IdleState(ATMMachine atmMachine) : IATMState
{
private ATMMachine _atmMachine = atmMachine;
public void InsertCard()
{
Console.WriteLine("Card inserted.");
_atmMachine.SetState(_atmMachine.GetAuthenticationState());
}
public void EnterPIN(int pin)
{
Console.WriteLine("Insert card first.");
}
public void SelectTransaction()
{
Console.WriteLine("Insert card first.");
}
public void ProcessTransaction()
{
Console.WriteLine("Insert card first.");
}
public void EjectCard()
{
Console.WriteLine("No card to eject.");
}
}
public class TransactionProcessingState : IATMState
{
private ATMMachine _atmMachine;
public TransactionProcessingState(ATMMachine atmMachine)
{
_atmMachine = atmMachine;
}
public void InsertCard()
{
Console.WriteLine("Transaction in progress. Please wait.");
}
public void EnterPIN(int pin)
{
Console.WriteLine("Transaction in progress. Please wait.");
}
public void SelectTransaction()
{
Console.WriteLine("Transaction in progress. Please wait.");
}
public void ProcessTransaction()
{
Console.WriteLine("Transaction completed.");
_atmMachine.SetState(_atmMachine.GetIdleState());
}
public void EjectCard()
{
Console.WriteLine("Transaction in progress. Please wait.");
}
}
public class TransactionSelectionState : IATMState
{
private ATMMachine _atmMachine;
public TransactionSelectionState(ATMMachine atmMachine)
{
_atmMachine = atmMachine;
}
public void InsertCard()
{
Console.WriteLine("Card already inserted.");
}
public void EnterPIN(int pin)
{
Console.WriteLine("PIN already entered.");
}
public void SelectTransaction()
{
Console.WriteLine("Transaction selected.");
_atmMachine.SetState(_atmMachine.GetTransactionProcessingState());
}
public void ProcessTransaction()
{
Console.WriteLine("Select a transaction first.");
}
public void EjectCard()
{
Console.WriteLine("Card ejected.");
_atmMachine.SetState(_atmMachine.GetIdleState());
}
}
🖥️ Blazor UI Integration
Razor UI binds directly to the machine state:
<input type="number" @bind="enteredPin" disabled="@AtmMachine.IsEnterPinDisabled" />
<button @onclick="SubmitPin" disabled="@AtmMachine.IsEnterPinDisabled">Submit PIN</button>
<button @onclick="AtmMachine.InsertCard" disabled="@AtmMachine.IsInsertCardDisabled">Insert Card</button>
<button @onclick="AtmMachine.SelectTransaction" disabled="@AtmMachine.IsSelectTransactionDisabled">Select Transaction</button>
<button @onclick="AtmMachine.ProcessTransaction" disabled="@AtmMachine.IsProcessTransactionDisabled">Process Transaction</button>
<button @onclick="AtmMachine.EjectCard" disabled="@AtmMachine.IsEjectCardDisabled">Eject Card</button>
public partial class Home
{
private int enteredPin = 1234;
private void SubmitPin()
{
AtmMachine.EnterPIN(enteredPin);
}
protected override void OnInitialized()
{
AtmMachine.OnStateChanged += StateHasChanged;
}
public void Dispose()
{
AtmMachine.OnStateChanged -= StateHasChanged;
}
}
Each action is enabled/disabled based on the current state logic, ensuring the UI only shows valid options.
🧩 Can the State Pattern Work with Other Patterns?
Yes—the State Pattern is often combined with other design patterns to solve more complex problems in a modular way:
✅ Strategy Pattern: State and Strategy share similar structures (both use composition and delegation). Strategy is used to select algorithms, while State is used to model behavioral change over time.
✅ Factory Pattern: You can use a Factory to instantiate and manage your State objects, especially when transitions depend on dynamic runtime conditions.
✅ Observer Pattern: Useful when you want the UI (or other components) to react when the context transitions between states—ideal for reactive UIs like Blazor or WPF.
✅ MVVM (Model-View-ViewModel): State transitions can drive ViewModel behavior cleanly, allowing views to respond automatically via data binding.
These combinations help make your architecture more flexible, testable, and extensible—especially in real-world Blazor or MVVM applications.
✅ State Pattern + MVVM = Clean UI Logic
The State Pattern encapsulates behavior per state, while MVVM exposes UI-facing logic through observable properties and commands. Combining them ensures:
🔁 Encapsulation of business rules in state classes (IATMState)
📦 Single-responsibility of the ViewModel (delegates logic, updates UI reactively)
🔄 Reactivity: ObservableProperty, RelayCommand, and OnPropertyChanged notify the Blazor UI when the state changes
💡 Testability: You can unit test both state logic and ViewModel independently
🧼 Maintainability: You avoid bloated code-behind or if-else spaghetti
🧠 HomeViewModel: The Mediator Between State Machine and View
In this design:
- HomeViewModel holds a reference to the ATM state machine (ATM instance)
- It exposes properties and commands that the Blazor UI binds to (ObservableProperty, RelayCommand)
- It reacts to state changes via _atm.OnStateChanged and notifies the view through OnPropertyChanged
- The actual behavior and rules (what's allowed in each state) stay encapsulated in the ATM and its IATMState implementations
using BlazorAppAtmMachine.State;
namespace BlazorAppAtmMachine.Vm;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
/// <summary>
/// ViewModel for the Blazor ATM UI. Implements MVVM pattern using CommunityToolkit.Mvvm.
/// Delegates all logic to the ATM state machine and exposes observable properties and commands for the UI.
/// </summary>
public partial class HomeViewModel : ObservableObject, IDisposable
{
/// <summary>
///
/// </summary>
private readonly ATM _atm;
/// <summary>
/// Gets the name of the current ATM state for debugging or display.
/// </summary>
public string CurrentStateName => _atm.CurrentState.GetType().Name;
/// <summary>
/// Gets the amount of cash remaining in the ATM.
/// </summary>
public int CashInMachine => _atm.CashInMachine;
/// <summary>
/// Initializes the ViewModel and subscribes to ATM state change notifications.
/// </summary>
public HomeViewModel(ATM atm)
{
_atm = atm;
_atm.OnStateChanged += OnStateChanged;
}
/// <summary>
/// PIN entered by the user.
/// </summary>
[ObservableProperty]
private int enteredPin = 0;
/// <summary>
/// Transaction amount input by the user.
/// </summary>
[ObservableProperty]
private decimal transactionAmount = 0m;
// ----------------- Command Bindings -----------------
/// <summary>
/// Submits the entered PIN to the ATM.
/// </summary>
[RelayCommand]
private void SubmitPin() => _atm.EnterPIN(this.EnteredPin);
/// <summary>
/// Inserts a card into the ATM.
/// </summary>
[RelayCommand]
private void InsertCard() => _atm.InsertCard();
/// <summary>
/// Simulates selecting a transaction.
/// </summary>
[RelayCommand]
private void SelectTransaction() => _atm.SelectTransaction();
/// <summary>
/// Processes a transaction with the specified amount.
/// Only enabled when the amount is greater than zero.
/// </summary>
[RelayCommand(CanExecute = nameof(CanProcessTransaction))]
public void ProcessTransaction()
{
_atm.TransactionAmount = this.TransactionAmount;
_atm.ProcessTransaction();
}
/// <summary>
/// Validates whether the transaction can be processed.
/// </summary>
private bool CanProcessTransaction => this.TransactionAmount > 0;
/// <summary>
/// Ejects the card from the ATM.
/// </summary>
[RelayCommand]
private void EjectCard() => _atm.EjectCard();
// ----------------- State-based UI Flags -----------------
/// <summary>
/// Whether the PIN input and submit button should be disabled.
/// </summary>
public bool IsEnterPinDisabled => !_atm.CurrentState.CanEnterPin;
/// <summary>
/// Whether the Insert Card button should be disabled.
/// </summary>
public bool IsInsertCardDisabled => !_atm.CurrentState.CanInsertCard;
/// <summary>
/// Whether the Select Transaction button should be disabled.
/// </summary>
public bool IsSelectTransactionDisabled => !_atm.CurrentState.CanSelectTransaction;
/// <summary>
/// Whether the Process Transaction button should be disabled.
/// </summary>
public bool IsProcessTransactionDisabled => !_atm.CurrentState.CanProcessTransaction;
/// <summary>
/// Whether the Eject Card button should be disabled.
/// </summary>
public bool IsEjectCardDisabled => !_atm.CurrentState.CanEjectCard;
// ----------------- State Change Subscription -----------------
/// <summary>
/// Event used to notify the UI when the ATM state changes.
/// </summary>
public event Action? StateChanged;
/// <summary>
/// Called when the ATM state changes. Raises UI update events for all state-dependent properties.
/// </summary>
private void OnStateChanged()
{
OnPropertyChanged(nameof(IsEnterPinDisabled));
OnPropertyChanged(nameof(IsInsertCardDisabled));
OnPropertyChanged(nameof(IsSelectTransactionDisabled));
OnPropertyChanged(nameof(IsProcessTransactionDisabled));
OnPropertyChanged(nameof(IsEjectCardDisabled));
StateChanged?.Invoke();
}
/// <summary>
/// Disposes of the ViewModel and unsubscribes from ATM events.
/// </summary>
public void Dispose() => _atm.OnStateChanged -= OnStateChanged;
}
Bind UI to state machine, expose commands, track UI state flags
public partial class Home : IDisposable
{
[Inject]
public HomeViewModel hViewModel { get; set; } = default!;
protected override void OnInitialized() => hViewModel.StateChanged += RefreshUI;
private void RefreshUI() => InvokeAsync(StateHasChanged);
public void Dispose() => hViewModel.StateChanged -= RefreshUI;
}
Bind to ViewModel, display inputs and buttons
@page "/"
@using BlazorAppAtmMachine.Vm
<p><strong>State:</strong> @hViewModel.CurrentStateName</p>
<p><strong>Cash left:</strong> $@hViewModel.CashInMachine</p>
<div class="mb-3">
<label for="pinInput" class="form-label">Enter PIN</label>
<input id="pinInput"
type="number"
class="form-control"
@bind="hViewModel.EnteredPin"
disabled="@hViewModel.IsEnterPinDisabled" />
<button class="btn btn-secondary mt-2"
@onclick="hViewModel.SubmitPinCommand.Execute"
disabled="@hViewModel.IsEnterPinDisabled">
Submit PIN
</button>
</div>
<div class="mb-3">
<input T="decimal"
@bind="hViewModel.TransactionAmount"
Label="Amount to Withdraw"
Variant="Variant.Outlined"
For="@(() => hViewModel.TransactionAmount)"
Immediate="true"
Class="w-100" />
</div>
<div class="btn-group mb-3" role="group">
<button class="btn btn-primary"
@onclick="hViewModel.InsertCardCommand.Execute"
disabled="@hViewModel.IsInsertCardDisabled">
Insert Card
</button>
<button class="btn btn-success"
@onclick="hViewModel.SelectTransactionCommand.Execute"
disabled="@hViewModel.IsSelectTransactionDisabled">
Select Transaction
</button>
<button class="btn btn-warning"
@onclick="hViewModel.ProcessTransactionCommand.Execute"
disabled="@(!hViewModel.ProcessTransactionCommand.CanExecute(null))">
Process Transaction
</button>
<button class="btn btn-danger"
@onclick="hViewModel.EjectCardCommand.Execute"
disabled="@hViewModel.IsEjectCardDisabled">
Eject Card
</button>
</div>
✅ Conclusion
The State Pattern is a powerful architectural approach that:
- Keeps logic encapsulated in clean, testable components.
- Removes complex if/else or switch statements.
- Allows dynamic transitions without confusing business logic.
Whether you're building a console calculator or a web-based ATM, this pattern scales elegantly with complexity.
🔗 References
💻 Blazor ATM State Machine (GitHub)
💻 Blazor Booking Project (GitHub)
🧮 Calculator State Pattern (GitHub)
💻 [Blazor ATM State Machine with MVVM (GitHub)]
Top comments (0)