Building a Professional Calculator App with Avalonia UI and Community Toolkit MVVM

Ready to put your Avalonia skills to the test? Let’s build a fully functional calculator that showcases modern MVVM patterns and sleek UI design.

Welcome back to our Avalonia UI series! Now that you’ve mastered the basics, it’s time to build something practical and impressive. Today we’re creating a calculator that not only works flawlessly but also demonstrates professional development patterns you’ll use in real-world applications.

This isn’t just another “hello world” tutorial, we’re building production-quality code that you can proudly show in your portfolio.

What You’ll Build Today

Our calculator will feature:

  • Clean, modern UI with smooth animations
  • Proper MVVM architecture using Community Toolkit
  • Full arithmetic operations with memory functions
  • Error handling and input validation
  • Keyboard shortcuts for desktop users
  • Professional styling that looks native on every platform

Designing the Calculator Interface That Users Actually Want

First, let’s create a calculator that doesn’t look like it was designed in the 90s. Replace the contents of Views/MainWindow.axaml:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:vm="using:ModernCalculator.ViewModels"
        x:Class="ModernCalculator.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="Calculator"
        Width="350" Height="550"
        MinWidth="350" MinHeight="550"
        MaxWidth="350" MaxHeight="550"
        Background="#1E1E1E"
        WindowStartupLocation="CenterScreen">
    
    <Window.KeyBindings>
        <KeyBinding Command="{Binding ClearCommand}" Gesture="Escape" />
        <KeyBinding Command="{Binding EqualsCommand}" Gesture="Enter" />
        <KeyBinding Command="{Binding BackspaceCommand}" Gesture="Back" />
        <KeyBinding
            Command="{Binding OperationCommand}"
            CommandParameter="+"
            Gesture="OemPlus" />
        <KeyBinding
            Command="{Binding OperationCommand}"
            CommandParameter="-"
            Gesture="OemMinus" />
        <KeyBinding
            Command="{Binding OperationCommand}"
            CommandParameter="×"
            Gesture="Multiply" />
        <KeyBinding
            Command="{Binding OperationCommand}"
            CommandParameter="/"
            Gesture="Divide" />

        <KeyBinding Command="{Binding DecimalCommand}" Gesture="OemPeriod" />

        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="0"
            Gesture="NumPad0" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="1"
            Gesture="NumPad1" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="2"
            Gesture="NumPad2" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="3"
            Gesture="NumPad3" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="4"
            Gesture="NumPad4" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="5"
            Gesture="NumPad5" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="6"
            Gesture="NumPad6" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="7"
            Gesture="NumPad7" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="8"
            Gesture="NumPad8" />
        <KeyBinding
            Command="{Binding NumberCommand}"
            CommandParameter="9"
            Gesture="NumPad9" />
    </Window.KeyBindings>

    <Grid Margin="15" RowDefinitions="160,*">

        <!--  Display Section  -->
        <Border
            Background="#2B2B2B"
            BoxShadow="0 4 20 0 #00000040"
            CornerRadius="12"
            Grid.Row="0"
            Padding="20">

            <Grid RowDefinitions="Auto,Auto,*">
                <!--  Memory Indicator  -->
                <TextBlock
                    FontSize="12"
                    Foreground="#888"
                    Grid.Row="0"
                    HorizontalAlignment="Left"
                    IsVisible="{Binding HasMemoryValue}"
                    Text="{Binding MemoryIndicator}" />

                <!--  Previous Operation  -->
                <TextBlock
                    FontSize="16"
                    Foreground="#999"
                    Grid.Row="1"
                    HorizontalAlignment="Right"
                    Margin="0,5"
                    Text="{Binding PreviousOperation}" />

                <!--  Current Display  -->
                <Viewbox
                    Grid.Row="2"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom">
                    <TextBlock
                        FontSize="48"
                        FontWeight="Light"
                        Foreground="White"
                        MaxWidth="300"
                        Text="{Binding DisplayText}" />
                </Viewbox>
            </Grid>
        </Border>

        <!--  Button Grid  -->
        <Grid
            ColumnDefinitions="*,*,*,*"
            Grid.Row="1"
            Margin="0,20,0,0"
            RowDefinitions="*,*,*,*,*,*">

            <!--  Memory Functions Row  -->
            <Button
                Classes="memory"
                Command="{Binding MemoryClearCommand}"
                Content="MC"
                Grid.Column="0"
                Grid.Row="0" />
            <Button
                Classes="memory"
                Command="{Binding MemoryRecallCommand}"
                Content="MR"
                Grid.Column="1"
                Grid.Row="0" />
            <Button
                Classes="memory"
                Command="{Binding MemoryAddCommand}"
                Content="M+"
                Grid.Column="2"
                Grid.Row="0" />
            <Button
                Classes="memory"
                Command="{Binding MemorySubtractCommand}"
                Content="M-"
                Grid.Column="3"
                Grid.Row="0" />

            <!--  Function Row  -->
            <Button
                Classes="function"
                Command="{Binding ClearCommand}"
                Content="C"
                Grid.Column="0"
                Grid.Row="1" />
            <Button
                Classes="function"
                Command="{Binding ClearEntryCommand}"
                Content="CE"
                Grid.Column="1"
                Grid.Row="1" />
            <Button
                Classes="function"
                Command="{Binding BackspaceCommand}"
                Content="⌫"
                Grid.Column="2"
                Grid.Row="1" />
            <Button
                Classes="operator"
                Command="{Binding OperationCommand}"
                CommandParameter="÷"
                Content="÷"
                Grid.Column="3"
                Grid.Row="1" />

            <!--  Number Rows  -->
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="7"
                Content="7"
                Grid.Column="0"
                Grid.Row="2" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="8"
                Content="8"
                Grid.Column="1"
                Grid.Row="2" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="9"
                Content="9"
                Grid.Column="2"
                Grid.Row="2" />
            <Button
                Classes="operator"
                Command="{Binding OperationCommand}"
                CommandParameter="×"
                Content="×"
                Grid.Column="3"
                Grid.Row="2" />

            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="4"
                Content="4"
                Grid.Column="0"
                Grid.Row="3" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="5"
                Content="5"
                Grid.Column="1"
                Grid.Row="3" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="6"
                Content="6"
                Grid.Column="2"
                Grid.Row="3" />
            <Button
                Classes="operator"
                Command="{Binding OperationCommand}"
                CommandParameter="-"
                Content="-"
                Grid.Column="3"
                Grid.Row="3" />

            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="1"
                Content="1"
                Grid.Column="0"
                Grid.Row="4" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="2"
                Content="2"
                Grid.Column="1"
                Grid.Row="4" />
            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="3"
                Content="3"
                Grid.Column="2"
                Grid.Row="4" />
            <Button
                Classes="operator"
                Command="{Binding OperationCommand}"
                CommandParameter="+"
                Content="+"
                Grid.Column="3"
                Grid.Row="4" />

            <Button
                Classes="number"
                Command="{Binding NumberCommand}"
                CommandParameter="0"
                Content="0"
                Grid.Column="0"
                Grid.ColumnSpan="2"
                Grid.Row="5"
                Width="130" />
            <Button
                Classes="number"
                Command="{Binding DecimalCommand}"
                Content="."
                Grid.Column="2"
                Grid.Row="5" />
            <Button
                Classes="equals"
                Command="{Binding EqualsCommand}"
                Content="="
                Grid.Column="3"
                Grid.Row="5" />
        </Grid>
    </Grid>
</Window>

Creating Professional Button Styles

Add these styles to your App.axaml file inside the <Application.Styles> section:

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="ModernCalculator.App"
             RequestedThemeVariant="Dark">
             
    <Application.Styles>
        <FluentTheme />
        
        <!--  Base Button Style  -->
        <Style Selector="Button">
            <Setter Property="Width" Value="50" />
            <Setter Property="Height" Value="50" />
            <Setter Property="HorizontalContentAlignment" Value="Center" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="CornerRadius" Value="30" />
            <Setter Property="Margin" Value="3" />
            <Setter Property="FontSize" Value="18" />
            <Setter Property="FontWeight" Value="Medium" />
            <Setter Property="BorderThickness" Value="0" />
            <Setter Property="Cursor" Value="Hand" />
        </Style>

        <Style Selector="Button:pointerover">
            <Setter Property="RenderTransform" Value="scale(1.05)" />
        </Style>

        <Style Selector="Button:pressed">
            <Setter Property="RenderTransform" Value="scale(0.95)" />
        </Style>

        <!--  Number Button Style  -->
        <Style Selector="Button.number">
            <Setter Property="Background" Value="#333333" />
            <Setter Property="Foreground" Value="White" />

        </Style>

        <Style Selector="Button.number:pointerover /template/ ContentPresenter">
            <Setter Property="Background" Value="#404040" />
            <Setter Property="Foreground" Value="White" />

        </Style>

        <!--  Zero Button (wider)  -->
        <Style Selector="Button.zero">
            <Setter Property="Padding" Value="25,0" />
        </Style>

        <!--  Operator Button Style  -->
        <Style Selector="Button.operator">
            <Setter Property="Background" Value="#FF9500" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontSize" Value="24" />
        </Style>

        <Style Selector="Button.operator:pointerover /template/ ContentPresenter">
            <Setter Property="Background" Value="#FFB143" />
            <Setter Property="Foreground" Value="White" />

        </Style>

        <!--  Function Button Style  -->
        <Style Selector="Button.function">
            <Setter Property="Background" Value="#A6A6A6" />
            <Setter Property="Foreground" Value="Black" />
        </Style>

        <Style Selector="Button.function:pointerover /template/ ContentPresenter">
            <Setter Property="Background" Value="#BFBFBF" />
            <Setter Property="Foreground" Value="Black" />

        </Style>

        <!--  Memory Button Style  -->
        <Style Selector="Button.memory">
            <Setter Property="Background" Value="#1E3A8A" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontSize" Value="14" />
        </Style>

        <Style Selector="Button.memory:pointerover /template/ ContentPresenter">
            <Setter Property="Background" Value="#3B82F6" />
            <Setter Property="Foreground" Value="White" />

        </Style>

        <!--  Equals Button Style  -->
        <Style Selector="Button.equals">
            <Setter Property="Background" Value="#FF9500" />
            <Setter Property="Foreground" Value="White" />
            <Setter Property="FontSize" Value="24" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>

        <Style Selector="Button.equals:pointerover /template/ ContentPresenter">
            <Setter Property="Background" Value="#FFB143" />
            <Setter Property="Foreground" Value="White" />
        </Style>
    </Application.Styles>
</Application>

Building the Calculator Brain with Community Toolkit MVVM

Now for the fun part – let’s create the ViewModel that makes everything work. Replace the contents of ViewModels/MainWindowViewModel.cs:

using System;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ModernCalculator.ViewModels;

public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty]
    private string displayText = "0";
    
    [ObservableProperty]
    private string previousOperation = "";
    
    [ObservableProperty]
    private string memoryIndicator = "";
    
    [ObservableProperty]
    private bool hasMemoryValue = false;
    
    private double currentValue = 0;
    private double previousValue = 0;
    private double memoryValue = 0;
    private string currentOperation = "";
    private bool isNewEntry = true;
    private bool hasDecimalPoint = false;
    private bool lastActionWasEquals = false;

    [RelayCommand]
    private void Number(string number)
    {
        if (lastActionWasEquals)
        {
            Clear();
            lastActionWasEquals = false;
        }

        if (isNewEntry || DisplayText == "0")
        {
            DisplayText = number;
            isNewEntry = false;
            hasDecimalPoint = false;
        }
        else
        {
            if (DisplayText.Length < 15) // Prevent overflow
            {
                DisplayText += number;
            }
        }
    }

    [RelayCommand]
    private void Decimal()
    {
        if (lastActionWasEquals)
        {
            Clear();
            lastActionWasEquals = false;
        }

        if (isNewEntry)
        {
            DisplayText = "0.";
            isNewEntry = false;
            hasDecimalPoint = true;
        }
        else if (!hasDecimalPoint)
        {
            DisplayText += ".";
            hasDecimalPoint = true;
        }
    }

    [RelayCommand]
    private void Operation(string operation)
    {
        if (!isNewEntry && !string.IsNullOrEmpty(currentOperation) && !lastActionWasEquals)
        {
            Equals();
        }

        if (double.TryParse(DisplayText, out double value))
        {
            previousValue = value;
        }

        currentOperation = operation;
        PreviousOperation = $"{FormatNumber(previousValue)} {operation}";
        isNewEntry = true;
        lastActionWasEquals = false;
    }

    [RelayCommand]
    private void Equals()
    {
        if (string.IsNullOrEmpty(currentOperation)) return;

        if (double.TryParse(DisplayText, out double value))
        {
            currentValue = value;
        }

        try
        {
            var result = currentOperation switch
            {
                "+" => previousValue + currentValue,
                "-" => previousValue - currentValue,
                "×" => previousValue * currentValue,
                "÷" => currentValue != 0 ? previousValue / currentValue : throw new DivideByZeroException(),
                _ => currentValue
            };

            DisplayText = FormatNumber(result);
            PreviousOperation = $"{FormatNumber(previousValue)} {currentOperation} {FormatNumber(currentValue)} =";
        }
        catch (DivideByZeroException)
        {
            DisplayText = "Cannot divide by zero";
            PreviousOperation = "";
        }
        catch (OverflowException)
        {
            DisplayText = "Overflow";
            PreviousOperation = "";
        }

        currentOperation = "";
        isNewEntry = true;
        lastActionWasEquals = true;
    }

    [RelayCommand]
    private void Clear()
    {
        DisplayText = "0";
        currentValue = 0;
        previousValue = 0;
        currentOperation = "";
        PreviousOperation = "";
        isNewEntry = true;
        hasDecimalPoint = false;
        lastActionWasEquals = false;
    }

    [RelayCommand]
    private void ClearEntry()
    {
        DisplayText = "0";
        isNewEntry = true;
        hasDecimalPoint = false;
    }

    [RelayCommand]
    private void Backspace()
    {
        if (DisplayText.Length > 1 && DisplayText != "0")
        {
            if (DisplayText.EndsWith("."))
            {
                hasDecimalPoint = false;
            }
            DisplayText = DisplayText[..^1];
        }
        else
        {
            DisplayText = "0";
            isNewEntry = true;
            hasDecimalPoint = false;
        }
    }

    // Memory Functions
    [RelayCommand]
    private void MemoryClear()
    {
        memoryValue = 0;
        HasMemoryValue = false;
        MemoryIndicator = "";
    }

    [RelayCommand]
    private void MemoryRecall()
    {
        DisplayText = FormatNumber(memoryValue);
        isNewEntry = true;
        hasDecimalPoint = DisplayText.Contains(".");
    }

    [RelayCommand]
    private void MemoryAdd()
    {
        if (double.TryParse(DisplayText, out double value))
        {
            memoryValue += value;
            UpdateMemoryIndicator();
        }
    }

    [RelayCommand]
    private void MemorySubtract()
    {
        if (double.TryParse(DisplayText, out double value))
        {
            memoryValue -= value;
            UpdateMemoryIndicator();
        }
    }

    private void UpdateMemoryIndicator()
    {
        HasMemoryValue = memoryValue != 0;
        MemoryIndicator = HasMemoryValue ? "M" : "";
    }

    private static string FormatNumber(double number)
    {
        // Handle special cases
        if (double.IsNaN(number) || double.IsInfinity(number))
            return "Error";

        // Format with appropriate decimal places
        if (Math.Abs(number) < 1e-10)
            return "0";

        // For very large or very small numbers, use scientific notation
        if (Math.Abs(number) >= 1e15 || (Math.Abs(number) < 1e-4 && number != 0))
            return number.ToString("E2", CultureInfo.InvariantCulture);

        // For normal numbers, remove unnecessary decimal places
        string formatted = number.ToString("0.##########", CultureInfo.InvariantCulture);
        
        // Limit display length
        if (formatted.Length > 15)
        {
            formatted = number.ToString("E2", CultureInfo.InvariantCulture);
        }

        return formatted;
    }
}

Advanced Features That Make Your Calculator Stand Out

Keyboard Support for Desktop Users

Notice how we added keyboard bindings in the XAML? This makes your calculator feel native on desktop platforms. Users can:

  • Use number pad keys for input
  • Press Enter for equals
  • Press Escape to clear
  • Use standard keyboard shortcuts

Memory Functions That Actually Work

Our memory functions (MC, MR, M+, M-) work just like a real calculator:

  • MC: Clear memory
  • MR: Recall from memory
  • M+: Add current display to memory
  • M-: Subtract current display from memory

Error Handling That Doesn’t Crash

Professional apps handle errors gracefully. Our calculator:

  • Prevents division by zero
  • Handles overflow situations
  • Limits input length to prevent UI breaking
  • Shows meaningful error messages

Testing Your Calculator Like a Pro

Before we call it done, let’s test thoroughly:

# Run the application
dotnet run

Test these scenarios:

  1. Basic operations: 2 + 2, 10 × 5, 15 ÷ 3
  2. Chain operations: 2 + 3 × 4 (tests operator precedence)
  3. Decimal handling: 3.14 × 2.5
  4. Edge cases: Division by zero, very large numbers
  5. Memory functions: Store 100, add 50, recall
  6. Keyboard shortcuts: Try using the number pad

Common Beginner Mistakes (And How We Avoided Them)

Mistake #1: Not Handling User Input Validation

What we did right: Added length limits and format validation in our NumberCommand.

Mistake #2: Ignoring Error Cases

What we did right: Implemented try-catch blocks and meaningful error messages.

Mistake #3: Poor State Management

What we did right: Used clear flags like isNewEntry and lastActionWasEquals to track calculator state.

Mistake #4: Forgetting About User Experience

What we did right: Added animations, keyboard shortcuts, and visual feedback.

Making Your Calculator Production-Ready

Want to take this further? Here are professional enhancements:

Add Unit Tests

[Test]
public void Addition_ShouldReturnCorrectResult()
{
    var viewModel = new MainWindowViewModel();
    viewModel.NumberCommand.Execute("2");
    viewModel.OperationCommand.Execute("+");
    viewModel.NumberCommand.Execute("3");
    viewModel.EqualsCommand.Execute(null);
    
    Assert.That(viewModel.DisplayText, Is.EqualTo("5"));
}

Implement Settings and Themes

  • Light/dark mode toggle
  • Sound effects for button clicks
  • History of calculations

Add Scientific Calculator Mode

  • Trigonometric functions
  • Logarithms and exponentials
  • Parentheses for complex expressions

Publishing Your Calculator to Different Platforms

Ready to share your creation? Here’s how to build for each platform:

# Build for Linux
dotnet publish -c Release -r linux-x64 --self-contained

# Build for Windows
dotnet publish -c Release -r win-x64 --self-contained

# Build for macOS
dotnet publish -c Release -r osx-x64 --self-contained

What You’ve Accomplished (And Why It Matters)

Congratulations! You’ve built a professional-grade calculator that demonstrates:

  • Modern MVVM architecture with Community Toolkit
  • Professional UI design with custom styling and animations
  • Robust error handling that prevents crashes
  • Cross-platform compatibility that works everywhere
  • Advanced features like memory functions and keyboard shortcuts

More importantly, you’ve learned patterns that scale to enterprise applications. The MVVM structure, error handling, and user experience principles you’ve implemented here are the same ones used in banking apps, productivity software, and business applications.

Mohammed Chami
Mohammed Chami
Articles: 45

Leave a Reply

Your email address will not be published. Required fields are marked *