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)