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