2

I have a view model:

public delegate void NotifyWithValidationMessages(Dictionary<string, string?> validationDictionary);

public partial class BaseViewModel : ObservableValidator
{
    public event NotifyWithValidationMessages? ValidationCompleted;
    public virtual ICommand ValidateCommand => new RelayCommand(() => ValidateModel());

    private ValidationContext validationContext;

    public BaseViewModel()
    {
        validationContext = new ValidationContext(this);
    }

    [IndexerName("ErrorDictionary")]
    public ValidationStatus this[string propertyName]
    {
        get
        {
            ClearErrors();
            ValidateAllProperties();

            var errors = this.GetErrors()
                             .ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>();

            var hasErrors = errors.TryGetValue(propertyName, out var error);
            return new ValidationStatus(hasErrors, error ?? string.Empty);
        }
    }

    private void ValidateModel()
    {
        ClearErrors();
        ValidateAllProperties();

        var validationMessages = this.GetErrors()
                                     .ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);

        ValidationCompleted?.Invoke(validationMessages);
    }
}

public partial class LoginModel : BaseViewModel
{
    protected string email;
    protected string password;


    [Required]
    [DataType(DataType.EmailAddress)]
    public string Email
    {
        get => this.email;
        set
        {
            SetProperty(ref this.email, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Email]");
        }
    }

    [Required]
    [DataType(DataType.Password)]
    public string Password
    {
        get => this.password;
        set
        {
            SetProperty(ref this.password, value, true);

            ClearErrors();
            ValidateAllProperties();
            OnPropertyChanged("ErrorDictionary[Password]");
        }
    }
}

public partial class LoginViewModel : LoginModel
{
    private readonly ISecurityClient securityClient;

    public LoginViewModel(ISecurityClient securityClient) : base()
    {
        this.securityClient = securityClient;
    }

    public ICommand LoginCommand => new RelayCommand(async() => await LoginAsync());

    public ICommand NavigateToRegisterPageCommand => new RelayCommand(async () => await Shell.Current.GoToAsync(PageRoutes.RegisterPage, true));

    private async Task LoginAsync()
    {
        if (this?.HasErrors ?? true)
            return;

        var requestParam = this.ConvertTo<LoginModel>();

        var response = await securityClient.LoginAsync(requestParam);

        if (response is null)
        {
            await Application.Current.MainPage.DisplayAlert("", "Login faild, or unauthorized", "OK");
            StorageService.Secure.Remove(StorageKeys.Secure.JWT);
            return;
        }

        await StorageService.Secure.SaveAsync<JWTokenModel>(StorageKeys.Secure.JWT, response);

        await Shell.Current.GoToAsync(PageRoutes.HomePage, true);
    }
}

The view looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:local="clr-namespace:Backend.Models;assembly=Backend.Models"
             xmlns:vm="clr-namespace:MauiUI.ViewModels"
             x:Class="MauiUI.Pages.LoginPage"
             x:DataType="vm:LoginViewModel"
             Shell.NavBarIsVisible="False">

    <ScrollView>
        <VerticalStackLayout Spacing="25" Padding="20,0"
                             VerticalOptions="Center">

            <VerticalStackLayout>
                <Label Text="Welcome to Amazons of Vollyeball" FontSize="28" TextColor="Gray" HorizontalTextAlignment="Center" />
            </VerticalStackLayout>

            <Image Source="volleyball.png"
                HeightRequest="250"
                WidthRequest="250"
                HorizontalOptions="Center" />

            <StackLayout Orientation="Horizontal">
                <Frame ZIndex="1" HasShadow="True" BorderColor="White"
                       HeightRequest="55" WidthRequest="55" CornerRadius="25"
                       Margin="0,0,-32,0">
                    <Image Source="email.png" HeightRequest="30" WidthRequest="30" />
                </Frame>
                <Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
                    <Entry x:Name="email" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="email"  Keyboard="Email"
                            Text="{Binding Email, Mode=TwoWay}"
                            toolkit:SetFocusOnEntryCompletedBehavior.NextElement="{x:Reference password}"
                            ReturnType="Next">
                        <Entry.Behaviors>
                            <toolkit:EventToCommandBehavior
                                EventName="TextChanged"
                                Command="{Binding ValidateCommand}" />
                        </Entry.Behaviors>
                    </Entry>
                </Frame>
            </StackLayout>
            <Label x:Name="lblValidationErrorEmail" Text="{Binding [Email].Error}" TextColor="Red" />

            <StackLayout Orientation="Horizontal">
                <Frame ZIndex="1" HasShadow="True" BorderColor="White" 
                       HeightRequest="55" WidthRequest="55" CornerRadius="25"
                       Margin="0,0,-32,0">
                    <Image Source="password.jpg" HeightRequest="30" WidthRequest="30"/>
                </Frame>
                <Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
                    <Entry x:Name="password" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="password" IsPassword="True"
                            Text="{Binding Password, Mode=TwoWay}">
                        <Entry.Behaviors>
                            <toolkit:EventToCommandBehavior
                                EventName="TextChanged"
                                Command="{Binding ValidateCommand}" />
                        </Entry.Behaviors>
                    </Entry>
                </Frame>
            </StackLayout>
            <Label x:Name="lblValidationErrorPassword" Text="{Binding [Password].Error}" TextColor="Red" />

            <Button Text="Login" WidthRequest="120" CornerRadius="25" HorizontalOptions="Center" BackgroundColor="Blue"
                    Command="{Binding LoginCommand}" />
            <StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
                <Label Text="Don't have an account?" TextColor="Gray"/>
                <Label>
                    <Label.FormattedText>
                        <FormattedString>
                            <Span Text="Register" TextColor="Blue">
                                <Span.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding NavigateToRegisterPageCommand}" />
                                </Span.GestureRecognizers>
                            </Span>
                        </FormattedString>
                    </Label.FormattedText>
                </Label>
            </StackLayout>

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

public partial class LoginPage : ContentPage
{
    private RegisterViewModel viewModel => BindingContext as RegisterViewModel;

    public LoginPage(LoginViewModel viewModel)
    {
        InitializeComponent();

        viewModel.ValidationCompleted += OnValidationHandler;
        BindingContext = viewModel;

#if ANDROID
        MauiUI.Platforms.Android.KeyboardHelper.HideKeyboard();
#elif IOS
        MauiUI.Platforms.iOS.KeyboardHelper.HideKeyboard();
#endif
    }

    private void OnValidationHandler(Dictionary<string, string> validationMessages)
    {
        if (validationMessages is null)
            return;

        lblValidationErrorEmail.Text = validationMessages.GetValueOrDefault("email");
        lblValidationErrorPassword.Text = validationMessages.GetValueOrDefault("password");
    }
}

When the public ValidationStatus this[string propertyName] or the ValidateModel() triggers in BaseViewModel, the this.GetErrors() form the ObservableValidator class, return no errors, even if there are validation errors.

Interesting part was that, when I did not use MVVM aproach, and used LoginModel that inherited the BaseViewModel, then worked.

I am out of idea. thnx

Wasyster
  • 2,279
  • 4
  • 26
  • 58
  • I don't see anything obviously wrong. To be sure it isn't some other code issue: inside `ValidateModels`, after `ValidateAllProperties();`, add `var errors = this.GetErrors();`. Put a breakpoint on the line after that. Is that breakpoint reached? does `errors` have count 0? That is, examine errors directly, rather than examining the dictionary you build. – ToolmakerSteve Jan 24 '23 at 01:55
  • Did what you suggested. Returns an empty list. – Wasyster Jan 24 '23 at 05:51

2 Answers2

1

I do not write my own properties. Instead, I let MVVM handle it.

Lets say we have this:

public partial class MainViewModel : BaseViewModel
{
    [Required(ErrorMessage = "Text is Required Field!")]
    [MinLength(5, ErrorMessage = "Text length is minimum 5!")]
    [MaxLength(10, ErrorMessage = "Text length is maximum 10!")]
    [ObservableProperty]
    string _text = "Hello";

Where BaseViewModel is inheriting ObservableValidator.

Now I can use Validation command:

[RelayCommand]
    void Validate()
    {
        ValidateAllProperties();
        
        if (HasErrors)
            Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
        else
            Error = String.Empty;

        IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
    }

Or use partial method:

partial void OnTextChanged(String text)
    {
        ValidateAllProperties();

        if (HasErrors)
            Error = string.Join(Environment.NewLine, GetErrors().Select(e => e.ErrorMessage));
        else
            Error = String.Empty;

        IsTextValid = (GetErrors().ToDictionary(k => k.MemberNames.First(), v => v.ErrorMessage) ?? new Dictionary<string, string?>()).TryGetValue(nameof(Text), out var error);
    }

Where Error is:

[ObservableProperty]
string _error;

And IsTextValid is:

[ObservableProperty]
bool _isTextValid;

Now you can bind those properties to whatever you want to display the error, or indicate that there is an error with your Text.

This is a working example, using validation, CommunityToolkit.MVVM and BaseViewModel class.

H.A.H.
  • 2,104
  • 1
  • 8
  • 21
0

I made a demo based on your code and make some debugs. The thing i found is that you just clear the Errors ClearErrors();. Then you could not get any message.

Workaround:

In LoginModel the Email setter, put ClearErrors(); before SetProperty(ref this.email, value, true);.

    [DataType(DataType.EmailAddress)]
    public string Email
    {
        get => this.email;
        set
        {
            ClearErrors();
            SetProperty(ref this.email, value, true);
            ....
        }
    }

In BaseViewModel, comment out other ClearErrors(); in Indexer and ValidateModel() since you have already cleared it.

[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
    get
    {
        //ClearErrors();
        ValidateAllProperties();
...
    }
}

private void ValidateModel()
{
    //ClearErrors();
    ValidateAllProperties();
....
}

However, your code show two ways of invoking the ValidateAllProperty() :

  1. First is because in the .xaml, you set Text="{Binding Email, Mode=TwoWay}". That means when changing the text, the setter of Email in your LoginModel will fire and so raise propertyChanged for the Indexer.

  2. Second is EventToCommandBehavior set in the .xaml. This also invoke ValidateAllProperty(). And pass the text to label Text which has already binded Text="{Binding [Email].Error}".

From my point of view, one is enough. You have to decide which one to use. Better not mix these two methods together that may cause troubles.

Also for MVVM structure, you could refer to Model-View-ViewModel (MVVM).

Hope it works for you.

Liqun Shen-MSFT
  • 3,490
  • 2
  • 3
  • 11
  • Did not worked. – Wasyster Jan 24 '23 at 06:07
  • 1
    If I remove the LoginModel and put the code inside the LoginViewModel directly, then it works! – Wasyster Jan 24 '23 at 06:10
  • It's really weird that i just make some changes in my answer and that worked for me. I just don't know if i missed anything. Also, i attached a link about MVVM which you could refer to. – Liqun Shen-MSFT Jan 24 '23 at 08:54
  • And you used a 4 level inheritance: ObservableValidator-> BaseViewModel -> LoginModel - > LoginViewModel? – Wasyster Jan 24 '23 at 09:41
  • It has nothing to do with ClearErrors. ValidateAllProperties sets totalErrors and calls for PropertyChanged for the HasErrors. It has nothing to do with level of inheritance also. You can use 100 levels, as long as your base class inherits ObservableValidator, you can make your structure as deep or shallow as you want. Every class can call for validation and test for errors. – H.A.H. Jan 24 '23 at 11:18
  • On the first look, you did not missed anything. I am wondering, can the emulator cache the things and not applying the changes I make? – Wasyster Jan 24 '23 at 12:24
  • Clear the project, delete the bin / obj folder might be help. – Liqun Shen-MSFT Jan 24 '23 at 15:23
  • @LiqunShen-MSFT I applied your suggestions. Now it behaves strange. On input there is no error, but when I delete all from the entry then the validation appears. – Wasyster Jan 24 '23 at 18:12
  • 1
    @LiqunShen-MSFT here is my repo, this my learning app, so not everything is as it shuold be. https://github.com/wasyster/amazonsofvolleyball-maui.git if you wish to play a little around. – Wasyster Jan 24 '23 at 18:17
  • I think the e-mail validator is not working! Any other validator is works, just the email not. – Wasyster Jan 24 '23 at 20:45
  • I did the same, it's working now, except that tha DataType(Datatype.EmailAddress) annotation not works, it's not validating an email address, not even with regex, or else. Will try with custom DataAnnotation today. – Wasyster Jan 25 '23 at 05:40
  • Glad that it worked. Much appreciate if you could share you solution. that might help others with similar questions. – Liqun Shen-MSFT Jan 25 '23 at 05:55
  • I will provide the solution when I finished it and tested. – Wasyster Jan 25 '23 at 08:10