@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user