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)