From 1f4d70c766b9fe5fb719f823d88b8d6e93940453 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Fri, 10 Apr 2026 20:54:10 +0800 Subject: [PATCH] feat: add validator support --- .../BallanceTasEditor.csproj | 1 + .../Frontend/Shared/DialogService.cs | 3 +- .../{BrowserHelper.cs => ProcessHelper.cs} | 4 +- .../Frontend/Validator/ValidatorAdapter.cs | 39 +++++++++ .../Frontend/Validator/Validators.cs | 42 ++++++++++ .../Frontend/ViewModels/MainWindow.cs | 2 +- .../Frontend/ViewModels/NewFileDialog.cs | 79 ++++++++----------- .../Frontend/Views/NewFileDialog.xaml | 4 +- .../Frontend/Views/NewFileDialog.xaml.cs | 8 +- 9 files changed, 125 insertions(+), 57 deletions(-) rename BallanceTasEditor/BallanceTasEditor/Frontend/Shared/{BrowserHelper.cs => ProcessHelper.cs} (90%) create mode 100644 BallanceTasEditor/BallanceTasEditor/Frontend/Validator/ValidatorAdapter.cs create mode 100644 BallanceTasEditor/BallanceTasEditor/Frontend/Validator/Validators.cs diff --git a/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj b/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj index a58e4f5..efcd7cb 100644 --- a/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj +++ b/BallanceTasEditor/BallanceTasEditor/BallanceTasEditor.csproj @@ -17,6 +17,7 @@ + diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/DialogService.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/DialogService.cs index 75c20b1..8d970cc 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/DialogService.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/DialogService.cs @@ -56,8 +56,7 @@ namespace BallanceTasEditor.Frontend.Shared { var dialog = new Views.NewFileDialog(); dialog.Owner = m_Parent; if (dialog.ShowDialog() is true) { - // TODO: Finish result extraction - return new NewFileDialogResult() { Count = 0, Fps = 60 }; + return dialog.ViewModel.GetUserInput(); } else { return null; } diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/BrowserHelper.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/ProcessHelper.cs similarity index 90% rename from BallanceTasEditor/BallanceTasEditor/Frontend/Shared/BrowserHelper.cs rename to BallanceTasEditor/BallanceTasEditor/Frontend/Shared/ProcessHelper.cs index b50463b..e6fb80a 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/BrowserHelper.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Shared/ProcessHelper.cs @@ -7,8 +7,8 @@ using System.Text; using System.Threading.Tasks; namespace BallanceTasEditor.Frontend.Shared { - public static class BrowserHelper { - public static void OpenInDefaultBrowser(string url) { + public static class ProcessHelper { + public static void OpenUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { throw new ArgumentException("The content of URL should not be empty.", nameof(url)); } diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/ValidatorAdapter.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/ValidatorAdapter.cs new file mode 100644 index 0000000..555f1c0 --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/ValidatorAdapter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BallanceTasEditor.Frontend.Validator { + + public sealed class ValidatorAdapter where V: IValidator { + public ValidatorAdapter(IValidator validator) { + m_Validator = validator; + } + + private readonly IValidator m_Validator; + + public ValidationResult? Validate(TIn value, ValidationContext validationContext) { + // YYC MARK: + // Due to the shitty behavior of LanguageExt + // which do not allow I return nullable class from Match, + // I was forcely use MatchUnsafe. + return m_Validator.Validate(value).MatchUnsafe( + Left: v => ValidationResult.Success, + Right: err => new ValidationResult(err) + ); + } + + public TOut Conclude(TIn value) { + return m_Validator.Validate(value).Match( + Left: v => v, + Right: _ => throw new InvalidOperationException("Can not unwrap an error casting.") + ); + } + + } + +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/Validators.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/Validators.cs new file mode 100644 index 0000000..deb387e --- /dev/null +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Validator/Validators.cs @@ -0,0 +1,42 @@ +using LanguageExt; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BallanceTasEditor.Frontend.Validator { + + public interface IValidator { + Either Validate(TIn value); + } + + public sealed class FpsValidator : IValidator { + public Either Validate(string value) { + if (uint.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, out uint fps)) { + if (Backend.FpsConverter.IsValidFps(fps)) { + return fps; + } else { + return "Given FPS is out of range."; + } + } else { + return "Given string can not be parsed as unsigned integer."; + } + } + } + + public sealed class CountValidator : IValidator { + public Either Validate(string value) { + if (int.TryParse(value, System.Globalization.CultureInfo.InvariantCulture, out int count)) { + if (count > 0) { + return count; + } else { + return "Given count must be greater than zero."; + } + } else { + return "Given string can not be parsed as integer."; + } + } + } + +} diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs index c0b1e33..9baeee5 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/MainWindow.cs @@ -158,7 +158,7 @@ namespace BallanceTasEditor.Frontend.ViewModels { [RelayCommand] private void ReportBug() { try { - Shared.BrowserHelper.OpenInDefaultBrowser(Shared.Constant.REPORT_BUG_URL); + Shared.ProcessHelper.OpenUrl(Shared.Constant.REPORT_BUG_URL); } catch (Exception) { m_DialogService.ShowManuallyReportBugDialog(); } diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs index 7f85ab6..6b8ca49 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/ViewModels/NewFileDialog.cs @@ -9,57 +9,49 @@ using System.Threading.Tasks; namespace BallanceTasEditor.Frontend.ViewModels { - public partial class NewFileDialog : ObservableObject { + public partial class NewFileDialog : ObservableValidator { public NewFileDialog() { Count = Shared.Constant.DEFAULT_NEW_COUNT.ToString(); Fps = Shared.Constant.DEFAULT_FPS.ToString(); } - // YYC MARK: - // 经过无数次的尝试,我发现将int类型绑定到TextBox中所需要涉及的事情太多了, - // 尤其是这种绑定到处存在于这个程序中,以至于每次都要重新写一遍。 - // 也许后面我会做一个只能接受数字输入的文本框,但现在我累了。 - // - // 具体来说,事情是这样的。我一开始就是使用一个int类型的数据, - // 然后按照CommunityToolkit.Mvvm的标准,将其应用了Required和Range Attribute,然后将其绑定到了TextBox的Text之上。 - // 然而最终的效果很奇怪,当我删除TextBox中的所有字符后,绑定什么也没做(后来我才知道要在Required里改一个选项)。 - // 其次,当我输入不被Range接受的数值时,它仍会将其同步绑定到属性上。比如我输入0,它仍会将其绑定到Fps上, - // 进而导致Delta Time的转换显示抛出除以零的错误。这些都不是我想要的。 - // 同时,这样做的话,我没有办法检测这些字段到底是不是合规的。 - // - // 然后我就将int改写为了int?类型,用null表示当前值不可接受,非null表示绑定的值。 - // 然后定义了一个转换器,负责在int?和string之间进行转换,并同时去除了所有的验证性Attribute - // 然而遗憾的是,这么做也不符合我的要求。当我尝试输入一个数值的时候,如果我输入了任何无效值, - // 那么转换器会将其转换为null值,同时将该值赋给Fps,而被赋值的Fps又反向传播,把这个null传递给了转换器, - // 转换器将其转换成为空白值,并显示在界面上。这么做的视觉效果就是一旦我输入无效值,整个文本框就会被清空,非常的反人类。 - // 而且这一问题是不可调和的,我不能在接收到null时,选择不更新文本框的值,因为有可能这个null是我手动放进去的,而不是从转换器接受的。 - // - // 所以最终,我想通了,我决定抛弃将int和TextBox.Text绑定在一起的想法。 - // 就直接把string绑定到TextBox.Text上,然后再辅以我自己定义的一套可复用验证逻辑。 - [ObservableProperty] + [NotifyDataErrorInfo] + [CustomValidation(typeof(NewFileDialog), nameof(ValidateCount))] [NotifyCanExecuteChangedFor(nameof(OkCommand))] private string count; [ObservableProperty] + [NotifyDataErrorInfo] + [CustomValidation(typeof(NewFileDialog), nameof(ValidateFps))] [NotifyCanExecuteChangedFor(nameof(OkCommand))] private string fps; - //[ObservableProperty] - ////[CustomValidation(typeof(NewFileDialog), nameof(ValidateCount))] - //[NotifyCanExecuteChangedFor(nameof(OkCommand))] - //private string count; + #region Validators - //[ObservableProperty] - ////[CustomValidation(typeof(NewFileDialog), nameof(ValidateFps))] - //[NotifyCanExecuteChangedFor(nameof(OkCommand))] - //private string fps; + private static readonly Validator.ValidatorAdapter g_CountValidator = + new Validator.ValidatorAdapter(new Validator.CountValidator()); - ////public static ValidationResult ValidateCount(string count, ValidationContext context) { - //// return CountValidator.Instance.Validate(count); - ////} - ////public static ValidationResult ValidateFps(string fps, ValidationContext context) { - //// return FpsValidator.Instance.Validate(fps); - ////} + public static ValidationResult? ValidateCount(string value, ValidationContext context) { + return g_CountValidator.Validate(value, context); + } + + private static readonly Validator.ValidatorAdapter g_FpsValidator = + new Validator.ValidatorAdapter(new Validator.FpsValidator()); + + public static ValidationResult? ValidateFps(string value, ValidationContext context) { + return g_FpsValidator.Validate(value, context); + } + + public Shared.NewFileDialogResult GetUserInput() { + return new Shared.NewFileDialogResult { + Count = g_CountValidator.Conclude(Count), + Fps = g_FpsValidator.Conclude(Fps) + }; + } + + #endregion + + #region Commands [RelayCommand(CanExecute = nameof(CanOk))] private void Ok() { @@ -67,8 +59,7 @@ namespace BallanceTasEditor.Frontend.ViewModels { } private bool CanOk() { - // TODO - return true; + return !HasErrors; } [RelayCommand] @@ -76,19 +67,13 @@ namespace BallanceTasEditor.Frontend.ViewModels { OnRequestCloseDialog(false); } - public event Shared.RequestCloseDialogEventHandler? RequestCloseDialog; private void OnRequestCloseDialog(bool result) { - RequestCloseDialog?.Invoke(new Shared.RequestCloseDialogEventArgs { Result = result}); + RequestCloseDialog?.Invoke(new Shared.RequestCloseDialogEventArgs { Result = result }); } - //public NewFileDialogResult ToResult() { - // return new NewFileDialogResult { - // Count = CountValidator.Instance.Fetch(Count), - // DeltaTime = FpsConverter.ToDelta(FpsValidator.Instance.Fetch(Fps)), - // }; - //} + #endregion } } diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml index 760b55e..a83df46 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml @@ -47,9 +47,9 @@ + Text="{Binding Count, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"/> + Text="{Binding Fps, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"/> diff --git a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml.cs b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml.cs index f06e880..46f9cb5 100644 --- a/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml.cs +++ b/BallanceTasEditor/BallanceTasEditor/Frontend/Views/NewFileDialog.xaml.cs @@ -20,11 +20,13 @@ namespace BallanceTasEditor.Frontend.Views { public NewFileDialog() { InitializeComponent(); - var vm = new ViewModels.NewFileDialog(); - vm.RequestCloseDialog += ViewModel_RequestCloseDialog; - this.DataContext = vm; + ViewModel = new ViewModels.NewFileDialog(); + ViewModel.RequestCloseDialog += ViewModel_RequestCloseDialog; + this.DataContext = ViewModel; } + public ViewModels.NewFileDialog ViewModel { get; private set; } + private void ViewModel_RequestCloseDialog(Shared.RequestCloseDialogEventArgs e) { this.DialogResult = e.Result; this.Close();