mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +01:00
* touching publish.yml * Moved Examples into ./Examples * Moved Benchmarks into ./Tests * Moved Benchmarks into ./Tests * Moved UICatalog into ./Examples * Moved UICatalog into ./Examples 2 * Moved tests into ./Tests * Updated nuget
This commit is contained in:
157
Examples/ReactiveExample/LoginView.cs
Normal file
157
Examples/ReactiveExample/LoginView.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using ReactiveMarbles.ObservableEvents;
|
||||
using ReactiveUI;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace ReactiveExample;
|
||||
|
||||
public class LoginView : Window, IViewFor<LoginViewModel>
|
||||
{
|
||||
private const string SuccessMessage = "The input is valid!";
|
||||
private const string ErrorMessage = "Please enter a valid user name and password.";
|
||||
private const string ProgressMessage = "Logging in...";
|
||||
private const string IdleMessage = "Press 'Login' to log in.";
|
||||
|
||||
private readonly CompositeDisposable _disposable = [];
|
||||
|
||||
public LoginView (LoginViewModel viewModel)
|
||||
{
|
||||
Title = $"Reactive Extensions Example - {Application.QuitKey} to Exit";
|
||||
ViewModel = viewModel;
|
||||
var title = this.AddControl<Label> (x => x.Text = "Login Form");
|
||||
var unLengthLabel = title.AddControlAfter<Label> ((previous, unLength) =>
|
||||
{
|
||||
unLength.X = Pos.Left (previous);
|
||||
unLength.Y = Pos.Top (previous) + 1;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.UsernameLength)
|
||||
.Select (length => $"_Username ({length} characters):")
|
||||
.BindTo (unLength, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
});
|
||||
unLengthLabel.AddControlAfter<TextField> ((previous, unInput) =>
|
||||
{
|
||||
unInput.X = Pos.Right (previous) + 1;
|
||||
unInput.Y = Pos.Top (previous);
|
||||
unInput.Width = 40;
|
||||
unInput.Text = ViewModel.Username;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.Username)
|
||||
.BindTo (unInput, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
|
||||
unInput
|
||||
.Events ()
|
||||
.TextChanged
|
||||
.Select (_ => unInput.Text)
|
||||
.DistinctUntilChanged ()
|
||||
.BindTo (ViewModel, x => x.Username)
|
||||
.DisposeWith (_disposable);
|
||||
});
|
||||
unLengthLabel.AddControlAfter<Label> ((previous, pwLength) =>
|
||||
{
|
||||
pwLength.X = Pos.Left (previous);
|
||||
pwLength.Y = Pos.Top (previous) + 1;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.PasswordLength)
|
||||
.Select (length => $"_Password ({length} characters):")
|
||||
.BindTo (pwLength, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
})
|
||||
.AddControlAfter<TextField> ((previous, pwInput) =>
|
||||
{
|
||||
pwInput.X = Pos.Right (previous) + 1;
|
||||
pwInput.Y = Pos.Top (previous);
|
||||
pwInput.Width = 40;
|
||||
pwInput.Text = ViewModel.Password;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.Password)
|
||||
.BindTo (pwInput, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
|
||||
pwInput
|
||||
.Events ()
|
||||
.TextChanged
|
||||
.Select (_ => pwInput.Text)
|
||||
.DistinctUntilChanged ()
|
||||
.BindTo (ViewModel, x => x.Password)
|
||||
.DisposeWith (_disposable);
|
||||
})
|
||||
.AddControlAfter<Label> ((previous, validation) =>
|
||||
{
|
||||
validation.X = Pos.Left (previous);
|
||||
validation.Y = Pos.Top (previous) + 1;
|
||||
validation.Text = ErrorMessage;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.IsValid)
|
||||
.Select (valid => valid ? SuccessMessage : ErrorMessage)
|
||||
.BindTo (validation, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
|
||||
ViewModel
|
||||
.WhenAnyValue (x => x.IsValid)
|
||||
.Select (valid => valid ? Colors.ColorSchemes ["Base"] : Colors.ColorSchemes ["Error"])
|
||||
.BindTo (validation, x => x.ColorScheme)
|
||||
.DisposeWith (_disposable);
|
||||
})
|
||||
.AddControlAfter<Button> ((previous, login) =>
|
||||
{
|
||||
login.X = Pos.Left (previous);
|
||||
login.Y = Pos.Top (previous) + 1;
|
||||
login.Text = "_Login";
|
||||
|
||||
login
|
||||
.Events ()
|
||||
.Accepting
|
||||
.InvokeCommand (ViewModel, x => x.Login)
|
||||
.DisposeWith (_disposable);
|
||||
})
|
||||
.AddControlAfter<Button> ((previous, clear) =>
|
||||
{
|
||||
clear.X = Pos.Left (previous);
|
||||
clear.Y = Pos.Top (previous) + 1;
|
||||
clear.Text = "_Clear";
|
||||
|
||||
clear
|
||||
.Events ()
|
||||
.Accepting
|
||||
.InvokeCommand (ViewModel, x => x.ClearCommand)
|
||||
.DisposeWith (_disposable);
|
||||
})
|
||||
.AddControlAfter<Label> ((previous, progress) =>
|
||||
{
|
||||
progress.X = Pos.Left (previous);
|
||||
progress.Y = Pos.Top (previous) + 1;
|
||||
progress.Width = 40;
|
||||
progress.Height = 1;
|
||||
progress.Text = IdleMessage;
|
||||
|
||||
ViewModel
|
||||
.WhenAnyObservable (x => x.Login.IsExecuting)
|
||||
.Select (executing => executing ? ProgressMessage : IdleMessage)
|
||||
.ObserveOn (RxApp.MainThreadScheduler)
|
||||
.BindTo (progress, x => x.Text)
|
||||
.DisposeWith (_disposable);
|
||||
});
|
||||
}
|
||||
|
||||
public LoginViewModel ViewModel { get; set; }
|
||||
|
||||
object IViewFor.ViewModel
|
||||
{
|
||||
get => ViewModel;
|
||||
set => ViewModel = (LoginViewModel)value;
|
||||
}
|
||||
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
_disposable.Dispose ();
|
||||
base.Dispose (disposing);
|
||||
}
|
||||
}
|
||||
83
Examples/ReactiveExample/LoginViewModel.cs
Normal file
83
Examples/ReactiveExample/LoginViewModel.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace ReactiveExample;
|
||||
|
||||
//
|
||||
// This view model can be easily shared across different UI frameworks.
|
||||
// For example, if you have a WPF or XF app with view models written
|
||||
// this way, you can easily port your app to Terminal.Gui by implementing
|
||||
// the views with Terminal.Gui classes and ReactiveUI bindings.
|
||||
//
|
||||
// We mark the view model with the [DataContract] attributes and this
|
||||
// allows you to save the view model class to the disk, and then to read
|
||||
// the view model from the disk, making your app state persistent.
|
||||
// See also: https://www.reactiveui.net../docs/handbook/data-persistence/
|
||||
//
|
||||
[DataContract]
|
||||
public partial class LoginViewModel : ReactiveObject
|
||||
{
|
||||
[IgnoreDataMember]
|
||||
[ObservableAsProperty] private bool _isValid;
|
||||
|
||||
[IgnoreDataMember]
|
||||
[ObservableAsProperty] private int _passwordLength;
|
||||
|
||||
[IgnoreDataMember]
|
||||
[ObservableAsProperty] private int _usernameLength;
|
||||
|
||||
[DataMember]
|
||||
[Reactive] private string _password = string.Empty;
|
||||
|
||||
[DataMember]
|
||||
[Reactive] private string _username = string.Empty;
|
||||
|
||||
public LoginViewModel ()
|
||||
{
|
||||
IObservable<bool> canLogin = this.WhenAnyValue
|
||||
(
|
||||
x => x.Username,
|
||||
x => x.Password,
|
||||
(username, password) =>
|
||||
!string.IsNullOrEmpty (username) && !string.IsNullOrEmpty (password)
|
||||
);
|
||||
|
||||
_isValidHelper = canLogin.ToProperty (this, x => x.IsValid);
|
||||
|
||||
Login = ReactiveCommand.CreateFromTask<HandledEventArgs>
|
||||
(
|
||||
e => Task.Delay (TimeSpan.FromSeconds (1)),
|
||||
canLogin
|
||||
);
|
||||
|
||||
_usernameLengthHelper = this
|
||||
.WhenAnyValue (x => x.Username)
|
||||
.Select (name => name.Length)
|
||||
.ToProperty (this, x => x.UsernameLength);
|
||||
|
||||
_passwordLengthHelper = this
|
||||
.WhenAnyValue (x => x.Password)
|
||||
.Select (password => password.Length)
|
||||
.ToProperty (this, x => x.PasswordLength);
|
||||
|
||||
ClearCommand.Subscribe (
|
||||
unit =>
|
||||
{
|
||||
Username = string.Empty;
|
||||
Password = string.Empty;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[ReactiveCommand]
|
||||
public void Clear (HandledEventArgs args) { }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public ReactiveCommand<HandledEventArgs, Unit> Login { get; }
|
||||
}
|
||||
18
Examples/ReactiveExample/Program.cs
Normal file
18
Examples/ReactiveExample/Program.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using ReactiveUI;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace ReactiveExample;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private static void Main (string [] args)
|
||||
{
|
||||
Application.Init ();
|
||||
RxApp.MainThreadScheduler = TerminalScheduler.Default;
|
||||
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
|
||||
Application.Run (new LoginView (new LoginViewModel ()));
|
||||
Application.Top.Dispose ();
|
||||
Application.Shutdown ();
|
||||
}
|
||||
}
|
||||
48
Examples/ReactiveExample/README.md
Normal file
48
Examples/ReactiveExample/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
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 [ObservableEvents](https://github.com/reactivemarbles/ObservableEvents) — a Source Generator that turns events into observable wrappers.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/6759207/94748621-646a7280-038a-11eb-8ea0-34629dc799b3.gif" width="450">
|
||||
|
||||
### Scheduling
|
||||
|
||||
In order to use reactive extensions scheduling, copy-paste the `TerminalScheduler.cs` file into your project, and add the following lines to the composition root of your `Terminal.Gui` application:
|
||||
|
||||
```cs
|
||||
Application.Init ();
|
||||
RxApp.MainThreadScheduler = TerminalScheduler.Default;
|
||||
RxApp.TaskpoolScheduler = TaskPoolScheduler.Default;
|
||||
Application.Run (new RootView (new RootViewModel ()));
|
||||
```
|
||||
|
||||
From now on, you can use `.ObserveOn(RxApp.MainThreadScheduler)` to return to the main loop from a background thread. This is useful when you have a `IObservable<TValue>` updated from a background thread, and you wish to update the UI with `TValue`s received from that observable.
|
||||
|
||||
### 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<TProperty>`:
|
||||
|
||||
```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 `string` 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);
|
||||
```
|
||||
20
Examples/ReactiveExample/ReactiveExample.csproj
Normal file
20
Examples/ReactiveExample/ReactiveExample.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<!-- Version numbers are automatically updated by gitversion when a release is released -->
|
||||
<!-- In the source tree the version will always be 2.0 for all projects. -->
|
||||
<!-- Do not modify these. -->
|
||||
<AssemblyVersion>2.0</AssemblyVersion>
|
||||
<FileVersion>2.0</FileVersion>
|
||||
<Version>2.0</Version>
|
||||
<InformationalVersion>2.0</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ReactiveUI" />
|
||||
<PackageReference Include="ReactiveMarbles.ObservableEvents.SourceGenerator" PrivateAssets="all" />
|
||||
<PackageReference Include="ReactiveUI.SourceGenerators" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
60
Examples/ReactiveExample/TerminalScheduler.cs
Normal file
60
Examples/ReactiveExample/TerminalScheduler.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Disposables;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace ReactiveExample;
|
||||
|
||||
public class TerminalScheduler : LocalScheduler
|
||||
{
|
||||
public static readonly TerminalScheduler Default = new ();
|
||||
private TerminalScheduler () { }
|
||||
|
||||
public override IDisposable Schedule<TState> (
|
||||
TState state,
|
||||
TimeSpan dueTime,
|
||||
Func<IScheduler, TState, IDisposable> action
|
||||
)
|
||||
{
|
||||
IDisposable PostOnMainLoop ()
|
||||
{
|
||||
var composite = new CompositeDisposable (2);
|
||||
var cancellation = new CancellationDisposable ();
|
||||
|
||||
Application.Invoke (
|
||||
() =>
|
||||
{
|
||||
if (!cancellation.Token.IsCancellationRequested)
|
||||
{
|
||||
composite.Add (action (this, state));
|
||||
}
|
||||
}
|
||||
);
|
||||
composite.Add (cancellation);
|
||||
|
||||
return composite;
|
||||
}
|
||||
|
||||
IDisposable PostOnMainLoopAsTimeout ()
|
||||
{
|
||||
var composite = new CompositeDisposable (2);
|
||||
|
||||
object timeout = Application.AddTimeout (
|
||||
dueTime,
|
||||
() =>
|
||||
{
|
||||
composite.Add (action (this, state));
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
composite.Add (Disposable.Create (() => Application.RemoveTimeout (timeout)));
|
||||
|
||||
return composite;
|
||||
}
|
||||
|
||||
return dueTime == TimeSpan.Zero
|
||||
? PostOnMainLoop ()
|
||||
: PostOnMainLoopAsTimeout ();
|
||||
}
|
||||
}
|
||||
24
Examples/ReactiveExample/ViewExtensions.cs
Normal file
24
Examples/ReactiveExample/ViewExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using Terminal.Gui;
|
||||
|
||||
namespace ReactiveExample;
|
||||
public static class ViewExtensions
|
||||
{
|
||||
public static (Window MainView, TOut LastControl) AddControl<TOut> (this Window view, Action<TOut> action)
|
||||
where TOut : View, new()
|
||||
{
|
||||
TOut result = new ();
|
||||
action (result);
|
||||
view.Add (result);
|
||||
return (view, result);
|
||||
}
|
||||
|
||||
public static (Window MainView, TOut LastControl) AddControlAfter<TOut> (this (Window MainView, View LastControl) view, Action<View, TOut> action)
|
||||
where TOut : View, new()
|
||||
{
|
||||
TOut result = new ();
|
||||
action (view.LastControl, result);
|
||||
view.MainView.Add (result);
|
||||
return (view.MainView, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user