added recommendations; fixed .sln

This commit is contained in:
John Baughman
2024-06-13 09:24:27 -05:00
parent 12adcf3bf4
commit dea0e5696f
5 changed files with 209 additions and 30 deletions

View File

@@ -8,13 +8,15 @@ namespace CommunityToolkitExample;
internal partial class LoginViewModel : ObservableObject
{
private const string DEFAULT_LOGIN_PROGRESS_MESSAGE = "Press 'Login' to log in.";
private const string INVALID_LOGIN_MESSAGE = "Please enter a valid user name and password.";
private const string LOGGING_IN_PROGRESS_MESSAGE = "Logging in...";
private const string VALID_LOGIN_MESSAGE = "The input is valid!";
private const string INVALID_LOGIN_MESSAGE = "Please enter a valid user name and password.";
[ObservableProperty]
private bool _canLogin;
[ObservableProperty]
private string _loginProgressMessage;
private string _password;
[ObservableProperty]
@@ -24,16 +26,11 @@ internal partial class LoginViewModel : ObservableObject
[ObservableProperty]
private string _usernameLengthMessage;
[ObservableProperty]
private string _loginProgressMessage;
[ObservableProperty]
private string _validationMessage;
[ObservableProperty]
private ColorScheme? _validationColorScheme;
[ObservableProperty]
private string _validationMessage;
public LoginViewModel ()
{
Username = string.Empty;
@@ -64,12 +61,6 @@ internal partial class LoginViewModel : ObservableObject
}
}
private void ValidateLogin ()
{
CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
SendMessage (LoginAction.Validation);
}
public string Username
{
get => _username;
@@ -81,6 +72,11 @@ internal partial class LoginViewModel : ObservableObject
}
}
public void Initialized ()
{
Clear ();
}
private void Clear ()
{
Username = string.Empty;
@@ -111,8 +107,9 @@ internal partial class LoginViewModel : ObservableObject
WeakReferenceMessenger.Default.Send (new Message<LoginAction> { Value = loginAction });
}
public void Initialized ()
private void ValidateLogin ()
{
Clear ();
CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
SendMessage (LoginAction.Validation);
}
}

View File

@@ -12,6 +12,7 @@ public static class Program
Services = ConfigureServices ();
Application.Init ();
Application.Run (Services.GetRequiredService<LoginView> ());
Application.Top.Dispose();
Application.Shutdown ();
}

View File

@@ -0,0 +1,180 @@
# CommunityToolkit.MVVM Example
This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient<T>` in conjunction with `Microsoft.Extensions.DependencyInjection`.
### Startup
Right away we use IoC to load our views and view models.
``` csharp
// As a public property for access further in the application if needed.
public static IServiceProvider Services { get; private set; }
. . .
// In Main
Services = ConfigureServices ();
. . .
private static IServiceProvider ConfigureServices ()
{
var services = new ServiceCollection ();
services.AddTransient<LoginView> ();
services.AddTransient<LoginViewModel> ();
return services.BuildServiceProvider ();
}
```
Now, we start the app and get our main view.
``` csharp
Application.Run (Services.GetRequiredService<LoginView> ());
```
Our view implements `IRecipient<T>` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.
``` csharp
internal partial class LoginView : IRecipient<Message<LoginAction>>
{
public LoginView (LoginViewModel viewModel)
{
// Initialize our Receive method
WeakReferenceMessenger.Default.Register (this);
...
ViewModel = viewModel;
...
passwordInput.TextChanged += (_, _) =>
{
ViewModel.Password = passwordInput.Text;
SetText ();
};
loginButton.Accept += (_, _) =>
{
if (!ViewModel.CanLogin) { return; }
ViewModel.LoginCommand.Execute (null);
};
...
Initialized += (_, _) => { ViewModel.Initialized (); };
}
...
}
```
Momentarily slipping over to the view model, all bindable properties use some form of `ObservableProperty` with the class deriving from `ObservableObject`. Commands are of the `RelayCommand` type.
``` csharp
internal partial class LoginViewModel : ObservableObject
{
...
[ObservableProperty]
private bool _canLogin;
private string _password;
...
public LoginViewModel ()
{
...
Password = string.Empty;
...
LoginCommand = new (Execute);
Clear ();
return;
async void Execute () { await Login (); }
}
...
public RelayCommand LoginCommand { get; }
public string Password
{
get => _password;
set
{
SetProperty (ref _password, value);
PasswordLengthMessage = $"_Password ({_password.Length} characters):";
ValidateLogin ();
}
}
```
The use of `WeakReferenceMessenger` provides one method of signaling the view from the view model. It's just one way to handle cross-thread messaging in this framework.
``` csharp
...
private async Task Login ()
{
SendMessage (LoginAction.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE);
await Task.Delay (TimeSpan.FromSeconds (1));
Clear ();
}
private void SendMessage (LoginAction loginAction, string message = "")
{
switch (loginAction)
{
case LoginAction.LoginProgress:
LoginProgressMessage = message;
break;
case LoginAction.Validation:
ValidationMessage = CanLogin ? VALID_LOGIN_MESSAGE : INVALID_LOGIN_MESSAGE;
ValidationColorScheme = CanLogin ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"];
break;
}
WeakReferenceMessenger.Default.Send (new Message<LoginAction> { Value = loginAction });
}
private void ValidateLogin ()
{
CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
SendMessage (LoginAction.Validation);
}
...
```
And the view's `Receive` function which provides an `Application.Refresh()` call to update the UI immediately.
``` csharp
public void Receive (Message<LoginAction> message)
{
switch (message.Value)
{
case LoginAction.LoginProgress:
{
loginProgressLabel.Text = ViewModel.LoginProgressMessage;
break;
}
case LoginAction.Validation:
{
validationLabel.Text = ViewModel.ValidationMessage;
validationLabel.ColorScheme = ViewModel.ValidationColorScheme;
break;
}
}
SetText();
Application.Refresh ();
}
```
There are other ways of handling cross-thread communication, this gives just one example.

View File

@@ -12,6 +12,7 @@ public static class Program
RxApp.MainThreadScheduler = TerminalScheduler.Default;
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
Application.Run (new LoginView (new LoginViewModel ()));
Application.Top.Dispose();
Application.Shutdown ();
}
}

View File

@@ -41,6 +41,11 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkitExample", "CommunityToolkitExample\CommunityToolkitExample.csproj", "{58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}"
EndProject
Global
GlobalSection(NestedProjects) = preSolution
{5DE91722-8765-4E2B-97E4-2A18010B5CED} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
@@ -50,6 +55,14 @@ Global
{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}.Release|Any CPU.Build.0 = Release|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Release|Any CPU.Build.0 = Release|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Release|Any CPU.Build.0 = Release|Any CPU
{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88979F89-9A42-448F-AE3E-3060145F6375}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88979F89-9A42-448F-AE3E-3060145F6375}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -66,14 +79,6 @@ Global
{B0A602CD-E176-449D-8663-64238D54F857}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0A602CD-E176-449D-8663-64238D54F857}.Release|Any CPU.Build.0 = Release|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DE91722-8765-4E2B-97E4-2A18010B5CED}.Release|Any CPU.Build.0 = Release|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD}.Release|Any CPU.Build.0 = Release|Any CPU
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -86,11 +91,6 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5DE91722-8765-4E2B-97E4-2A18010B5CED} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
{715DB4BA-F989-4DF6-B46F-5ED26A32B2DD} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2} = {CCADA0BC-61CF-4B4B-96BA-A3B0C0A7F54D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F8F8A4D-7B8D-4C2A-AC5E-CD7117F74C03}
EndGlobalSection