DEV Community

Cover image for Understanding the State Pattern
Spyros Ponaris
Spyros Ponaris

Posted on • Edited on

Understanding the State Pattern

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); } 
Enter fullscreen mode Exit fullscreen mode

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."); } } 
Enter fullscreen mode Exit fullscreen mode

➡️ 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 } } 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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("+"); } } 
Enter fullscreen mode Exit fullscreen mode

💡 Output:

Number entered: 10 Second number entered: 20 Operation performed: 30 
Enter fullscreen mode Exit fullscreen mode

🏧 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(); } 
Enter fullscreen mode Exit fullscreen mode

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(); } 
Enter fullscreen mode Exit fullscreen mode
 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()); } } 
Enter fullscreen mode Exit fullscreen mode
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."); } } 
Enter fullscreen mode Exit fullscreen mode
 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."); } } 
Enter fullscreen mode Exit fullscreen mode
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()); } } 
Enter fullscreen mode Exit fullscreen mode

🖥️ 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; } } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

✅ 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)]

🧠 State Pattern – Refactoring Guru

📘 State Pattern in C# – DotNetTutorials

Top comments (0)