From b82cc67f54809df4d4d58605cde0fd094375a72e Mon Sep 17 00:00:00 2001
From: John Baughman <1634414+johnmbaughman@users.noreply.github.com>
Date: Wed, 12 Jun 2024 20:51:50 -0500
Subject: [PATCH 1/4] Add Community Toolkit example
---
.../CommunityToolkitExample.csproj | 19 +++
CommunityToolkitExample/LoginAction.cs | 7 ++
CommunityToolkitExample/LoginView.Designer.cs | 62 +++++++++
CommunityToolkitExample/LoginView.cs | 68 ++++++++++
CommunityToolkitExample/LoginViewModel.cs | 118 ++++++++++++++++++
CommunityToolkitExample/Message.cs | 6 +
CommunityToolkitExample/Program.cs | 25 ++++
Terminal.sln | 32 +++--
8 files changed, 324 insertions(+), 13 deletions(-)
create mode 100644 CommunityToolkitExample/CommunityToolkitExample.csproj
create mode 100644 CommunityToolkitExample/LoginAction.cs
create mode 100644 CommunityToolkitExample/LoginView.Designer.cs
create mode 100644 CommunityToolkitExample/LoginView.cs
create mode 100644 CommunityToolkitExample/LoginViewModel.cs
create mode 100644 CommunityToolkitExample/Message.cs
create mode 100644 CommunityToolkitExample/Program.cs
diff --git a/CommunityToolkitExample/CommunityToolkitExample.csproj b/CommunityToolkitExample/CommunityToolkitExample.csproj
new file mode 100644
index 000000000..fdb41dfcd
--- /dev/null
+++ b/CommunityToolkitExample/CommunityToolkitExample.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CommunityToolkitExample/LoginAction.cs b/CommunityToolkitExample/LoginAction.cs
new file mode 100644
index 000000000..2fd8d4a45
--- /dev/null
+++ b/CommunityToolkitExample/LoginAction.cs
@@ -0,0 +1,7 @@
+namespace CommunityToolkitExample;
+
+internal enum LoginAction
+{
+ Validation,
+ LoginProgress
+}
diff --git a/CommunityToolkitExample/LoginView.Designer.cs b/CommunityToolkitExample/LoginView.Designer.cs
new file mode 100644
index 000000000..e1bddff45
--- /dev/null
+++ b/CommunityToolkitExample/LoginView.Designer.cs
@@ -0,0 +1,62 @@
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginView : Window
+{
+ private Label titleLabel;
+ private Label usernameLengthLabel;
+ private TextField usernameInput;
+ private Label passwordLengthLabel;
+ private TextField passwordInput;
+ private Label validationLabel;
+ private Button loginButton;
+ private Button clearButton;
+ private Label loginProgressLabel;
+
+ private void InitializeComponent ()
+ {
+ titleLabel = new Label ();
+ titleLabel.Text = "Login Form";
+ Add (titleLabel);
+ usernameLengthLabel = new Label ();
+ usernameLengthLabel.X = Pos.Left (titleLabel);
+ usernameLengthLabel.Y = Pos.Top (titleLabel) + 1;
+ Add (usernameLengthLabel);
+ usernameInput = new TextField ();
+ usernameInput.X = Pos.Right (usernameLengthLabel) + 1;
+ usernameInput.Y = Pos.Top (usernameLengthLabel);
+ usernameInput.Width = 40;
+ Add (usernameInput);
+ passwordLengthLabel = new Label ();
+ passwordLengthLabel.X = Pos.Left (usernameLengthLabel);
+ passwordLengthLabel.Y = Pos.Top (usernameLengthLabel) + 1;
+ Add (passwordLengthLabel);
+ passwordInput = new TextField ();
+ passwordInput.X = Pos.Right (passwordLengthLabel) + 1;
+ passwordInput.Y = Pos.Top (passwordLengthLabel);
+ passwordInput.Width = 40;
+ passwordInput.Secret = true;
+ Add (passwordInput);
+ validationLabel = new Label ();
+ validationLabel.X = Pos.Left (passwordInput);
+ validationLabel.Y = Pos.Top (passwordInput) + 1;
+ Add (validationLabel);
+ loginButton = new Button ();
+ loginButton.X = Pos.Left (validationLabel);
+ loginButton.Y = Pos.Top (validationLabel) + 1;
+ loginButton.Text = "_Login";
+ Add (loginButton);
+ clearButton = new Button ();
+ clearButton.X = Pos.Left (loginButton);
+ clearButton.Y = Pos.Top (loginButton) + 1;
+ clearButton.Text = "_Clear";
+ Add (clearButton);
+ loginProgressLabel = new Label ();
+ loginProgressLabel.X = Pos.Left (clearButton);
+ loginProgressLabel.Y = Pos.Top (clearButton) + 1;
+ loginProgressLabel.Width = 40;
+ loginProgressLabel.Height = 1;
+ Add (loginProgressLabel);
+ }
+}
diff --git a/CommunityToolkitExample/LoginView.cs b/CommunityToolkitExample/LoginView.cs
new file mode 100644
index 000000000..262928e35
--- /dev/null
+++ b/CommunityToolkitExample/LoginView.cs
@@ -0,0 +1,68 @@
+using CommunityToolkit.Mvvm.Messaging;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginView : IRecipient>
+{
+ public LoginView (LoginViewModel viewModel)
+ {
+ WeakReferenceMessenger.Default.Register (this);
+ Title = $"Community Toolkit MVVM Example - {Application.QuitKey} to Exit";
+ ViewModel = viewModel;
+ InitializeComponent ();
+ usernameInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Username = usernameInput.Text;
+ SetText ();
+ };
+ passwordInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Password = passwordInput.Text;
+ SetText ();
+ };
+ loginButton.Accept += (_, _) =>
+ {
+ if (!ViewModel.CanLogin) { return; }
+ ViewModel.LoginCommand.Execute (null);
+ };
+
+ clearButton.Accept += (_, _) =>
+ {
+ ViewModel.ClearCommand.Execute (null);
+ SetText ();
+ };
+
+ Initialized += (_, _) => { ViewModel.Initialized (); };
+ }
+
+ public LoginViewModel ViewModel { get; set; }
+
+ public void Receive (Message 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 ();
+ }
+
+ private void SetText ()
+ {
+ usernameInput.Text = ViewModel.Username;
+ usernameLengthLabel.Text = ViewModel.UsernameLengthMessage;
+ passwordInput.Text = ViewModel.Password;
+ passwordLengthLabel.Text = ViewModel.PasswordLengthMessage;
+ }
+}
\ No newline at end of file
diff --git a/CommunityToolkitExample/LoginViewModel.cs b/CommunityToolkitExample/LoginViewModel.cs
new file mode 100644
index 000000000..b5fbfa32f
--- /dev/null
+++ b/CommunityToolkitExample/LoginViewModel.cs
@@ -0,0 +1,118 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.Mvvm.Messaging;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+internal partial class LoginViewModel : ObservableObject
+{
+ private const string DEFAULT_LOGIN_PROGRESS_MESSAGE = "Press 'Login' to log in.";
+ 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;
+
+ private string _password;
+
+ [ObservableProperty]
+ private string _passwordLengthMessage;
+
+ private string _username;
+
+ [ObservableProperty]
+ private string _usernameLengthMessage;
+
+ [ObservableProperty]
+ private string _loginProgressMessage;
+
+ [ObservableProperty]
+ private string _validationMessage;
+
+ [ObservableProperty]
+ private ColorScheme? _validationColorScheme;
+
+ public LoginViewModel ()
+ {
+ Username = string.Empty;
+ Password = string.Empty;
+
+ ClearCommand = new (Clear);
+ LoginCommand = new (Execute);
+
+ Clear ();
+
+ return;
+
+ async void Execute () { await Login (); }
+ }
+
+ public RelayCommand ClearCommand { get; }
+
+ public RelayCommand LoginCommand { get; }
+
+ public string Password
+ {
+ get => _password;
+ set
+ {
+ SetProperty (ref _password, value);
+ PasswordLengthMessage = $"_Password ({_password.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+
+ private void ValidateLogin ()
+ {
+ CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
+ SendMessage (LoginAction.Validation);
+ }
+
+ public string Username
+ {
+ get => _username;
+ set
+ {
+ SetProperty (ref _username, value);
+ UsernameLengthMessage = $"_Username ({_username.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+
+ private void Clear ()
+ {
+ Username = string.Empty;
+ Password = string.Empty;
+ SendMessage (LoginAction.Validation);
+ SendMessage (LoginAction.LoginProgress, DEFAULT_LOGIN_PROGRESS_MESSAGE);
+ }
+
+ 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 { Value = loginAction });
+ }
+
+ public void Initialized ()
+ {
+ Clear ();
+ }
+}
diff --git a/CommunityToolkitExample/Message.cs b/CommunityToolkitExample/Message.cs
new file mode 100644
index 000000000..a1081512e
--- /dev/null
+++ b/CommunityToolkitExample/Message.cs
@@ -0,0 +1,6 @@
+namespace CommunityToolkitExample;
+
+internal class Message
+{
+ public T Value { get; set; }
+}
diff --git a/CommunityToolkitExample/Program.cs b/CommunityToolkitExample/Program.cs
new file mode 100644
index 000000000..0a557fc47
--- /dev/null
+++ b/CommunityToolkitExample/Program.cs
@@ -0,0 +1,25 @@
+using Microsoft.Extensions.DependencyInjection;
+using Terminal.Gui;
+
+namespace CommunityToolkitExample;
+
+public static class Program
+{
+ public static IServiceProvider Services { get; private set; }
+
+ private static void Main (string [] args)
+ {
+ Services = ConfigureServices ();
+ Application.Init ();
+ Application.Run (Services.GetRequiredService ());
+ Application.Shutdown ();
+ }
+
+ private static IServiceProvider ConfigureServices ()
+ {
+ var services = new ServiceCollection ();
+ services.AddTransient ();
+ services.AddTransient ();
+ return services.BuildServiceProvider ();
+ }
+}
\ No newline at end of file
diff --git a/Terminal.sln b/Terminal.sln
index 98de83b41..7dbb5432a 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -38,12 +38,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui.Analyzers.Inte
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui.Analyzers.Internal.Debugging", "Analyzers\Terminal.Gui.Analyzers.Internal.Debugging\Terminal.Gui.Analyzers.Internal.Debugging.csproj", "{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}"
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
@@ -53,14 +50,6 @@ 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
@@ -77,14 +66,31 @@ 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
{C2AD09BD-D579-45A7-ACA3-E4EF3BC027D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58FDCA8F-08F7-4D80-9DA3-6A9AED01E163}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
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
From 182b63da929647099285beed7a940da515709686 Mon Sep 17 00:00:00 2001
From: John Baughman <1634414+johnmbaughman@users.noreply.github.com>
Date: Thu, 13 Jun 2024 09:24:27 -0500
Subject: [PATCH 2/4] added recommendations; fixed .sln
---
CommunityToolkitExample/LoginViewModel.cs | 31 ++--
CommunityToolkitExample/Program.cs | 1 +
CommunityToolkitExample/README.md | 180 ++++++++++++++++++++++
ReactiveExample/Program.cs | 1 +
Terminal.sln | 26 ++--
5 files changed, 209 insertions(+), 30 deletions(-)
create mode 100644 CommunityToolkitExample/README.md
diff --git a/CommunityToolkitExample/LoginViewModel.cs b/CommunityToolkitExample/LoginViewModel.cs
index b5fbfa32f..2de4688a3 100644
--- a/CommunityToolkitExample/LoginViewModel.cs
+++ b/CommunityToolkitExample/LoginViewModel.cs
@@ -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 { Value = loginAction });
}
- public void Initialized ()
+ private void ValidateLogin ()
{
- Clear ();
+ CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
+ SendMessage (LoginAction.Validation);
}
}
diff --git a/CommunityToolkitExample/Program.cs b/CommunityToolkitExample/Program.cs
index 0a557fc47..96c43a14a 100644
--- a/CommunityToolkitExample/Program.cs
+++ b/CommunityToolkitExample/Program.cs
@@ -12,6 +12,7 @@ public static class Program
Services = ConfigureServices ();
Application.Init ();
Application.Run (Services.GetRequiredService ());
+ Application.Top.Dispose();
Application.Shutdown ();
}
diff --git a/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md
new file mode 100644
index 000000000..254ca9bc4
--- /dev/null
+++ b/CommunityToolkitExample/README.md
@@ -0,0 +1,180 @@
+# CommunityToolkit.MVVM Example
+
+This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient` 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 ();
+ services.AddTransient ();
+ return services.BuildServiceProvider ();
+}
+```
+
+Now, we start the app and get our main view.
+
+``` csharp
+Application.Run (Services.GetRequiredService ());
+```
+
+Our view implements `IRecipient` to demonstrate the use of the `WeakReferenceMessenger`. The binding of the view events is then created.
+
+``` csharp
+internal partial class LoginView : IRecipient>
+{
+ 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 { 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 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.
\ No newline at end of file
diff --git a/ReactiveExample/Program.cs b/ReactiveExample/Program.cs
index 2b5105b8c..2bbc6667b 100644
--- a/ReactiveExample/Program.cs
+++ b/ReactiveExample/Program.cs
@@ -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 ();
}
}
diff --git a/Terminal.sln b/Terminal.sln
index 7dbb5432a..ee838a41a 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -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
From 35379e8c19838306271d889b73ed71461cc52bb4 Mon Sep 17 00:00:00 2001
From: "John M. Baughman" <1634414+johnmbaughman@users.noreply.github.com>
Date: Thu, 13 Jun 2024 09:31:54 -0500
Subject: [PATCH 3/4] Update README.md
---
CommunityToolkitExample/README.md | 31 +++++--------------------------
1 file changed, 5 insertions(+), 26 deletions(-)
diff --git a/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md
index 254ca9bc4..24c8663d8 100644
--- a/CommunityToolkitExample/README.md
+++ b/CommunityToolkitExample/README.md
@@ -9,14 +9,10 @@ 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 ();
@@ -41,13 +37,9 @@ internal partial class LoginView : IRecipient>
{
// Initialize our Receive method
WeakReferenceMessenger.Default.Register (this);
-
...
-
ViewModel = viewModel;
-
...
-
passwordInput.TextChanged += (_, _) =>
{
ViewModel.Password = passwordInput.Text;
@@ -58,14 +50,11 @@ internal partial class LoginView : IRecipient>
if (!ViewModel.CanLogin) { return; }
ViewModel.LoginCommand.Execute (null);
};
-
...
-
+ // Let the view model know the view is intialized.
Initialized += (_, _) => { ViewModel.Initialized (); };
}
-
...
-
}
```
@@ -76,22 +65,16 @@ Momentarily slipping over to the view model, all bindable properties use some fo
internal partial class LoginViewModel : ObservableObject
{
...
-
[ObservableProperty]
private bool _canLogin;
private string _password;
-
...
-
public LoginViewModel ()
{
...
-
Password = string.Empty;
-
- ...
-
+ ...
LoginCommand = new (Execute);
Clear ();
@@ -100,9 +83,7 @@ internal partial class LoginViewModel : ObservableObject
async void Execute () { await Login (); }
}
-
...
-
public RelayCommand LoginCommand { get; }
public string Password
@@ -121,7 +102,6 @@ The use of `WeakReferenceMessenger` provides one method of signaling the view fr
``` csharp
...
-
private async Task Login ()
{
SendMessage (LoginAction.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE);
@@ -149,7 +129,6 @@ private void ValidateLogin ()
CanLogin = !string.IsNullOrEmpty (Username) && !string.IsNullOrEmpty (Password);
SendMessage (LoginAction.Validation);
}
-
...
```
@@ -177,4 +156,4 @@ public void Receive (Message message)
}
```
-There are other ways of handling cross-thread communication, this gives just one example.
\ No newline at end of file
+There are other ways of handling cross-thread communication, this gives just one example.
From 5709843d8f2f6bfe3c3f94c57bcefce34696703f Mon Sep 17 00:00:00 2001
From: "John M. Baughman" <1634414+johnmbaughman@users.noreply.github.com>
Date: Thu, 13 Jun 2024 09:54:00 -0500
Subject: [PATCH 4/4] Update README.md
---
CommunityToolkitExample/README.md | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md
index 24c8663d8..c3a712868 100644
--- a/CommunityToolkitExample/README.md
+++ b/CommunityToolkitExample/README.md
@@ -2,8 +2,6 @@
This small demo gives an example of using the `CommunityToolkit.MVVM` framework's `ObservableObject`, `ObservableProperty`, and `IRecipient` in conjunction with `Microsoft.Extensions.DependencyInjection`.
-### Startup
-
Right away we use IoC to load our views and view models.
``` csharp
@@ -58,10 +56,9 @@ internal partial class LoginView : IRecipient>
}
```
-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.
+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. The use of `ObservableProperty` generates the code for handling `INotifyPropertyChanged` and `INotifyPropertyChanging`.
``` csharp
-
internal partial class LoginViewModel : ObservableObject
{
...
@@ -155,5 +152,3 @@ public void Receive (Message message)
Application.Refresh ();
}
```
-
-There are other ways of handling cross-thread communication, this gives just one example.