Skip to content

Commit

Permalink
feature: Add IReactiveCommand generic interface to allow for generic …
Browse files Browse the repository at this point in the history
…variants (#3583)

* feature: Add IReactiveCommand generic interface to allow for generic variants

* Update documentation

* Fix api tests

* One more fix
  • Loading branch information
glennawatson authored Jul 13, 2023
1 parent c074228 commit a68adcb
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 81 deletions.
2 changes: 1 addition & 1 deletion src/Directory.build.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<PackageReference Include="Microsoft.Reactive.Testing" Version="6.0.0" />
<PackageReference Include="PublicApiGenerator" Version="11.0.0" />
<PackageReference Include="coverlet.msbuild" Version="6.0.0" PrivateAssets="All" />
<PackageReference Include="Verify.Xunit" Version="20.4.0" />
<PackageReference Include="Verify.Xunit" Version="20.5.0" />
</ItemGroup>

<ItemGroup Condition="'$(IsTestProject)' != 'true'">
Expand Down
2 changes: 1 addition & 1 deletion src/ReactiveUI.Blazor/ReactiveUI.Blazor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</ItemGroup>

<ItemGroup Condition=" $(TargetFramework.StartsWith('net7')) ">
<PackageReference Include="Microsoft.AspNetCore.Components" Version="7.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="7.0.9" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ namespace ReactiveUI
System.IObservable<bool> CanExecute { get; }
System.IObservable<bool> IsExecuting { get; }
}
public interface IReactiveCommand<in TParam, out TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, System.IDisposable, System.IObservable<TResult>
{
System.IObservable<TResult> Execute();
System.IObservable<TResult> Execute(TParam parameter);
}
public interface IReactiveNotifyPropertyChanged<out TSender>
{
System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<TSender>> Changed { get; }
Expand Down Expand Up @@ -645,7 +650,7 @@ namespace ReactiveUI
public static ReactiveUI.ReactiveCommand<System.Reactive.Unit, TResult> CreateRunInBackground<TResult>(System.Func<TResult> execute, System.IObservable<bool>? canExecute = null, System.Reactive.Concurrency.IScheduler? backgroundScheduler = null, System.Reactive.Concurrency.IScheduler? outputScheduler = null) { }
public static ReactiveUI.ReactiveCommand<TParam, TResult> CreateRunInBackground<TParam, TResult>(System.Func<TParam, TResult> execute, System.IObservable<bool>? canExecute = null, System.Reactive.Concurrency.IScheduler? backgroundScheduler = null, System.Reactive.Concurrency.IScheduler? outputScheduler = null) { }
}
public abstract class ReactiveCommandBase<TParam, TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, System.IDisposable, System.IObservable<TResult>, System.Windows.Input.ICommand
public abstract class ReactiveCommandBase<TParam, TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, ReactiveUI.IReactiveCommand<TParam, TResult>, System.IDisposable, System.IObservable<TResult>, System.Windows.Input.ICommand
{
protected ReactiveCommandBase() { }
public abstract System.IObservable<bool> CanExecute { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,11 @@ namespace ReactiveUI
System.IObservable<bool> CanExecute { get; }
System.IObservable<bool> IsExecuting { get; }
}
public interface IReactiveCommand<in TParam, out TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, System.IDisposable, System.IObservable<TResult>
{
System.IObservable<TResult> Execute();
System.IObservable<TResult> Execute(TParam parameter);
}
public interface IReactiveNotifyPropertyChanged<out TSender>
{
System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<TSender>> Changed { get; }
Expand Down Expand Up @@ -650,7 +655,7 @@ namespace ReactiveUI
public static ReactiveUI.ReactiveCommand<System.Reactive.Unit, TResult> CreateRunInBackground<TResult>(System.Func<TResult> execute, System.IObservable<bool>? canExecute = null, System.Reactive.Concurrency.IScheduler? backgroundScheduler = null, System.Reactive.Concurrency.IScheduler? outputScheduler = null) { }
public static ReactiveUI.ReactiveCommand<TParam, TResult> CreateRunInBackground<TParam, TResult>(System.Func<TParam, TResult> execute, System.IObservable<bool>? canExecute = null, System.Reactive.Concurrency.IScheduler? backgroundScheduler = null, System.Reactive.Concurrency.IScheduler? outputScheduler = null) { }
}
public abstract class ReactiveCommandBase<TParam, TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, System.IDisposable, System.IObservable<TResult>, System.Windows.Input.ICommand
public abstract class ReactiveCommandBase<TParam, TResult> : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveCommand, ReactiveUI.IReactiveCommand<TParam, TResult>, System.IDisposable, System.IObservable<TResult>, System.Windows.Input.ICommand
{
protected ReactiveCommandBase() { }
public abstract System.IObservable<bool> CanExecute { get; }
Expand Down
79 changes: 79 additions & 0 deletions src/ReactiveUI/ReactiveCommand/IReactiveCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,83 @@ public interface IReactiveCommand : IDisposable, IHandleObservableErrors
/// will always yield <c>false</c> from this observable, even if the <c>canExecute</c> pipeline is currently <c>true</c>.
/// </remarks>
IObservable<bool> CanExecute { get; }
}

/// <summary>
/// Encapsulates a user action behind a reactive interface.
/// This is for interop inside for the command binding.
/// Not meant for external use due to the fact it doesn't implement ICommand
/// to force the user to favor the Reactive style command execution.
/// </summary>
/// <typeparam name="TParam">
/// The type of parameter values passed in during command execution.
/// </typeparam>
/// <typeparam name="TResult">
/// The type of the values that are the result of command execution.
/// </typeparam>
/// <remarks>
/// <para>
/// This interface extends <see cref="IReactiveCommand"/> and adds generic type parameters for the parameter values passed
/// into command execution, and the return values of command execution.
/// </para>
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "SA1402: File may only contain a single type", Justification = "Same interface name")]
public interface IReactiveCommand<in TParam, out TResult> : IObservable<TResult>, IReactiveCommand
{
/// <summary>
/// Gets an observable that, when subscribed, executes this command.
/// </summary>
/// <remarks>
/// <para>
/// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
/// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
/// happen if you call <c>Execute</c> and neglect to subscribe (directly or indirectly) to the returned observable.
/// </para>
/// <para>
/// If no parameter value is provided, a default value of type <typeparamref name="TParam"/> will be passed into
/// the execution logic.
/// </para>
/// <para>
/// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
/// run once. That is, the result is broadcast to those subscribers.
/// </para>
/// <para>
/// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
/// <see cref="IHandleObservableErrors.ThrownExceptions"/> observable.
/// </para>
/// </remarks>
/// <param name="parameter">
/// The parameter to pass into command execution.
/// </param>
/// <returns>
/// An observable that will tick the single result value if and when it becomes available.
/// </returns>
IObservable<TResult> Execute(TParam parameter);

/// <summary>
/// Gets an observable that, when subscribed, executes this command.
/// </summary>
/// <remarks>
/// <para>
/// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
/// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
/// happen if you call <c>Execute</c> and neglect to subscribe (directly or indirectly) to the returned observable.
/// </para>
/// <para>
/// If no parameter value is provided, a default value of type <typeparamref name="TParam"/> will be passed into
/// the execution logic.
/// </para>
/// <para>
/// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
/// run once. That is, the result is broadcast to those subscribers.
/// </para>
/// <para>
/// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
/// <see cref="IHandleObservableErrors.ThrownExceptions"/> observable.
/// </para>
/// </remarks>
/// <returns>
/// An observable that will tick the single result value if and when it becomes available.
/// </returns>
IObservable<TResult> Execute();
}
83 changes: 6 additions & 77 deletions src/ReactiveUI/ReactiveCommand/ReactiveCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ namespace ReactiveUI;
/// <typeparam name="TResult">
/// The type of the values that are the result of command execution.
/// </typeparam>
public abstract class ReactiveCommandBase<TParam, TResult> : IObservable<TResult>, ICommand, IReactiveCommand
public abstract class ReactiveCommandBase<TParam, TResult> : IReactiveCommand<TParam, TResult>, ICommand
{
private EventHandler? _canExecuteChanged;
private bool _canExecuteValue;
Expand All @@ -82,39 +82,19 @@ event EventHandler? ICommand.CanExecuteChanged
remove => _canExecuteChanged -= value;
}

/// <summary>
/// Gets an observable whose value indicates whether the command can currently execute.
/// </summary>
/// <remarks>
/// The value provided by this observable is governed both by any <c>canExecute</c> observable provided during
/// command creation, as well as the current execution status of the command. A command that is currently executing
/// will always yield <c>false</c> from this observable, even if the <c>canExecute</c> pipeline is currently <c>true</c>.
/// </remarks>
/// <inheritdoc />
public abstract IObservable<bool> CanExecute
{
get;
}

/// <summary>
/// Gets an observable whose value indicates whether the command is currently executing.
/// </summary>
/// <remarks>
/// This observable can be particularly useful for updating UI, such as showing an activity indicator whilst a command
/// is executing.
/// </remarks>
/// <inheritdoc />
public abstract IObservable<bool> IsExecuting
{
get;
}

/// <summary>
/// Gets an observable that ticks any exceptions in command execution logic.
/// </summary>
/// <remarks>
/// Any exceptions that are not observed via this observable will propagate out and cause the application to be torn
/// down. Therefore, you will always want to subscribe to this observable if you expect errors could occur (e.g. if
/// your command execution includes network activity).
/// </remarks>
/// <inheritdoc />
public abstract IObservable<Exception> ThrownExceptions
{
get;
Expand Down Expand Up @@ -144,61 +124,10 @@ public void Dispose()
/// </returns>
public abstract IDisposable Subscribe(IObserver<TResult> observer);

/// <summary>
/// Gets an observable that, when subscribed, executes this command.
/// </summary>
/// <remarks>
/// <para>
/// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
/// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
/// happen if you call <c>Execute</c> and neglect to subscribe (directly or indirectly) to the returned observable.
/// </para>
/// <para>
/// If no parameter value is provided, a default value of type <typeparamref name="TParam"/> will be passed into
/// the execution logic.
/// </para>
/// <para>
/// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
/// run once. That is, the result is broadcast to those subscribers.
/// </para>
/// <para>
/// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
/// <see cref="ThrownExceptions"/> observable.
/// </para>
/// </remarks>
/// <param name="parameter">
/// The parameter to pass into command execution.
/// </param>
/// <returns>
/// An observable that will tick the single result value if and when it becomes available.
/// </returns>
/// <inheritdoc/>
public abstract IObservable<TResult> Execute(TParam parameter);

/// <summary>
/// Gets an observable that, when subscribed, executes this command.
/// </summary>
/// <remarks>
/// <para>
/// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
/// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
/// happen if you call <c>Execute</c> and neglect to subscribe (directly or indirectly) to the returned observable.
/// </para>
/// <para>
/// If no parameter value is provided, a default value of type <typeparamref name="TParam"/> will be passed into
/// the execution logic.
/// </para>
/// <para>
/// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
/// run once. That is, the result is broadcast to those subscribers.
/// </para>
/// <para>
/// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
/// <see cref="ThrownExceptions"/> observable.
/// </para>
/// </remarks>
/// <returns>
/// An observable that will tick the single result value if and when it becomes available.
/// </returns>
/// <inheritdoc/>
public abstract IObservable<TResult> Execute();

/// <summary>
Expand Down

0 comments on commit a68adcb

Please sign in to comment.