Mohammed Chami
.NET Developer | Content Creator
Mohammed Chami
.NET Developer | Content Creator

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.

Our calculator will feature:
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>
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>
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;
}
}
Notice how we added keyboard bindings in the XAML? This makes your calculator feel native on desktop platforms. Users can:
Our memory functions (MC, MR, M+, M-) work just like a real calculator:
Professional apps handle errors gracefully. Our calculator:
Before we call it done, let’s test thoroughly:
# Run the application
dotnet run
Test these scenarios:
What we did right: Added length limits and format validation in our NumberCommand.
What we did right: Implemented try-catch blocks and meaningful error messages.
What we did right: Used clear flags like isNewEntry and lastActionWasEquals to track calculator state.
What we did right: Added animations, keyboard shortcuts, and visual feedback.
Want to take this further? Here are professional enhancements:
[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"));
}
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
Congratulations! You’ve built a professional-grade calculator that demonstrates:
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.