Reputation: 2535
I am not sure that I understand MVVM correctly. This is what I did:
BaseViewModel
public class BaseViewModel : ObservableValidator
{
public event NotifyWithValidationMessages? ValidationCompleted;
public BaseViewModel() : base()
{}
public virtual ICommand ValidateCommand => new RelayCommand(() =>
{
ClearErrors();
ValidateAllProperties();
var validationMessages = this.GetErrors()
.ToDictionary(k => k.MemberNames.First().ToLower(), v => v.ErrorMessage);
ValidationCompleted?.Invoke(validationMessages);
});
[IndexerName("ErrorDictionary")]
public ValidationStatus this[string propertyName]
{
get
{
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);
}
}
}
RegisterModel
public class RegisterModel : BaseViewModel
{
[Required(ErrorMessage = "User Name is required")]
public string? Username { get; set; }
[EmailAddress]
[Required(ErrorMessage = "Email is required")]
public string? Email { get; set; }
[Required(ErrorMessage = "Password is required")]
[DataType(DataType.Password)]
public string? Password { get; set; }
public RegisterModel() : base()
{
}
}
RegisterViewModel
public class RegisterViewModel : RegisterModel
{
private readonly ISecurityClient securityClient;
public RegisterViewModel(ISecurityClient securityClient) : base()
{
this.securityClient = securityClient;
}
public ICommand NavigateToLoginPageCommand => new RelayCommand(async() =>
await Shell.Current.GoToAsync(PageRoutes.LoginPage, true)
);
public ICommand RegisterCommand => new RelayCommand(OnRegisterCommand);
private async void OnRegisterCommand()
{
if (this?.HasErrors ?? true)
return;
var requestParam = this.ConvertTo<RegisterModel>();
var success = await securityClient.RegisterAsync(requestParam);
if (!success)
{
await Application.Current.MainPage.DisplayAlert("", "Register faild", "OK");
return;
}
await Application.Current.MainPage.DisplayAlert("", "Registered successfully.\nYou can now login.", "OK");
await Shell.Current.GoToAsync(PageRoutes.LoginPage, true);
}
}
RegisterPage (code-behind)
public partial class RegisterPage : ContentPage
{
public RegisterViewModel ViewModel => BindingContext as RegisterViewModel;
public RegisterPage(RegisterViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
viewModel.ValidationCompleted += OnValidationHandler;
}
private void OnValidationHandler(Dictionary<string, string?> validationMessages)
{
if (validationMessages is null)
return;
lblValidationErrorUserName.Text = validationMessages.GetValueOrDefault("username");
lblValidationErrorEmail.Text = validationMessages.GetValueOrDefault("email");
lblValidationErrorPassword.Text = validationMessages.GetValueOrDefault("password");
}
}
When I add the following line to the XAML:
<ContentPage.BindingContext>
<vm:RegisterViewModel />
</ContentPage.BindingContext>
I get the the following error: 'RegisterViewModel' is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter.
Whole XAML:
<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"
x:Class="MauiUI.Pages.RegisterPage"
xmlns:vm="clr-namespace:MauiUI.ViewModels"
Title="Register">
<ContentPage.BindingContext>
<vm:RegisterViewModel />
</ContentPage.BindingContext>
<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="user.png" HeightRequest="30" WidthRequest="30" />
</Frame>
<Frame HasShadow="True" Padding="0" BorderColor="White" HeightRequest="55" HorizontalOptions="FillAndExpand">
<Entry x:Name="username" Margin="35,0,20,0" VerticalOptions="Center" Placeholder="email" Keyboard="Email"
Text="{Binding Username, Mode=TwoWay}"
toolkit:SetFocusOnEntryCompletedBehavior.NextElement="{x:Reference email}"
ReturnType="Next">
<Entry.Behaviors>
<toolkit:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding [Username].HasError}" />
</Entry.Behaviors>
</Entry>
</Frame>
<Label x:Name="lblValidationErrorUserName" Text="{Binding [Username].Error}" TextColor="Red" />
</StackLayout>
<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 [Email].HasError}" />
</Entry.Behaviors>
</Entry>
</Frame>
<Label x:Name="lblValidationErrorEmail" Text="{Binding [Email].Error}" TextColor="Red" />
</StackLayout>
<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 [Password].HasError}" />
</Entry.Behaviors>
</Entry>
</Frame>
<Label x:Name="lblValidationErrorPassword" Text="{Binding [Password].Error}" TextColor="Red" />
</StackLayout>
<Button Text="Register" WidthRequest="120" CornerRadius="25" HorizontalOptions="Center" BackgroundColor="Blue"
Command="{Binding RegisterCommand}" />
<StackLayout Orientation="Horizontal" Spacing="5" HorizontalOptions="Center">
<Label Text="Have an account?" TextColor="Gray"/>
<Label>
<Label.FormattedText>
<FormattedString>
<Span Text="Login" TextColor="Blue">
<Span.GestureRecognizers>
<TapGestureRecognizer Command="{Binding NavigateToLoginPageCommand}" />
</Span.GestureRecognizers>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
I registered the page and the view model in DI container.
//viewModels
builder.Services.AddSingleton<RegisterViewModel>();
//pages
builder.Services.AddSingleton<RegisterPage>();
Any guidance is welcome.
Upvotes: 1
Views: 3236
Reputation: 8856
Your problem is that you're trying to instantiate a ViewModel without passing arguments to the constructor although your ViewModel only defines a constructor with a required parameter. You either need to define a parameterless constructor or provide arguments to the constructor.
Here is your constructor:
public class RegisterViewModel : RegisterModel
{
private readonly ISecurityClient securityClient;
public RegisterViewModel(ISecurityClient securityClient) : base()
{
this.securityClient = securityClient;
}
//...
}
You're trying to instantiate it without arguments here:
<ContentPage.BindingContext>
<vm:RegisterViewModel />
</ContentPage.BindingContext>
This instantiation is unnecessary, because you're already setting the BindingContext
in the code-behind using the ViewModel instance that is passed in via dependency injection:
public partial class RegisterPage : ContentPage
{
public RegisterViewModel ViewModel => BindingContext as RegisterViewModel;
public RegisterPage(RegisterViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
viewModel.ValidationCompleted += OnValidationHandler;
}
//...
}
You can safely remove the code that sets the BindingContext
in the XAML:
<!-- remove this -->
<ContentPage.BindingContext>
<vm:RegisterViewModel />
</ContentPage.BindingContext>
And make sure that the required argument somehow gets passed into the ViewModel's constructor.
Here are a few ways to do this:
var registerVm = new RegisterViewModel(securityClient);
builder.Services.AddSingleton<RegisterViewModel>(registerVm);
You could also pass the ISecurityClient
into the IoC container:
builder.Services.AddSingleton<ISecurityClient>(new SecurityClient());
builder.Services.AddSingleton<RegisterViewModel>();
Alternatively, depending on how the ISecurityClient
implementation is defined, assuming it's something like "SecurityClient", you could also register it like this (avoiding any constructor calls altogether provided that the constructor of "SecurityClient" doesn't take any parameters):
builder.Services.AddSingleton<SecurityClient>();
builder.Services.AddSingleton<RegisterViewModel>();
This should automatically resolve the constructor with the required argument.
If you want Intellisense support for your ViewModel inside your XAML, you could add compiled bindings to your XAML like this (using the x:DataType
attribute):
<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"
x:Class="MauiUI.Pages.RegisterPage"
xmlns:vm="clr-namespace:MauiUI.ViewModels"
x:DataType="vm:RegisterViewModel"
Title="Register">
or use design-time XAML.
Upvotes: 5