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