diff --git a/README.md b/README.md index ba59c4dd2..a2ef1bdb7 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ In addition, a complete Xterm/Vt100 terminal emulator that you can embed is now * **[Arbitrary Views](https://migueldeicaza.github.io/gui.cs/api/Terminal.Gui/Terminal.Gui.View.html)** - All visible UI elements are subclasses of the `View` class, and these in turn can contain an arbitrary number of sub-views. * **Advanced App Features** - The [Mainloop](https://migueldeicaza.github.io/gui.cs/api/Terminal.Gui/Mono.Terminal.MainLoop.html) supports processing events, idle handlers, timers, and monitoring file descriptors. +* **Reactive Extensions Support** - Use [reactive extensions](https://github.com/dotnet/reactive) and benefit from increased code readability, and the ability to apply the MVVM pattern and [ReactiveUI](https://www.reactiveui.net/) data bindings. See the [source code](https://github.com/migueldeicaza/gui.cs/tree/master/ReactiveExample) of a sample app in order to learn how to achieve this. ### Keyboard Input Handling diff --git a/ReactiveExample/LoginView.cs b/ReactiveExample/LoginView.cs index abd776ba1..c633d11ae 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -17,6 +17,7 @@ namespace ReactiveExample { .Append (PasswordInput ()) .Append (ValidationLabel ()) .Append (LoginButton ()) + .Append (ClearButton ()) .Append (LoginProgressLabel ()); } @@ -36,7 +37,7 @@ namespace ReactiveExample { usernameInput .Events () .TextChanged - .Select (old => usernameInput.Text.ToString ()) + .Select (old => usernameInput.Text) .DistinctUntilChanged () .BindTo (ViewModel, x => x.Username) .DisposeWith (_disposable); @@ -62,7 +63,7 @@ namespace ReactiveExample { passwordInput .Events () .TextChanged - .Select (old => passwordInput.Text.ToString ()) + .Select (old => passwordInput.Text) .DistinctUntilChanged () .BindTo (ViewModel, x => x.Password) .DisposeWith (_disposable); @@ -117,6 +118,16 @@ namespace ReactiveExample { .DisposeWith (_disposable); return loginButton; } + + Button ClearButton () { + var clearButton = new Button("Clear") { Width = 40 }; + clearButton + .Events () + .Clicked + .InvokeCommand (ViewModel, x => x.Clear) + .DisposeWith (_disposable); + return clearButton; + } object IViewFor.ViewModel { get => ViewModel; diff --git a/ReactiveExample/LoginViewModel.cs b/ReactiveExample/LoginViewModel.cs index 0b630eda1..bce330b8f 100644 --- a/ReactiveExample/LoginViewModel.cs +++ b/ReactiveExample/LoginViewModel.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Runtime.Serialization; using System.Threading.Tasks; +using NStack; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -18,8 +20,8 @@ namespace ReactiveExample { x => x.Username, x => x.Password, (username, password) => - !string.IsNullOrWhiteSpace (username) && - !string.IsNullOrWhiteSpace (password)); + !ustring.IsNullOrEmpty (username) && + !ustring.IsNullOrEmpty (password)); _isValid = canLogin.ToProperty (this, x => x.IsValid); Login = ReactiveCommand.CreateFromTask ( @@ -34,13 +36,19 @@ namespace ReactiveExample { .WhenAnyValue (x => x.Password) .Select (password => password.Length) .ToProperty (this, x => x.PasswordLength); + + Clear = ReactiveCommand.Create (() => { }); + Clear.Subscribe (unit => { + Username = ustring.Empty; + Password = ustring.Empty; + }); } [Reactive, DataMember] - public string Username { get; set; } = string.Empty; + public ustring Username { get; set; } = ustring.Empty; [Reactive, DataMember] - public string Password { get; set; } = string.Empty; + public ustring Password { get; set; } = ustring.Empty; [IgnoreDataMember] public int UsernameLength => _usernameLength.Value; @@ -51,6 +59,9 @@ namespace ReactiveExample { [IgnoreDataMember] public ReactiveCommand Login { get; } + [IgnoreDataMember] + public ReactiveCommand Clear { get; } + [IgnoreDataMember] public bool IsValid => _isValid.Value; } diff --git a/ReactiveExample/Program.cs b/ReactiveExample/Program.cs index 4f20602ce..4a8216a63 100644 --- a/ReactiveExample/Program.cs +++ b/ReactiveExample/Program.cs @@ -5,7 +5,7 @@ namespace ReactiveExample { public static class Program { static void Main (string [] args) { Application.Init (); // A hacky way to enable instant UI updates. - Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(600), a => true); + Application.MainLoop.AddTimeout(TimeSpan.FromSeconds(0.1), a => true); Application.Run (new LoginView (new LoginViewModel ())); } } diff --git a/ReactiveExample/README.md b/ReactiveExample/README.md new file mode 100644 index 000000000..bd8f760df --- /dev/null +++ b/ReactiveExample/README.md @@ -0,0 +1,35 @@ +This is a sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [Pharmacist](https://github.com/reactiveui/pharmacist) — a tool that converts all events in a NuGet package into observable wrappers. + + + +### Data Bindings + +If you wish to implement `OneWay` data binding, then use the `WhenAnyValue` [ReactiveUI extension method](https://www.reactiveui.net/docs/handbook/when-any/) that listens to `INotifyPropertyChanged` events of the specified property, and converts that events into `IObservable`: + +```cs +// 'usernameInput' is 'TextField' +ViewModel + .WhenAnyValue (x => x.Username) + .BindTo (usernameInput, x => x.Text); +``` + +Note that your view model should implement `INotifyPropertyChanged` or inherit from a `ReactiveObject`. If you wish to implement `OneWayToSource` data binding, then install [Pharmacist.MSBuild](https://github.com/reactiveui/pharmacist) into your project and listen to e.g. `TextChanged` event of a `TextField`: + +```cs +// 'usernameInput' is 'TextField' +usernameInput + .Events () // The Events() extension is generated by Pharmacist. + .TextChanged + .Select (old => usernameInput.Text) + .DistinctUntilChanged () + .BindTo (ViewModel, x => x.Username); +``` + +If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` data binding. Also be sure to use the `ustring` type instead of the `string` type. Invoking commands should be as simple as this: +```cs +// 'clearButton' is 'Button' +clearButton + .Events () + .Clicked + .InvokeCommand (ViewModel, x => x.Clear); +``` \ No newline at end of file