5
\$\begingroup\$

I'm beginning to learn WPF and wanted to do a quick exercise. The code is as simple as it gets but I tried to evaluate each and every possible edge case. Any improvement is welcome, thanks.

C#:

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;

namespace Calculator
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        double firstNumber, secondNumber, resultNumber = 0;
        bool calcDone = false;
        Operations operation = Operations.None;
        string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;

        public MainWindow()
        {
            InitializeComponent();

            //Assign to the decimal button the separator from the current culture
            dec.Content = separator;
        }

        //List the possible numeric operations
        private enum Operations
        {
            None,
            Division,
            Multiplication,
            Subtraction,
            Sum
        }

        //Manage number buttons input
        private void NumberButton_Click(object sender, RoutedEventArgs e)
        {
            Button button = (Button)sender;

            if (calcDone) //calculation already done
            {
                result.Content = $"{button.Content}";
                calcDone = false;
            }
            else //calculation not yet done
            {
                if (result.Content.ToString() == "0")
                {
                    result.Content = $"{button.Content}";
                }
                else
                {
                    result.Content = $"{result.Content}{button.Content}";
                }
            }

        }

        //Manage operation buttons input
        private void OperationButton_Click(object sender, RoutedEventArgs e)
        {
            Button button = (Button)sender;

            //Evaluate button pressed
            switch (button.Content.ToString())
            {
                case "AC":
                    result.Content = "0";
                    break;
                case "+/-":
                    if (!(result.Content.ToString() == "0"))
                    {
                        result.Content = Convert.ToDouble(result.Content) * -1;
                    }
                    break;
                case "%":
                    if (!(result.Content.ToString() == "0"))
                    {
                        result.Content = Convert.ToDouble(result.Content) / 100;
                    }
                    break;
                case "÷":
                    firstNumber = Convert.ToDouble(result.Content);
                    operation = Operations.Division;
                    result.Content = "0";
                    break;
                case "×":
                    firstNumber = Convert.ToDouble(result.Content);
                    operation = Operations.Multiplication;
                    result.Content = "0";
                    break;
                case "-":
                    firstNumber = Convert.ToDouble(result.Content);
                    operation = Operations.Subtraction;
                    result.Content = "0";
                    break;
                case "+":
                    firstNumber = Convert.ToDouble(result.Content);
                    operation = Operations.Sum;
                    result.Content = "0";
                    break;
                case "=":
                    switch (operation)
                    {
                        case Operations.Division:
                            if (calcDone)
                            {
                                if (!(result.Content.ToString() == "ERROR"))
                                {
                                    result.Content = Convert.ToDouble(result.Content) / secondNumber;
                                }
                            }
                            else
                            {
                                //Check if division by 0
                                if (result.Content.ToString() == "0")
                                {
                                    result.Content = "ERROR";
                                    calcDone = true;
                                }
                                else
                                {
                                    secondNumber = Convert.ToDouble(result.Content);
                                    resultNumber = firstNumber / secondNumber;
                                    result.Content = resultNumber;
                                    calcDone = true;
                                }
                            }
                            break;
                        case Operations.Multiplication:
                            if (calcDone)
                            {
                                result.Content = Convert.ToDouble(result.Content) * secondNumber;
                            }
                            else
                            {
                                secondNumber = Convert.ToDouble(result.Content);
                                resultNumber = firstNumber * secondNumber;
                                result.Content = resultNumber;
                                calcDone = true;
                            }
                            break;
                        case Operations.Subtraction:
                            if (calcDone)
                            {
                                result.Content = Convert.ToDouble(result.Content) - secondNumber;
                            }
                            else
                            {
                                secondNumber = Convert.ToDouble(result.Content);
                                resultNumber = firstNumber - secondNumber;
                                result.Content = resultNumber;
                                calcDone = true;
                            }
                            break;
                        case Operations.Sum:
                            if (calcDone)
                            {
                                result.Content = Convert.ToDouble(result.Content) + secondNumber;
                            }
                            else
                            {
                                secondNumber = Convert.ToDouble(result.Content);
                                MessageBox.Show($"{firstNumber} + {secondNumber}");
                                resultNumber = firstNumber + secondNumber;
                                result.Content = resultNumber;
                                calcDone = true;
                            }
                            break;
                    }
                    break;
                default:
                    if (!result.Content.ToString().Contains(separator))
                    {
                        result.Content = $"{result.Content}{button.Content}";
                    }
                    break;
            }
        }
    }
}

XAML:

<Window x:Class="Calculator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Calculator"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="300">
    <Grid Margin="8">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="1.5*"/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Label x:Name="result" Content="0" Grid.ColumnSpan="4" HorizontalAlignment="Right" 
               VerticalAlignment="Bottom" FontSize="50"/>
        <Button Content="AC" Margin="2" Grid.Column="0" Grid.Row="1"
                FontWeight="bold" Click="OperationButton_Click"/>
        <Button Content="+/-" Margin="2" Grid.Column="1" Grid.Row="1"
                FontWeight="bold" Click="OperationButton_Click"/>
        <Button Content="%" Margin="2" Grid.Column="2" Grid.Row="1"
                FontWeight="bold" Click="OperationButton_Click"/>
        <Button Content="÷" Margin="2" Grid.Column="3" Grid.Row="1"
                FontWeight="bold" Click="OperationButton_Click"/>

        <Button Content="7" Margin="2" Grid.Column="0" Grid.Row="2"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="8" Margin="2" Grid.Column="1" Grid.Row="2"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="9" Margin="2" Grid.Column="2" Grid.Row="2"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="×" Margin="2" Grid.Column="3" Grid.Row="2"
                FontWeight="bold" Click="OperationButton_Click"/>

        <Button Content="4" Margin="2" Grid.Column="0" Grid.Row="3"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="5" Margin="2" Grid.Column="1" Grid.Row="3"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="6" Margin="2" Grid.Column="2" Grid.Row="3"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="-" Margin="2" Grid.Column="3" Grid.Row="3"
                FontWeight="bold" Click="OperationButton_Click"/>

        <Button Content="1" Margin="2" Grid.Column="0" Grid.Row="4"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="2" Margin="2" Grid.Column="1" Grid.Row="4"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="3" Margin="2" Grid.Column="2" Grid.Row="4"
                FontSize="20" Click="NumberButton_Click"/>
        <Button Content="+" Margin="2" Grid.Column="3" Grid.Row="4"
                FontWeight="bold" Click="OperationButton_Click"/>

        <Button Content="0" Margin="2" Grid.Column="0" Grid.Row="5" 
                FontSize="20" Grid.ColumnSpan="2" Click="NumberButton_Click"/>
        <Button x:Name="dec" Content="" Margin="2" Grid.Column="2" Grid.Row="5"
                FontWeight="bold" Click="OperationButton_Click"/>
        <Button Content="=" Margin="2" Grid.Column="3" Grid.Row="5"
                FontWeight="bold" Click="OperationButton_Click"/>
    </Grid>
</Window>
\$\endgroup\$
  • 3
    \$\begingroup\$ Welcome to CodeReview. Could you please post the XAML part, so that the code is complete? \$\endgroup\$ – JanDotNet Feb 18 at 18:54
  • \$\begingroup\$ Sure! Didn't think about adding it because it's extremely simple but for completeness sake I'm adding it now. \$\endgroup\$ – nicktheone Feb 18 at 19:15
  • 2
    \$\begingroup\$ Even if it's extremely simple, having this code readily available allows us to copy and paste it into our IDE and try running your program, to see whether the improvements we're going to suggest really work. As a result, you get better quality reviews. :) \$\endgroup\$ – Roland Illig Feb 18 at 21:19
6
\$\begingroup\$
  • It's common to write down access modifier for fields
  • Defining one field per line makes the code more readable because you dont have to scan each declaration line for multiple variables
  • Initialization of integers with '0' is not required because '0' is its default.

     private double firstNumber;
     private double secondNumber;
     private double resultNumber;
     private bool calcDone = false;
     private Operations operation = Operations.None;
     private string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
    

result.Content = $"{button.Content}";

can be simplified by

result.Content = button.Content;

There are multiple occurences of result.Content.ToString() == "0". Maybe it makes sense to put it in a property and use the the property instead:

private bool IsInputEmpty => result.Equals("0");

Using MVVM

In WPF it is common to prefer MVVM over code behind. It is nonessential for such an simple application like a calculator - But for pracice it would be a nice exercise to convert it to MMVM using commands and data binding.


Use OOP to abstract your logic and separate state

Your solution consists of big nested switch statements and global state (firstNumber, secondNumber, resultNumber, calcDone, operation). That kind of solutions are really hard to maintain because each case of the switch has access to the whole state, some cases require a defined state and it is not clear what are the transisions from one state to another.

It is better to think about more abstract concepts and try to design them via the power of object oriented programming ;). Actually there are many patterns that have proven themselves.. In your case I would check the State Pattern

For the caluclator, meaningful stats may be

  • EmtpyInput
  • OneNumberEntered
  • OneNumberAndOperatorEntered
  • ResultShown

properties / behavior of states:

  • There is only one active state (the initial state is EmptyInput)
  • Each state has only the required attributes (e.g. EmptyInput has no attributes, OneNumberEntered has the entered number as attribute).
  • Each state can not be created without the attributes it needs. Therefore it is not possible to create invalid states
  • Each state can say which input can be handled and which not (e.g. corresponding buttons can be disabled)
  • Each state can change the active state.

A simple impl. could look like:

public interface IContext
{
    void ChangeState(State state);
}

public abstract class State
{
    public State(IContext context)
    {
        this.Context = context;
    }

    protected IContext Context { get; }

    public abstract void HandleInput(string input);

    public abstract bool CanHandleInput(string input);
}

public class EmptyInput : State
{
    public EmptyInput(IContext context) : base(context)
    {}

    public override bool CanHandleInput(string input) => "0123456789".Contains(input);

    public override void HandleInput(string input)
    {
        this.Context.ChangeState(new OneNumberEntered(context, input));
    }
}

The ViewModel or the MainWindow should implement the IContext interface, hold the active state and pass all input to the active state. That approach is understandable and maintainable because the complexity has been separated in different simple states and the problem of "handling input" can be handled for each state with the corresponding context information separatly. ....

\$\endgroup\$
2
\$\begingroup\$

Inspired by JanDotNet's answer, I have made an attempt at improving the OP's code using best practices. I am using both the M-V-VM and State Machine pattern.

View

The calculator view is no longer a part of MainWindow.

MainWindow

<Window x:Class="Calculator.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:view="clr-namespace:Calculator.Views"
        Title="Calculator" Height="500" Width="300">
    <Grid>
        <view:CalculatorView />
    </Grid>
</Window>

CalculatorView

Styles, commands, command parameters are refactored into best practices. Styles are moved into resource dictionaries. Commands are plumbed with the command pattern of Telerik. Command parameters are type-casted for convenience in order for the view model and model not to parse strings.

<UserControl x:Class="Calculator.Views.CalculatorView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:view="clr-namespace:Calculator.Views"
             xmlns:viewModel="clr-namespace:Calculator.ViewModels"
             xmlns:model="clr-namespace:Calculator.Models"
             mc:Ignorable="d" 
             d:DesignHeight="500" d:DesignWidth="300">
    <Grid Margin="8">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="1.5*"/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Label x:Name="result" Content="0" Grid.ColumnSpan="4" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="50"/>

        <Button Content="AC" Grid.Column="0" Grid.Row="1"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.ClearAll}" />
        <Button Content="+/-" Grid.Column="1" Grid.Row="1"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Negate}" />
        <Button Content="%" Grid.Column="2" Grid.Row="1"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Modulo}" />
        <Button Content="÷" Grid.Column="3" Grid.Row="1"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Divide}" />

        <Button Content="7" Grid.Column="0" Grid.Row="2"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="7" />
        <Button Content="8" Grid.Column="1" Grid.Row="2"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="8" />
        <Button Content="9" Grid.Column="2" Grid.Row="2"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="9" />
        <Button Content="×" Grid.Column="3" Grid.Row="2"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Multiply}" />

        <Button Content="4" Grid.Column="0" Grid.Row="3"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="4" />
        <Button Content="5" Grid.Column="1" Grid.Row="3"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="5" />
        <Button Content="6" Grid.Column="2" Grid.Row="3"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="6" />
        <Button Content="-" Grid.Column="3" Grid.Row="3"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Subtract}" />

        <Button Content="1" Grid.Column="0" Grid.Row="4"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="1" />
        <Button Content="2" Grid.Column="1" Grid.Row="4"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="2" />
        <Button Content="3" Grid.Column="2" Grid.Row="4"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="3" />
        <Button Content="+" Grid.Column="3" Grid.Row="4"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Add}" />

        <Button Content="0" Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2"
                Style="{StaticResource NumericButton}" Command="{Binding NumericCommand}" 
                CommandParameter="0" />
        <Button x:Name="dec" Content="." Grid.Column="2" Grid.Row="5"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.DecimalSeperator}" />
        <Button Content="=" Grid.Column="3" Grid.Row="5"
                Style="{StaticResource OperationButton}" Command="{Binding OperationCommand}" 
                CommandParameter="{x:Static model:Operation.Equals}" />
    </Grid>
</UserControl>

CalculatorView (code-behind)

This is the entrypoint for plumbing the view model and model to the view. The view model attaches itself as Datacontext on the view (inside constructor of the view model).

public partial class CalculatorView : UserControl, ICalculatorView
    {
        private CalculatorViewModel viewModel;

        public CalculatorView() {
            InitializeComponent();
            this.viewModel = new CalculatorViewModel(this, new SimpleCalculator());
        }

        public void Display(string value) {
            result.Content = value;
        }
    }

ViewModel

The view model is responsible for dispatching user events to the model and displaying evaluated data from the model to the view.

CalculatorViewModel

public class CalculatorViewModel : ViewModelBase<ICalculatorView>
    {
        private ICommand operationCommand;
        private ICommand numericCommand;
        private ICalculator calculator;

        public CalculatorViewModel(ICalculatorView view, ICalculator calculator)
            : base(view) 
        {
            Guard.NotNull(calculator);
            this.calculator = calculator;
        }

        protected virtual void OnOperationCommand(Operation operation) {
            calculator.HandleOperationEvent(operation);
            View.Display(calculator.CurrentToken);
        }

        protected virtual void OnNumericCommand(int digit) {
            calculator.HandleNumericEvent(digit);
            View.Display(calculator.CurrentToken);
        }

        public ICommand OperationCommand {
            get {
                return this.operationCommand ?? (this.operationCommand
                    = new RelayCommand<Operation>(this.OnOperationCommand));
            }
        }

        public ICommand NumericCommand {
            get {
                return this.numericCommand ?? (this.numericCommand
                    = new RelayCommand<int>(this.OnNumericCommand));
            }
        }
    }

Model

The model is a simple calculator that uses states to process user input. Note that this part is the most complex and could still get improved:

  • error handling (perhaps an error state)
  • unit tests (state machines tend to have alot of edge cases!)

SimpleCalculator

    /// <remarks>
    /// Implemented as a simple subset of an UML state machine: https://en.wikipedia.org/wiki/UML_state_machine
    /// with event processing, extended state and mealy/moore operations.
    /// </remarks>
    public class SimpleCalculator : ICalculator
    {
        public SimpleCalculator() {
            Transition(new InitialState(this));
        }

        #region Event Processing

        public void HandleOperationEvent(Operation operation) {
            state.HandleOperationEvent(operation);
        }

        public void HandleNumericEvent(int digit) {
            state.HandleNumericEvent(digit);
        }

        public Operation DeferredOperation { get; internal set; }

        #endregion

        #region Extended State

        // only important if you want to send the evaluated result to some other API
        public decimal Result {
            get;
            internal set;
        }

        // this is what will be outputted on the view after each user input event
        public string CurrentToken {
            get;
            internal set;
        }

        #endregion

        #region State Machine

        private State state;

        internal void Transition(State newState) {
            Guard.NotNull(newState);
            if (state != null)
                state.Exit();
            state = newState;
            state.Enter();
        }

        protected decimal CurrentResult {
            get {
                return string.IsNullOrEmpty(CurrentToken) ? 0m
                    : decimal.Parse(CurrentToken.EndsWith(State.DecimalSeperator) 
                    ? CurrentToken.Substring(0, CurrentToken.Length - 1) : CurrentToken,
                    State.Formatter);
            }
        }

        protected void Negate() {
            if (CurrentResult != 0) {
                if (CurrentToken.StartsWith(State.NegateSymbol)) {
                    CurrentToken = CurrentToken.Substring(1);
                }
                else {
                    CurrentToken = State.NegateSymbol + CurrentToken;
                }
            }
        }

        protected void HandleDeferredOperation() {
            switch (DeferredOperation) {
                case Operation.Add:
                    Result += CurrentResult;
                    break;
                case Operation.Subtract:
                    Result -= CurrentResult;
                    break;
                case Operation.Multiply:
                    Result *= CurrentResult;
                    break;
                case Operation.Divide:
                    Result /= CurrentResult;
                    break;
                case Operation.Modulo:
                    Result %= CurrentResult;
                    break;
                default:
                    break;
            }
            Result = Math.Round(Result, 8);
            DeferredOperation = Operation.None;
            CurrentToken = Result.ToString(State.Formatter);
        }

        internal abstract class State
        {
            protected SimpleCalculator context;
            internal const string DecimalSeperator = ".";
            internal const string NegateSymbol = "-";
            internal readonly static IFormatProvider Formatter = new CultureInfo("en-US", false);

            protected internal State(SimpleCalculator context) {
                Guard.NotNull(context);
                this.context = context;
            }

            protected void ChangeState(State newState) {
                context.Transition(newState);
            }

            protected void ChangeToDecimalInputState(Operation operation) {
                var decimalState = new DecimalInputState(context);
                ChangeState(decimalState);
                decimalState.HandleOperationEvent(operation);
            }

            protected void ChangeToNumericInputState(int digit) {
                var newState = new NumericInputState(context);
                ChangeState(newState);
                newState.HandleNumericEvent(digit);
            }

            protected void ChangeToBinaryOperationState(Operation operation) {
                var binaryState = new BinaryOperationState(context);
                ChangeState(binaryState);
                binaryState.HandleOperationEvent(operation);
            }

            internal virtual bool HandleOperationEvent(Operation operation) 
            {
                switch (operation) {
                    case Operation.ClearAll:
                        ChangeState(new InitialState(context));
                        return true;
                    case Operation.Equals:
                        ChangeState(new EqualState(context));
                        return true;
                    case Operation.DecimalSeperator:
                        ChangeToDecimalInputState(operation);
                        return true;
                    case Operation.Add:
                    case Operation.Subtract:
                    case Operation.Multiply:
                    case Operation.Divide:
                    case Operation.Modulo:
                        ChangeToBinaryOperationState(operation);
                        return true;
                    case Operation.Negate:
                        context.Negate();
                        return true;
                }

                return false;
            }

            internal virtual bool HandleNumericEvent(int digit) {
                this.ChangeToNumericInputState(digit);
                return true; 
            }

            internal virtual void Enter() {}
            internal virtual void Exit() { }
        }

        internal sealed class InitialState : State
        {
            internal InitialState(SimpleCalculator context)
                : base(context) {
            }

            internal override bool HandleOperationEvent(Operation operation) {
                switch (operation) {
                    case Operation.ClearAll:
                        // let's stay in the current state, rather then triggering an
                        // external self transition
                        return true;
                    default:
                        return base.HandleOperationEvent(operation);
                }
            }

            internal override void Enter() {
                context.Result = 0m;
                context.CurrentToken = string.Empty;
                context.DeferredOperation = Operation.None;
            }
        }

        private sealed class EqualState : State
        {
            internal EqualState(SimpleCalculator context)
                : base(context) {
            }

            internal override void Enter() {
                context.HandleDeferredOperation();
            }

            internal override bool HandleOperationEvent(Operation operation) {
                switch (operation) {
                    case Operation.Negate:
                        // ignore
                        return true;
                    default:
                        return base.HandleOperationEvent(operation);
                }
            }
        }

        internal sealed class DecimalInputState : State
        {
            internal DecimalInputState(SimpleCalculator context)
                : base(context) {
            }

            internal override bool HandleOperationEvent(Operation operation) {
                switch (operation) {
                    case Operation.DecimalSeperator:
                        // ignore attempt to set another decimal seperator
                        if (!string.IsNullOrEmpty(context.CurrentToken)
                            && !context.CurrentToken.Contains(State.DecimalSeperator)) 
                        {
                            context.CurrentToken += State.DecimalSeperator;
                        }
                        return true;
                    default:
                        return base.HandleOperationEvent(operation);
                }
            }

            internal override bool HandleNumericEvent(int digit) {
                if (context.CurrentResult == 0) {
                    // only output the decimal seperator once a number event is received
                    context.CurrentToken = "0";
                }
                if (!context.CurrentToken.Contains(State.DecimalSeperator)) {
                    context.CurrentToken += State.DecimalSeperator;
                }
                // append the digit to the current token
                context.CurrentToken += digit.ToString();
                return true;
            }

            internal override void Enter() {
                context.CurrentToken = string.Empty;
                base.Enter();
            }

            internal override void Exit() {
                if (context.DeferredOperation == Operation.None) {
                    context.Result = 0m;
                    context.DeferredOperation = Operation.Add;
                }
            }
        }

        private sealed class NumericInputState : State
        {
            internal NumericInputState(SimpleCalculator context)
                : base(context) {
            }

            internal override bool HandleNumericEvent(int digit) {
                if (context.CurrentResult == 0 && digit == 0) {
                    // ignore 0 as prepend
                    return true;
                }
                // append the digit to the current token
                context.CurrentToken += digit.ToString();
                return true;
            }

            internal override void Enter() {
                context.CurrentToken = string.Empty;
                base.Enter();
            }

            internal override void Exit() {
                if (context.DeferredOperation == Operation.None) {
                    context.Result = 0m;
                    context.DeferredOperation = Operation.Add;
                }
            }
        }

        private sealed class BinaryOperationState : State
        {
            internal BinaryOperationState(SimpleCalculator context)
                : base(context) {
            }

            internal override void Enter() {
                context.HandleDeferredOperation();
            }

            internal override bool HandleOperationEvent(Operation operation) {
                switch (operation) {
                    case Operation.Negate:
                        // ignore
                        return true;
                    case Operation.Add:
                    case Operation.Subtract:
                    case Operation.Multiply:
                    case Operation.Divide:
                    case Operation.Modulo:
                        // store the deferred event
                        context.DeferredOperation = operation;
                        return true;
                    default:
                        return base.HandleOperationEvent(operation);
                }
            }
        }

        #endregion
    }

Infrastructure

An appendix of other classes and resources used.

public enum Operation
    {
        None,
        ClearAll,
        Negate,
        Modulo,
        Divide,
        Multiply,
        Subtract,
        Add,
        DecimalSeperator,
        Equals,
    }

public interface ICalculator
    {
        void HandleOperationEvent(Operation operation);
        void HandleNumericEvent(int digit);
        decimal Result { get; }
        string CurrentToken { get; }
    }

public interface ICalculatorViewModel
    {
        ICalculatorView View { get; }
    }

public interface ICalculatorView : IView
    {
        void Display(string value);
    }

public interface IViewModel : INotifyPropertyChanged, IDisposable
    {
        IView View { get; }
    }

public interface IViewModel<TView> : IViewModel where TView : IView
    {
        new TView View { get; }
    }

public interface IView
    {
        object DataContext { get; set; }
    }

public abstract class ViewModelBase<TView> : IViewModel<TView>
        where TView : IView
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private readonly TView view;

        public TView View {
            get {
                return this.view;
            }
        }

        protected ViewModelBase(TView view) {
            Guard.NotNull(view);
            this.view = view;
            this.View.DataContext = this;
        }

        protected virtual void RaisePropertyChanged(string propertyName) {
            var handler = this.PropertyChanged;
            if (handler != null) {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        IView IViewModel.View {
            get { return View; }
        }

        ~ViewModelBase() {
            Dispose(false);
        }

        public void Dispose() {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing) {
            if (disposing) {
                this.PropertyChanged = null;
            }
        }
    }

public class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged {
            add { 
                CommandManager.RequerySuggested += value;
            }
            remove { 
                CommandManager.RequerySuggested -= value; 
            }
        }

        private Action methodToExecute;
        private Func<bool> canExecuteEvaluator;

        public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator) {
            Guard.NotNull(methodToExecute);
            this.methodToExecute = methodToExecute;
            this.canExecuteEvaluator = canExecuteEvaluator;
        }

        public RelayCommand(Action methodToExecute)
            : this(methodToExecute, null) {
        }

        public bool CanExecute(object parameter) {
            return this.canExecuteEvaluator == null || this.canExecuteEvaluator.Invoke();
        }

        public void Execute(object parameter) {
            this.methodToExecute.Invoke();
        }
    }

    public class RelayCommand<TParameter> : ICommand
    {
        public event EventHandler CanExecuteChanged {
            add {
                CommandManager.RequerySuggested += value;
            }
            remove {
                CommandManager.RequerySuggested -= value;
            }
        }

        private Action<TParameter> methodToExecute;
        private Func<TParameter, bool> canExecuteEvaluator;

        public RelayCommand(Action<TParameter> methodToExecute, Func<TParameter, bool> canExecuteEvaluator) {
            Guard.NotNull(methodToExecute);
            this.methodToExecute = methodToExecute;
            this.canExecuteEvaluator = canExecuteEvaluator;
        }

        public RelayCommand(Action<TParameter> methodToExecute)
            : this(methodToExecute, null) {
        }

        private TParameter ConvertType(object parameter) {
          return parameter == null ? default(TParameter) 
              : (TParameter)Convert.ChangeType(parameter, typeof(TParameter));
        }

        public bool CanExecute(object parameter) {
            return this.canExecuteEvaluator == null
                || this.canExecuteEvaluator.Invoke(ConvertType(parameter));
        }

        public void Execute(object parameter) {
            this.methodToExecute.Invoke(ConvertType(parameter));
        }
    }

internal static class Guard
    {
        [DebuggerStepThrough]
        public static void NotNull(object value) {
            if (value == null) {
                throw new ArgumentNullException("The value must be set.");
            }
        }
    }

AppResources.xaml ->

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Style.xaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

Style.xaml ->

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style x:Key="CalculatorButton" TargetType="{x:Type Button}">
        <Setter Property="Margin" Value="2" />
    </Style>

    <Style x:Key="OperationButton" TargetType="{x:Type Button}" BasedOn="{StaticResource CalculatorButton}">
        <Setter Property="FontWeight" Value="bold" />
    </Style>

    <Style x:Key="NumericButton" TargetType="{x:Type Button}" BasedOn="{StaticResource CalculatorButton}">
        <Setter Property="FontSize" Value="20" />
    </Style>

</ResourceDictionary>

App.xaml ->

<Application x:Class="Calculator.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary Source="Resources/AppResources.xaml" />
    </Application.Resources>
</Application>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.