runceel / ReactiveProperty

ReactiveProperty provides MVVM and asynchronous support features under Reactive Extensions. Target frameworks are .NET 6+, .NET Framework 4.7.2 and .NET Standard 2.0.
MIT License
903 stars 101 forks source link

Validation error doesn't apply to WPF UI when calling `ValidatableReactiveProperty.ForceNotify` #471

Closed runceel closed 9 months ago

runceel commented 9 months ago

Repro steps

  1. Create a WpfApp1 project.
  2. Add ReactiveProperty.Wpf package
  3. Past the following codes:
<Application x:Class="WpfApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp1"
             Startup="Application_Startup"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>
using Reactive.Bindings;
using Reactive.Bindings.Schedulers;
using System.Configuration;
using System.Data;
using System.Windows;

namespace WpfApp1;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    private void Application_Startup(object sender, StartupEventArgs e)
    {
        ReactivePropertyScheduler.SetDefault(new ReactivePropertyWpfScheduler(Dispatcher));
    }
}
<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel Margin="10">
        <Label Content="First name" />
        <TextBox x:Name="textBoxFirstName" Text="{Binding FirstName.Value, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True}" />
        <TextBlock Text="{Binding FirstName.ErrorMessage}" Foreground="Red" />
        <TextBlock Text="{Binding ElementName=textBoxFirstName, Path=(Validation.Errors)/ErrorContent}" />
        <Label Content="Last name" />
        <TextBox x:Name="textBoxLastName" Text="{Binding LastName.Value, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True}" />
        <TextBlock Text="{Binding LastName.ErrorMessage}" Foreground="Red" />
        <TextBlock Text="{Binding ElementName=textBoxLastName, Path=(Validation.Errors)/ErrorContent}" />
        <Button Content="OK" Command="{Binding SubmitCommand}" />
    </StackPanel>
</Window>
using Reactive.Bindings;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Diagnostics;

namespace WpfApp1;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        DataContext = new MainWindowViewModel();
        InitializeComponent();
    }
}

public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler? PropertyChanged;

    [Required]
    public ValidatableReactiveProperty<string> FirstName { get; }
    [Required]
    public ValidatableReactiveProperty<string> LastName { get; }

    private ReadOnlyReactivePropertySlim<bool> _hasError;
    public ReactiveCommand SubmitCommand { get; }

    public MainWindowViewModel()
    {
        FirstName = ValidatableReactiveProperty.CreateFromDataAnnotations("", () => FirstName,
            ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError);
        LastName = ValidatableReactiveProperty.CreateFromDataAnnotations("", () => LastName,
            ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError);

        FirstName.ErrorsChanged += (_, e) =>
        {
            Debug.WriteLine($"First name errors changed: {FirstName.Value}, {e.PropertyName}");
            Debug.WriteLine(string.Join(", ", ((INotifyDataErrorInfo)FirstName).GetErrors("").Cast<string>()));
        };
        LastName.ErrorsChanged += (_, e) =>
        {
            Debug.WriteLine($"First name errors changed: {LastName.Value}, {e.PropertyName}");
            Debug.WriteLine(string.Join(", ", ((INotifyDataErrorInfo)LastName).GetErrors("").Cast<string>()));
        };

        _hasError = Observable.CombineLatest(FirstName.ObserveHasErrors, LastName.ObserveHasErrors)
            .Select(x => x.Any(y => y))
            .ToReadOnlyReactivePropertySlim();
        SubmitCommand = _hasError
            .Select(x => x is false)
            .ToReactiveCommand(false)
            .WithSubscribe(() =>
            {
                FirstName.ForceNotify();
                LastName.ForceNotify();
                if (_hasError is { Value: false })
                {
                    MessageBox.Show("OK");
                }
            });
    }

    public void Dispose()
    {
        Disposable.Create(() =>
        {
            _hasError.Dispose();
            FirstName.Dispose();
            LastName.Dispose();
            SubmitCommand.Dispose();
        }).Dispose();
    }
}

// ReactiveProperty works fine.
//public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
//{
//    public event PropertyChangedEventHandler? PropertyChanged;

//    [Required]
//    public ReactiveProperty<string> FirstName { get; }
//    [Required]
//    public ReactiveProperty<string> LastName { get; }

//    private ReadOnlyReactivePropertySlim<bool> _hasError;
//    public ReactiveCommand SubmitCommand { get; }

//    public MainWindowViewModel()
//    {
//        FirstName = new ReactiveProperty<string>("", ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError)
//            .SetValidateAttribute(() => FirstName);
//        LastName = new ReactiveProperty<string>("", ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError)
//            .SetValidateAttribute(() => LastName);

//        FirstName.ErrorsChanged += (_, e) =>
//        {
//            Debug.WriteLine($"First name errors changed: {FirstName.Value}, {e.PropertyName}");
//            Debug.WriteLine(string.Join(", ", ((INotifyDataErrorInfo)FirstName).GetErrors("").Cast<string>()));
//        };
//        LastName.ErrorsChanged += (_, e) =>
//        {
//            Debug.WriteLine($"First name errors changed: {LastName.Value}, {e.PropertyName}");
//            Debug.WriteLine(string.Join(", ", ((INotifyDataErrorInfo)LastName).GetErrors("").Cast<string>()));
//        };

//        _hasError = Observable.CombineLatest(FirstName.ObserveHasErrors, LastName.ObserveHasErrors)
//            .Select(x => x.Any(y => y))
//            .ToReadOnlyReactivePropertySlim();
//        SubmitCommand = _hasError
//            .Select(x => x is false)
//            .ToReactiveCommand(false)
//            .WithSubscribe(() =>
//            {
//                FirstName.ForceNotify();
//                LastName.ForceNotify();
//                if (_hasError is { Value: false })
//                {
//                    MessageBox.Show("OK");
//                }
//            });
//    }

//    public void Dispose()
//    {
//        Disposable.Create(() =>
//        {
//            _hasError.Dispose();
//            FirstName.Dispose();
//            LastName.Dispose();
//            SubmitCommand.Dispose();
//        }).Dispose();
//    }
//}
  1. Launch the app
  2. Click OK button

Validation errors have occurred, but the TextBoxes are not outlined in red.

image

Expected behavior

image

runceel commented 9 months ago

https://github.com/runceel/ReactiveProperty/blob/0c0c62a3cc51cb7a51e9c73ae7eab97a8fc1967c/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs#L371

Swap 371 for 370.