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.