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/LoginActions.cs b/CommunityToolkitExample/LoginActions.cs
new file mode 100644
index 000000000..3cbc6972c
--- /dev/null
+++ b/CommunityToolkitExample/LoginActions.cs
@@ -0,0 +1,8 @@
+namespace CommunityToolkitExample;
+
+internal enum LoginActions
+{
+ Clear,
+ 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..b0d891fa9
--- /dev/null
+++ b/CommunityToolkitExample/LoginView.cs
@@ -0,0 +1,72 @@
+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;
+ };
+ passwordInput.TextChanged += (_, _) =>
+ {
+ ViewModel.Password = passwordInput.Text;
+ };
+ loginButton.Accept += (_, _) =>
+ {
+ if (!ViewModel.CanLogin) { return; }
+ ViewModel.LoginCommand.Execute (null);
+ };
+
+ clearButton.Accept += (_, _) =>
+ {
+ ViewModel.ClearCommand.Execute (null);
+ };
+
+ Initialized += (_, _) => { ViewModel.Initialized (); };
+ }
+
+ public LoginViewModel ViewModel { get; set; }
+
+ public void Receive (Message message)
+ {
+ switch (message.Value)
+ {
+ case LoginActions.Clear:
+ {
+ loginProgressLabel.Text = ViewModel.LoginProgressMessage;
+ validationLabel.Text = ViewModel.ValidationMessage;
+ validationLabel.ColorScheme = ViewModel.ValidationColorScheme;
+ break;
+ }
+ case LoginActions.LoginProgress:
+ {
+ loginProgressLabel.Text = ViewModel.LoginProgressMessage;
+ break;
+ }
+ case LoginActions.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..8d48546bb
--- /dev/null
+++ b/CommunityToolkitExample/LoginViewModel.cs
@@ -0,0 +1,128 @@
+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 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!";
+
+ [ObservableProperty]
+ private bool _canLogin;
+
+ [ObservableProperty]
+ private string _loginProgressMessage;
+
+ private string _password;
+
+ [ObservableProperty]
+ private string _passwordLengthMessage;
+
+ private string _username;
+
+ [ObservableProperty]
+ private string _usernameLengthMessage;
+
+ [ObservableProperty]
+ private ColorScheme? _validationColorScheme;
+
+ [ObservableProperty]
+ private string _validationMessage;
+ public LoginViewModel ()
+ {
+ _loginProgressMessage = string.Empty;
+ _password = string.Empty;
+ _passwordLengthMessage = string.Empty;
+ _username = string.Empty;
+ _usernameLengthMessage = string.Empty;
+ _validationMessage = string.Empty;
+
+ 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 ();
+ }
+ }
+
+ public string Username
+ {
+ get => _username;
+ set
+ {
+ SetProperty (ref _username, value);
+ UsernameLengthMessage = $"_Username ({_username.Length} characters):";
+ ValidateLogin ();
+ }
+ }
+
+ public void Initialized ()
+ {
+ Clear ();
+ }
+
+ private void Clear ()
+ {
+ Username = string.Empty;
+ Password = string.Empty;
+ SendMessage (LoginActions.Clear, DEFAULT_LOGIN_PROGRESS_MESSAGE);
+ }
+
+ private async Task Login ()
+ {
+ SendMessage (LoginActions.LoginProgress, LOGGING_IN_PROGRESS_MESSAGE);
+ await Task.Delay (TimeSpan.FromSeconds (1));
+ Clear ();
+ }
+
+ private void SendMessage (LoginActions loginAction, string message = "")
+ {
+ switch (loginAction)
+ {
+ case LoginActions.Clear:
+ LoginProgressMessage = message;
+ ValidationMessage = INVALID_LOGIN_MESSAGE;
+ ValidationColorScheme = Colors.ColorSchemes ["Error"];
+ break;
+ case LoginActions.LoginProgress:
+ LoginProgressMessage = message;
+ break;
+ case LoginActions.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 (LoginActions.Validation);
+ }
+}
diff --git a/CommunityToolkitExample/Message.cs b/CommunityToolkitExample/Message.cs
new file mode 100644
index 000000000..f0e8ad530
--- /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..0d4f21c30
--- /dev/null
+++ b/CommunityToolkitExample/Program.cs
@@ -0,0 +1,26 @@
+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.Top.Dispose();
+ 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/CommunityToolkitExample/README.md b/CommunityToolkitExample/README.md
new file mode 100644
index 000000000..c3a712868
--- /dev/null
+++ b/CommunityToolkitExample/README.md
@@ -0,0 +1,154 @@
+# 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`.
+
+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);
+ };
+ ...
+ // Let the view model know the view is intialized.
+ 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. The use of `ObservableProperty` generates the code for handling `INotifyPropertyChanged` and `INotifyPropertyChanging`.
+
+``` 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 ();
+}
+```
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 98de83b41..ee838a41a 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -38,6 +38,8 @@ 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}
@@ -81,6 +83,10 @@ Global
{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