A. Struktura rozwiązania
Solution: ProstyKalkulatorPodTesty
- WPF App
ProstyKalkulatorPodTesty(target:net8.0-windows)
Foldery:ModeleLogikaWalidacja
Pliki systemowe:App.xaml,MainWindow.xaml,MainWindow.xaml.cs
- Testy
ProstyKalkulatorPodTesty.Tests(target:net8.0)
Referencja do projektu WPF (korzystamy z klas Modele/Logika/Walidacja).
B. Kod aplikacji
App.xaml
<Application x:Class="ProstyKalkulatorPodTesty.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="0,4,0,4"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Margin" Value="0,4,0,4"/>
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="0,8,0,4"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="Height" Value="34"/>
</Style>
</Application.Resources>
</Application>
MainWindow.xaml
<Window x:Class="ProstyKalkulatorPodTesty.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Prosty Kalkulator (MVVM)" Height="440" Width="540"
WindowStartupLocation="CenterScreen">
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- A -->
<RowDefinition Height="Auto"/> <!-- B -->
<RowDefinition Height="Auto"/> <!-- Operacja -->
<RowDefinition Height="Auto"/> <!-- Oblicz + Wynik -->
<RowDefinition Height="*"/> <!-- Komunikaty + Historia -->
</Grid.RowDefinitions>
<!-- Wiersz 0: Liczba A -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Liczba A:" VerticalAlignment="Center"/>
<TextBox Grid.Column="2"
Text="{Binding LiczbaA, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<!-- Wiersz 1: Liczba B -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Liczba B:" VerticalAlignment="Center"/>
<TextBox Grid.Column="2"
Text="{Binding LiczbaB, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<!-- Wiersz 2: Operacja -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Operacja:" VerticalAlignment="Center"/>
<ComboBox Grid.Column="2"
ItemsSource="{Binding Operacje}"
SelectedItem="{Binding WybranaOperacja}"/>
</Grid>
<!-- Wiersz 3: Oblicz + wynik -->
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="0,8,0,0">
<Button Content="Oblicz" Command="{Binding ObliczPolecenie}" Margin="0,0,12,0"/>
<TextBlock VerticalAlignment="Center">
<Run Text="Wynik: "/>
<!-- KLUCZOWE: WynikTekst jest tylko do odczytu -> Mode=OneWay -->
<Run Text="{Binding WynikTekst, Mode=OneWay}"/>
</TextBlock>
</StackPanel>
<!-- Wiersz 4: Komunikat + historia -->
<StackPanel Grid.Row="4" Margin="0,10,0,0">
<TextBlock Text="{Binding KomunikatBledu}" Foreground="Tomato" FontWeight="Bold"/>
<TextBlock Text="Historia:" FontWeight="SemiBold" Margin="0,8,0,4"/>
<ListView ItemsSource="{Binding Historia}">
<ListView.View>
<GridView>
<GridViewColumn Header="Czas" Width="120" DisplayMemberBinding="{Binding CzasFormat}"/>
<GridViewColumn Header="Działanie" Width="240" DisplayMemberBinding="{Binding Opis}"/>
<GridViewColumn Header="Wynik" Width="120" DisplayMemberBinding="{Binding WynikFormat}"/>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using ProstyKalkulatorPodTesty.Logika;
namespace ProstyKalkulatorPodTesty;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new LogikaWidokModel(new LogikaKalkulator());
}
}
Folder: Modele
ModeleOperacja.cs
namespace ProstyKalkulatorPodTesty.Modele;
public enum ModeleOperacja
{
Dodawanie,
Odejmowanie,
Mnożenie,
Dzielenie
}
ModeleDane.cs – wpis do historii
using System.Globalization;
namespace ProstyKalkulatorPodTesty.Modele;
public sealed class ModeleDane
{
public DateTime Czas { get; init; } = DateTime.Now;
public double A { get; init; }
public double B { get; init; }
public ModeleOperacja Operacja { get; init; }
public double Wynik { get; init; }
public string CzasFormat => Czas.ToString("HH:mm:ss");
public string WynikFormat => Wynik.ToString(CultureInfo.CurrentCulture);
public string Opis
{
get
{
var znak = Operacja switch
{
ModeleOperacja.Dodawanie => "+",
ModeleOperacja.Odejmowanie => "-",
ModeleOperacja.Mnożenie => "×",
ModeleOperacja.Dzielenie => "÷",
_ => "?"
};
return $"{A.ToString(CultureInfo.CurrentCulture)} {znak} {B.ToString(CultureInfo.CurrentCulture)}";
}
}
}
Folder: Walidacja
WalidacjaKalkulator.cs
using System.Globalization;
namespace ProstyKalkulatorPodTesty.Walidacja;
public static class WalidacjaKalkulator
{
public static bool SprobujParsowac(string? tekstA, string? tekstB,
out double a, out double b, out string komunikatBledu)
{
a = 0; b = 0; komunikatBledu = string.Empty;
if (string.IsNullOrWhiteSpace(tekstA) || string.IsNullOrWhiteSpace(tekstB))
{
komunikatBledu = "Podaj obie liczby.";
return false;
}
var culture = CultureInfo.CurrentCulture;
if (!double.TryParse(tekstA.Trim(), NumberStyles.Float, culture, out a))
{
komunikatBledu = $"Niepoprawna liczba A: „{tekstA}”.";
return false;
}
if (!double.TryParse(tekstB.Trim(), NumberStyles.Float, culture, out b))
{
komunikatBledu = $"Niepoprawna liczba B: „{tekstB}”.";
return false;
}
return true;
}
public static bool SprawdzDzielenie(double b, out string komunikatBledu)
{
if (b == 0)
{
komunikatBledu = "Nie można dzielić przez zero.";
return false;
}
komunikatBledu = string.Empty;
return true;
}
}
Folder: Logika
LogikaKalkulator.cs
using ProstyKalkulatorPodTesty.Modele;
namespace ProstyKalkulatorPodTesty.Logika;
public class LogikaKalkulator
{
public double Oblicz(double a, double b, ModeleOperacja operacja)
{
return operacja switch
{
ModeleOperacja.Dodawanie => a + b,
ModeleOperacja.Odejmowanie => a - b,
ModeleOperacja.Mnożenie => a * b,
ModeleOperacja.Dzielenie => a / b, // 0 jest weryfikowane w walidacji
_ => throw new NotSupportedException("Nieznana operacja.")
};
}
}
LogikaPolecenie.cs (ICommand)
using System;
using System.Windows.Input;
namespace ProstyKalkulatorPodTesty.Logika;
public sealed class LogikaPolecenie : ICommand
{
private readonly Action _wykonaj;
private readonly Func<bool>? _czyMozna;
public LogikaPolecenie(Action wykonaj, Func<bool>? czyMozna = null)
{
_wykonaj = wykonaj;
_czyMozna = czyMozna;
}
public bool CanExecute(object? parameter) => _czyMozna?.Invoke() ?? true;
public void Execute(object? parameter) => _wykonaj();
public event EventHandler? CanExecuteChanged;
public void OdświeżCanExecute() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
LogikaWidokModel.cs (ViewModel)
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using ProstyKalkulatorPodTesty.Modele;
using ProstyKalkulatorPodTesty.Walidacja;
namespace ProstyKalkulatorPodTesty.Logika;
public sealed class LogikaWidokModel : INotifyPropertyChanged
{
private readonly LogikaKalkulator _kalkulator;
public LogikaWidokModel(LogikaKalkulator kalkulator)
{
_kalkulator = kalkulator;
Operacje = new ObservableCollection<ModeleOperacja>(
(ModeleOperacja[])Enum.GetValues(typeof(ModeleOperacja))
);
WybranaOperacja = ModeleOperacja.Dodawanie;
ObliczPolecenie = new LogikaPolecenie(Oblicz);
}
// Wejście
private string _liczbaA = string.Empty;
public string LiczbaA { get => _liczbaA; set { _liczbaA = value; OnPropertyChanged(); } }
private string _liczbaB = string.Empty;
public string LiczbaB { get => _liczbaB; set { _liczbaB = value; OnPropertyChanged(); } }
public ObservableCollection<ModeleOperacja> Operacje { get; }
private ModeleOperacja _wybranaOperacja;
public ModeleOperacja WybranaOperacja { get => _wybranaOperacja; set { _wybranaOperacja = value; OnPropertyChanged(); } }
// Wyjście
private double? _wynik;
public string WynikTekst => _wynik.HasValue ? _wynik.Value.ToString(CultureInfo.CurrentCulture) : "-";
private string _komunikatBledu = string.Empty;
public string KomunikatBledu { get => _komunikatBledu; set { _komunikatBledu = value; OnPropertyChanged(); } }
public ObservableCollection<ModeleDane> Historia { get; } = new();
// Polecenia
public ICommand ObliczPolecenie { get; }
private void Oblicz()
{
KomunikatBledu = string.Empty;
_wynik = null; OnPropertyChanged(nameof(WynikTekst));
if (!WalidacjaKalkulator.SprobujParsowac(LiczbaA, LiczbaB, out var a, out var b, out var blad))
{
KomunikatBledu = blad;
return;
}
if (WybranaOperacja == ModeleOperacja.Dzielenie &&
!WalidacjaKalkulator.SprawdzDzielenie(b, out var bladDzielenia))
{
KomunikatBledu = bladDzielenia;
return;
}
var wynik = _kalkulator.Oblicz(a, b, WybranaOperacja);
_wynik = wynik; OnPropertyChanged(nameof(WynikTekst));
Historia.Insert(0, new ModeleDane
{
A = a,
B = b,
Operacja = WybranaOperacja,
Wynik = wynik
});
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? nazwa = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
}
C. Testy jednostkowe (MSTest)
Projekt: ProstyKalkulatorPodTesty.Tests
(Dołącz MSTest.TestFramework i MSTest.TestAdapter jeśli szablon nie dodał.)
LogikaKalkulatorTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ProstyKalkulatorPodTesty.Logika;
using ProstyKalkulatorPodTesty.Modele;
namespace ProstyKalkulatorPodTesty.Tests
{
[TestClass]
public class LogikaKalkulatorTests
{
private readonly LogikaKalkulator _kalkulator = new();
[TestMethod]
public void Dodawanie_Daje_Poprawny_Wynik()
{
double wynik = _kalkulator.Oblicz(2, 3, ModeleOperacja.Dodawanie);
Assert.AreEqual(5, wynik);
}
[TestMethod]
public void Odejmowanie_Daje_Poprawny_Wynik()
{
double wynik = _kalkulator.Oblicz(10, 4, ModeleOperacja.Odejmowanie);
Assert.AreEqual(6, wynik);
}
[TestMethod]
public void Mnozenie_Daje_Poprawny_Wynik()
{
double wynik = _kalkulator.Oblicz(2, 5, ModeleOperacja.Mnożenie);
Assert.AreEqual(10, wynik);
}
[TestMethod]
public void Dzielenie_Daje_Poprawny_Wynik()
{
double wynik = _kalkulator.Oblicz(9, 3, ModeleOperacja.Dzielenie);
Assert.AreEqual(3, wynik);
}
}
}
WalidacjaKalkulatorTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ProstyKalkulatorPodTesty.Walidacja;
namespace ProstyKalkulatorPodTesty.Tests
{
[TestClass]
public class WalidacjaKalkulatorTests
{
[TestMethod]
public void Parsowanie_Poprawnych_Wartosci_Przechodzi()
{
// Uwaga: używamy bieżącej kultury – w PL separator to przecinek.
bool ok = WalidacjaKalkulator.SprobujParsowac("1,5", "2", out double a, out double b, out string err);
Assert.IsTrue(ok);
Assert.AreEqual(1.5, a, 1e-9);
Assert.AreEqual(2.0, b, 1e-9);
Assert.AreEqual(string.Empty, err);
}
[TestMethod]
public void Parsowanie_Tekstu_Z_Spacji_Dziala()
{
bool ok = WalidacjaKalkulator.SprobujParsowac(" 3 ", " 4,2 ", out double a, out double b, out string err);
Assert.IsTrue(ok);
Assert.AreEqual(3.0, a, 1e-9);
Assert.AreEqual(4.2, b, 1e-9);
Assert.AreEqual(string.Empty, err);
}
[TestMethod]
public void Puste_Pole_Zwraca_Blad()
{
bool ok = WalidacjaKalkulator.SprobujParsowac("", "2", out _, out _, out string err);
Assert.IsFalse(ok);
StringAssert.Contains(err, "Podaj obie liczby");
}
[TestMethod]
public void Niepoprawna_Liczba_A_Zwraca_Blad()
{
bool ok = WalidacjaKalkulator.SprobujParsowac("abc", "2", out _, out _, out string err);
Assert.IsFalse(ok);
StringAssert.Contains(err, "Niepoprawna liczba A");
}
[TestMethod]
public void Niepoprawna_Liczba_B_Zwraca_Blad()
{
bool ok = WalidacjaKalkulator.SprobujParsowac("2", "xyz", out _, out _, out string err);
Assert.IsFalse(ok);
StringAssert.Contains(err, "Niepoprawna liczba B");
}
[TestMethod]
public void Dzielenie_Przez_Zero_Wykryte()
{
bool ok = WalidacjaKalkulator.SprawdzDzielenie(0, out string err);
Assert.IsFalse(ok);
StringAssert.Contains(err, "Nie można dzielić przez zero");
}
[TestMethod]
public void Dzielenie_Nie_Przez_Zero_Prawidlowe()
{
bool ok = WalidacjaKalkulator.SprawdzDzielenie(2, out string err);
Assert.IsTrue(ok);
Assert.AreEqual(string.Empty, err);
}
}
}
D. Szybka instrukcja w VS 2022
- Utwórz WPF App ProstyKalkulatorPodTesty (Framework: .NET 8, Windows).
- Dodaj foldery Modele, Logika, Walidacja.
- Wklej powyższe pliki do odpowiednich folderów.
- Utwórz projekt testowy MSTest: ProstyKalkulatorPodTesty.Tests (net8.0) i dodaj referencję do projektu WPF.
- Testy → Uruchom wszystkie testy.
- Uruchom aplikację.

