Added first code.

Signed-off-by: Naoriel Sa' Rocí <naoriel@sa-roci.de>
This commit is contained in:
Naoriel Sa' Rocí 2026-03-30 02:26:31 +02:00
parent bbf68e1621
commit 4a4fb54143
25 changed files with 1072 additions and 0 deletions

2
.gitignore vendored
View File

@ -12,3 +12,5 @@
# Built Visual Studio Code Extensions
*.vsix
bin/*
obj/*

14
App.axaml Normal file
View File

@ -0,0 +1,14 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="EinmaleinsTrainer.App"
xmlns:local="using:EinmaleinsTrainer"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

28
App.axaml.cs Normal file
View File

@ -0,0 +1,28 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using EinmaleinsTrainer.Views;
using ReactiveUI;
using Avalonia.ReactiveUI;
namespace EinmaleinsTrainer;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

BIN
Assets/avalonia-logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

28
EinmaleinsTrainer.csproj Normal file
View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.3.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.12" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.12">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
</ItemGroup>
</Project>

24
EinmaleinsTrainer.sln Normal file
View File

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EinmaleinsTrainer", "EinmaleinsTrainer.csproj", "{660FFE01-9483-61B8-28ED-58FAD08A78FD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{660FFE01-9483-61B8-28ED-58FAD08A78FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{660FFE01-9483-61B8-28ED-58FAD08A78FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{660FFE01-9483-61B8-28ED-58FAD08A78FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{660FFE01-9483-61B8-28ED-58FAD08A78FD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E30069BC-F120-41C6-970C-3AE4CCC2F5C1}
EndGlobalSection
EndGlobal

35
Models/Question.cs Normal file
View File

@ -0,0 +1,35 @@
using System;
namespace EinmaleinsTrainer.Models;
public class Question : IEquatable<Question>
{
public int A { get; set; }
public int B { get; set; }
public int CorrectAnswer => A * B;
public bool Equals(Question? other)
{
if (other is null) return false;
return A == other.A &&
B == other.B;
}
public override bool Equals(object? obj)
{
return Equals(obj as Question);
}
public override int GetHashCode()
{
return HashCode.Combine(A, B);
}
public override string ToString()
{
return $"{A} x {B}";
}
}

10
Models/QuizModel.cs Normal file
View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace EinmaleinsTrainer.Models;
public class QuizModel
{
public List<Question> Questions { get; set; } = [];
public bool UseTimer { get; set; }
public int Seconds { get; set; } = 10;
}

9
Models/ResultModel.cs Normal file
View File

@ -0,0 +1,9 @@
using System;
namespace EinmaleinsTrainer.Models;
public class ResultModel
{
public int Score { get; set; } = 0;
public int Total { get; set; } = 0;
public TimeSpan? TimeRequired = null;
}

23
Program.cs Normal file
View File

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
namespace EinmaleinsTrainer;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.WithInterFont()
.LogToTrace();
}

View File

@ -0,0 +1,33 @@
using EinmaleinsTrainer.Models;
using System;
using System.Linq;
using System.Collections.Generic;
namespace EinmaleinsTrainer.Services;
public class QuestionGenerator
{
private readonly Random _random = new();
public List<Question> Generate(List<int> rows, bool includeSquares)
{
var pool = new List<Question>();
foreach (var r in rows)
{
for (int i = 1; i <= 10; i++)
pool.Add(new Question { A = r, B = i });
}
if (includeSquares)
{
for (int i = 1; i <= 10; i++)
pool.Add(new Question { A = i, B = i });
}
return pool.OrderBy(_ => _random.Next())
.Take(10)
.ToList();
}
}

139
ViewModels/MainViewModel.cs Normal file
View File

@ -0,0 +1,139 @@
using ReactiveUI;
using System.Reactive;
using EinmaleinsTrainer.Services;
using EinmaleinsTrainer.Models;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System;
namespace EinmaleinsTrainer.ViewModels;
public class MainViewModel : ViewModelBase
{
public ObservableCollection<int> SelectedRows { get; } = new();
public event EventHandler? RedrawRequired;
private bool _includeSquares;
public bool IncludeSquares
{
get => _includeSquares;
set => this.RaiseAndSetIfChanged(ref _includeSquares, value);
}
private bool _useTimer;
public bool UseTimer
{
get => _useTimer;
set
{
this.RaiseAndSetIfChanged(ref _useTimer, value);
RedrawRequired?.Invoke(this, new());
}
}
private int _secondsPerQuestion = 0;
public int SecondsPerQuestion
{
get => _secondsPerQuestion;
set
{
if (value < 2) value = 2;
if (value > 120) value = 120;
this.RaiseAndSetIfChanged(ref _secondsPerQuestion, value);
}
}
public ReactiveCommand<Unit, QuizModel> StartCommand { get; }
public MainViewModel()
{
StartCommand = ReactiveCommand.Create(
execute: () =>
{
var generator = new QuestionGenerator();
var questions = generator.Generate(SelectedRows.ToList(), IncludeSquares);
return new QuizModel(){ Questions = questions, UseTimer = UseTimer, Seconds = SecondsPerQuestion};
},
canExecute: Observable.Return(true).ObserveOn(RxApp.MainThreadScheduler),
outputScheduler: RxApp.MainThreadScheduler);
}
public void ToggleRow(int row, bool isChecked)
{
if (isChecked)
SelectedRows.Add(row);
else
SelectedRows.Remove(row);
}
public bool IsRow1Checked
{
get => SelectedRows.Contains(1);
set => ToggleRow(1, value);
}
public bool IsRow2Checked
{
get => SelectedRows.Contains(2);
set => ToggleRow(2, value);
}
public bool IsRow3Checked
{
get => SelectedRows.Contains(3);
set => ToggleRow(3, value);
}
public bool IsRow4Checked
{
get => SelectedRows.Contains(4);
set => ToggleRow(4, value);
}
public bool IsRow5Checked
{
get => SelectedRows.Contains(5);
set => ToggleRow(5, value);
}
public bool IsRow6Checked
{
get => SelectedRows.Contains(6);
set => ToggleRow(6, value);
}
public bool IsRow7Checked
{
get => SelectedRows.Contains(7);
set => ToggleRow(7, value);
}
public bool IsRow8Checked
{
get => SelectedRows.Contains(8);
set => ToggleRow(8, value);
}
public bool IsRow9Checked
{
get => SelectedRows.Contains(9);
set => ToggleRow(9, value);
}
public bool IsRow10Checked
{
get => SelectedRows.Contains(10);
set => ToggleRow(10, value);
}
public void Init()
{
SelectedRows.Clear();
IncludeSquares = false;
UseTimer = false;
SecondsPerQuestion = 30;
}
}

View File

@ -0,0 +1,73 @@
using ReactiveUI;
using System;
using System.Reactive.Linq;
namespace EinmaleinsTrainer.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
private ViewModelBase _current;
public ViewModelBase Current
{
get => _current;
set => SetProperty(ref _current, value);
}
public readonly MainViewModel MainModel = new();
public readonly QuizViewModel QuizModel = new();
public readonly ResultViewModel ResultModel = new();
private readonly ViewModelBase _dummy = new();
public MainWindowViewModel()
{
MainModel.Init();
_current = MainModel;
ResultModel.RestartCommand
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(result =>
{
// In order to keep the last value if required
int secondsPerQuestion = MainModel.SecondsPerQuestion;
MainModel.Init();
if (result.TimeRequired != null)
{
MainModel.SecondsPerQuestion = (result.Score >= 10) ?
(int)Math.Round(0.15 * result.TimeRequired.Value.TotalSeconds, 0, MidpointRounding.AwayFromZero) :
secondsPerQuestion;
MainModel.UseTimer = true;
}
Current = MainModel;
});
QuizModel.Finished
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(result =>
{
ResultModel.Init(result);
Current = ResultModel;
});
QuizModel.RedrawRequired += (s, a) =>
{
Current = _dummy;
Current = QuizModel;
};
MainModel.StartCommand
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(quiz =>
{
if (quiz.Questions.Count == 0) return;
QuizModel.Init(quiz);
Current = QuizModel;
});
MainModel.RedrawRequired += (s, a) =>
{
Current = _dummy;
Current = MainModel;
};
}
}

293
ViewModels/QuizViewModel.cs Normal file
View File

@ -0,0 +1,293 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using EinmaleinsTrainer.Models;
namespace EinmaleinsTrainer.ViewModels;
public class QuizViewModel : ViewModelBase
{
// =========================
// Basisdaten
// =========================
private QuizModel _model = new();
private List<Question> _questions = new();
public List<Question> Questions
{
get => _questions;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _questions, value);
this.RaisePropertyChanged(nameof(Total));
// Wenn sich Questions ändern → Index resetten
Index = 0;
});
}
}
public int Total => Questions.Count;
private int _index;
public int Index
{
get => _index;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
bool changed = _index == value;
this.RaiseAndSetIfChanged(ref _index, value);
if (!changed)
{
this.RaisePropertyChanged();
}
UpdateDerivedProperties();
});
}
}
// =========================
// Abgeleitete Properties
// =========================
private Question _currentQuestion = new() { A = 0, B = 0 };
public Question CurrentQuestion
{
get => _currentQuestion;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _currentQuestion, value);
});
}
}
private int _questionNumber;
public int QuestionNumber
{
get => _questionNumber;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _questionNumber, value);
});
}
}
private string _currentQuestionText = "0 × 0";
public string CurrentQuestionText
{
get => _currentQuestionText;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _currentQuestionText, value);
});
}
}
// =========================
// User Input / Status
// =========================
private string _userAnswer = "";
public string UserAnswer
{
get => _userAnswer;
set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _userAnswer, value);
});
}
}
private int _remaining;
public int RemainingSeconds
{
get => _remaining;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _remaining, value);
if (value > 0)
RedrawRequired?.Invoke(this, new());
});
}
}
private int _score;
public int Score
{
get => _score;
private set
{
RxApp.MainThreadScheduler.Schedule(() =>
{
this.RaiseAndSetIfChanged(ref _score, value);
});
}
}
public bool UseTimer { get; private set; }
private TimeSpan TimeRequired = TimeSpan.Zero;
private DateTime Started = DateTime.MinValue;
// =========================
// Commands / Events
// =========================
public ReactiveCommand<Unit, Unit> SubmitCommand { get; }
private readonly Subject<ResultModel> _finished = new();
public IObservable<ResultModel> Finished => _finished.AsObservable();
public event EventHandler? RedrawRequired;
private IDisposable? _timerSub;
// =========================
// ctor
// =========================
public QuizViewModel()
{
SubmitCommand = ReactiveCommand.Create(
Submit,
outputScheduler: RxApp.MainThreadScheduler
);
}
// =========================
// Init
// =========================
public void Init(QuizModel model)
{
_timerSub?.Dispose();
_model = model;
TimeRequired = TimeSpan.Zero;
Questions = new List<Question>(model.Questions);
Score = 0;
UserAnswer = "";
UseTimer = model.UseTimer && model.Seconds > 1;
RemainingSeconds = UseTimer ? model.Seconds : 0;
// Abgeleitete Properties initial setzen
UpdateDerivedProperties();
if (UseTimer)
StartTimer(model.Seconds);
}
// =========================
// Ableitungen zentral
// =========================
private void UpdateDerivedProperties()
{
if (Questions.Count == 0 || Index < 0 || Index >= Questions.Count)
{
CurrentQuestion = new Question { A = 0, B = 0 };
QuestionNumber = 0;
CurrentQuestionText = "0 × 0";
return;
}
var q = Questions[Index];
CurrentQuestion = q; // Direkt zuweisen
QuestionNumber = Index + 1;
CurrentQuestionText = $"{q.A} × {q.B}";
}
// =========================
// Timer
// =========================
private void StartTimer(int seconds)
{
Started = DateTime.Now;
RemainingSeconds = seconds;
_timerSub?.Dispose();
_timerSub = Observable
.Interval(TimeSpan.FromSeconds(1), RxApp.MainThreadScheduler)
.Select(i => seconds - (int)i - 1)
.TakeWhile(s => s >= 0)
.Subscribe(s =>
{
RemainingSeconds = s;
if (s == 0)
SubmitInternal(false);
});
}
// =========================
// Submit
// =========================
private void Submit()
{
bool correct =
int.TryParse(UserAnswer, out int a) &&
a == CurrentQuestion.CorrectAnswer;
SubmitInternal(correct);
}
private void SubmitInternal(bool correct)
{
if (correct)
RxApp.MainThreadScheduler.Schedule(() => Score++);
_timerSub?.Dispose();
if (UseTimer)
TimeRequired += DateTime.Now - Started;
RxApp.MainThreadScheduler.Schedule(() =>
{
Index++;
if (Index >= Questions.Count)
{
_finished.OnNext(new ResultModel
{
Score = Score,
Total = Total,
TimeRequired = UseTimer ? TimeRequired : null
});
return;
}
// UserAnswer zurücksetzen
UserAnswer = "";
// Update der abgeleiteten Properties (CurrentQuestionText, QuestionNumber)
UpdateDerivedProperties();
RedrawRequired?.Invoke(this, new());
});
if (UseTimer && Index < Questions.Count)
StartTimer(_model.Seconds);
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using EinmaleinsTrainer.Models;
using ReactiveUI;
namespace EinmaleinsTrainer.ViewModels;
public class ResultViewModel : ViewModelBase
{
public string Text { get; private set;} = "";
public string TimeRequiredText { get; private set;} = "";
public bool ShowTimeRequired => !string.IsNullOrEmpty(TimeRequiredText);
private ResultModel _model = new();
public void Init(ResultModel result)
{
double percent = result.Score * 1.0 / result.Total;
if (percent > 0.9)
{
Text = $"🎉 {result.Score} von {result.Total} richtig!";
}
else if (percent > 0.5)
{
Text = $"👍 {result.Score} von {result.Total} richtig.";
}
else if (percent > 0.25)
{
Text = $"🤨 {result.Score} von {result.Total} richtig.";
}
else if (percent > 0.0)
{
Text = $"😑 {result.Score} von {result.Total} richtig.";
}
else
{
Text = $"😭 {result.Score} von {result.Total} richtig.";
}
if (result.TimeRequired != null)
{
int SecondsRequired = (int)Math.Round(result.TimeRequired.Value.TotalSeconds, 0, MidpointRounding.AwayFromZero);
TimeRequiredText = $"In {SecondsRequired} s gelöst.";
}
else
{
TimeRequiredText = "";
}
_model = result;
}
public ReactiveCommand<Unit, ResultModel> RestartCommand { get; }
public ResultViewModel()
{
RestartCommand = ReactiveCommand.Create(
execute: () =>
{
return _model;
},
canExecute: Observable.Return(true).ObserveOn(RxApp.MainThreadScheduler),
outputScheduler: RxApp.MainThreadScheduler);
}
}

View File

@ -0,0 +1,42 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using ReactiveUI;
namespace EinmaleinsTrainer.ViewModels;
public class ViewModelBase : IReactiveObject
{
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;
public void RaisePropertyChanged(PropertyChangedEventArgs args)
{
PropertyChanged?.Invoke(this, args);
}
public void RaisePropertyChanging(PropertyChangingEventArgs args)
{
PropertyChanging?.Invoke(this, args);
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (Equals(field, value)) return false;
OnPropertyChanging(propertyName);
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

52
Views/MainView.axaml Normal file
View File

@ -0,0 +1,52 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
x:Class="EinmaleinsTrainer.Views.MainView"
x:DataType="vm:MainViewModel">
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10" Margin="20">
<TextBlock FontSize="32" Text="Einmaleins Trainer"/>
<WrapPanel>
<CheckBox FontSize="24" Content="1" IsChecked="{Binding IsRow1Checked}" Width="60" />
<CheckBox FontSize="24" Content="2" IsChecked="{Binding IsRow2Checked}" Width="60" />
<CheckBox FontSize="24" Content="3" IsChecked="{Binding IsRow3Checked}" Width="60" />
<CheckBox FontSize="24" Content="4" IsChecked="{Binding IsRow4Checked}" Width="60" />
<CheckBox FontSize="24" Content="5" IsChecked="{Binding IsRow5Checked}" Width="60" />
</WrapPanel>
<WrapPanel>
<CheckBox FontSize="24" Content="6" IsChecked="{Binding IsRow6Checked}" Width="60" />
<CheckBox FontSize="24" Content="7" IsChecked="{Binding IsRow7Checked}" Width="60" />
<CheckBox FontSize="24" Content="8" IsChecked="{Binding IsRow8Checked}" Width="60" />
<CheckBox FontSize="24" Content="9" IsChecked="{Binding IsRow9Checked}" Width="60" />
<CheckBox FontSize="24" Content="10" IsChecked="{Binding IsRow10Checked}" Width="60" />
</WrapPanel>
<CheckBox FontSize="24" Content="Quadratzahlen" IsChecked="{Binding IncludeSquares}" />
<WrapPanel>
<Grid Height="44">
<CheckBox FontSize="24" Content="Zeitlimit" VerticalAlignment="Center"
IsChecked="{Binding UseTimer}" Width="140"/>
</Grid>
<NumericUpDown Minimum="2" Maximum="120" FontSize="24" Value="{Binding SecondsPerQuestion, Mode=TwoWay}" Width="140" FormatString="F0" ParsingNumberStyle="Integer" IsVisible="{Binding UseTimer}" />
<Grid Height="40">
<TextBlock FontSize="24" Text=" s" VerticalAlignment="Center" IsVisible="{Binding UseTimer}" />
</Grid>
</WrapPanel>
<StackPanel HorizontalAlignment="Center" >
<Button FontSize="24" Content="Start" IsDefault="True"
Command="{Binding StartCommand}" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

39
Views/MainView.xaml.cs Normal file
View File

@ -0,0 +1,39 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using EinmaleinsTrainer.ViewModels;
namespace EinmaleinsTrainer.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void Row_Checked(object sender, RoutedEventArgs e)
{
if (sender is CheckBox cb
&& DataContext is MainViewModel vm
&& int.TryParse(cb.Content?.ToString(), out int value))
{
vm.ToggleRow(value, true);
}
}
private void Row_Unchecked(object sender, RoutedEventArgs e)
{
if (sender is CheckBox cb
&& DataContext is MainViewModel vm
&& int.TryParse(cb.Content?.ToString(), out int value))
{
vm.ToggleRow(value, false);
}
}
}

25
Views/MainWindow.axaml Normal file
View File

@ -0,0 +1,25 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
xmlns:views="using:EinmaleinsTrainer.Views"
x:Class="EinmaleinsTrainer.Views.MainWindow"
x:DataType="vm:MainWindowViewModel">
<Window.DataTemplates>
<DataTemplate DataType="vm:MainViewModel">
<views:MainView/>
</DataTemplate>
<DataTemplate DataType="vm:QuizViewModel">
<views:QuizView/>
</DataTemplate>
<DataTemplate DataType="vm:ResultViewModel">
<views:ResultView/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Content="{Binding Current}" />
</Window>

19
Views/MainWindow.xaml.cs Normal file
View File

@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using EinmaleinsTrainer.ViewModels;
namespace EinmaleinsTrainer.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

29
Views/QuizView.axaml Normal file
View File

@ -0,0 +1,29 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
x:Class="EinmaleinsTrainer.Views.QuizView"
x:DataType="vm:QuizViewModel">
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="12" Margin="20">
<TextBlock FontSize="18"
Text="{Binding QuestionNumber, StringFormat='Frage {0}'}" />
<TextBlock FontSize="36"
Text="{Binding CurrentQuestionText}" />
<TextBox x:Name="AnswerTextBox" FontSize="24" Text="{Binding UserAnswer, Mode=TwoWay}" />
<Button FontSize="24" Content="OK" IsDefault="True"
Command="{Binding SubmitCommand}" />
<TextBlock FontSize="12" Text="{Binding Score, StringFormat='Punkte: {0}'}" />
<TextBlock FontSize="18" Text="{Binding RemainingSeconds, StringFormat='Zeit: {0} s'}"
IsVisible="{Binding UseTimer}" />
</StackPanel>
</Grid>
</UserControl>

26
Views/QuizView.xaml.cs Normal file
View File

@ -0,0 +1,26 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
namespace EinmaleinsTrainer.Views;
public partial class QuizView : UserControl
{
public QuizView()
{
InitializeComponent();
this.AttachedToVisualTree += async (_,_) =>
{
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
var tb = this.FindControl<TextBox>("AnswerTextBox");
tb?.Focus();
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

29
Views/ResultView.axaml Normal file
View File

@ -0,0 +1,29 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:EinmaleinsTrainer.ViewModels"
x:Class="EinmaleinsTrainer.Views.ResultView"
x:DataType="vm:ResultViewModel">
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="12" Margin="20">
<StackPanel HorizontalAlignment="Center" >
<TextBlock FontSize="36"
Text="{Binding Text}" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" IsVisible="{Binding ShowTimeRequired}" >
<TextBlock FontSize="24"
Text="{Binding TimeRequiredText}" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" >
<Button FontSize="36" Content="Neu starten" IsDefault="True"
Command="{Binding RestartCommand}" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>

18
Views/ResultView.xaml.cs Normal file
View File

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace EinmaleinsTrainer.Views;
public partial class ResultView : UserControl
{
public ResultView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

18
app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="SaRociSolutions.OSS.EinmaleinsTrainer"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>