Fixes #4046 - Moves examples into ./Examples and fixes ./Tests (#4047)

* 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:
Tig
2025-04-25 09:49:33 -06:00
committed by GitHub
parent dca3923491
commit 0baa881dc5
199 changed files with 149 additions and 142 deletions

View 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);
}
}

View 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; }
}

View 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 ();
}
}

View 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);
```

View 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>

View 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 ();
}
}

View 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);
}
}