Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix IsExecuting and Can Execute threading issue #2835

Merged
merged 3 commits into from
Jul 24, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using ReactiveUI;
using Splat;

namespace ReactiveUI.Tests.Wpf
{
/// <summary>
/// Always False Mode Detector.
/// </summary>
/// <seealso cref="Splat.IModeDetector" />
public class AlwaysFalseModeDetector : IModeDetector
{
/// <summary>
/// Gets a value indicating whether the current library or application is running through a unit test.
/// </summary>
/// <returns>
/// If we are currently running in a unit test.
/// </returns>
public bool? InUnitTestRunner() => false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<rxui:ReactiveUserControl
x:Class="ReactiveUI.Tests.Wpf.CanExecuteExecutingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ReactiveUI.Tests.Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="local:CommandBindingViewModel"
mc:Ignorable="d">
<Grid>
<TextBlock Margin="0,30,0,0" Text="Test User Control" />
<TextBlock
x:Name="Result"
Margin="0,0,0,30"
VerticalAlignment="Bottom"
Text="Result Will be Displayed here" />
<Button
x:Name="Execute"
Height="30"
VerticalAlignment="Center"
Content="Execute" />
</Grid>
</rxui:ReactiveUserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace ReactiveUI.Tests.Wpf
{
/// <summary>
/// Interaction logic for CommandBindingView.xaml.
/// </summary>
public partial class CanExecuteExecutingView
{
public CanExecuteExecutingView()
{
InitializeComponent();
ViewModel = new();
this.WhenActivated(d =>
{
this.BindCommand(ViewModel, vm => vm.Command3, v => v.Execute);
this.OneWayBind(ViewModel, vm => vm.Result, v => v.Result.Text);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Splat;

namespace ReactiveUI.Tests.Wpf
{
public static class LiveModeDetector
{
private static AlwaysFalseModeDetector liveModeDetector = new();
private static DefaultModeDetector defaultModeDetector = new();

public static void UseRuntimeThreads() =>
ModeDetector.OverrideModeDetector(liveModeDetector);

public static void UseDefaultModeDetector() =>
ModeDetector.OverrideModeDetector(defaultModeDetector);

public static bool? InUnitTestRunner() => ModeDetector.InUnitTestRunner();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Threading;
using System.Threading.Tasks;

namespace ReactiveUI.Tests.Wpf
{
public class CommandBindingViewModel : ReactiveObject
{
private ReactiveCommand<int, int> _Command1;
private ReactiveCommand<Unit, Unit> _Command2;
private ReactiveCommand<Unit, int?> _Command3;
private ObservableAsPropertyHelper<int?> _result;

private int _value;

public CommandBindingViewModel()
{
_Command1 = ReactiveCommand.Create<int, int>(_ => { return _; }, outputScheduler: ImmediateScheduler.Instance);
_Command2 = ReactiveCommand.Create(() => { }, outputScheduler: ImmediateScheduler.Instance);
_Command3 = ReactiveCommand.CreateFromTask(RunAsync, outputScheduler: RxApp.TaskpoolScheduler);
_result = _Command3.ToProperty(this, x => x.Result, scheduler: RxApp.MainThreadScheduler);
}

public ReactiveCommand<int, int> Command1
Expand All @@ -33,12 +40,26 @@ public CommandBindingViewModel()
set => this.RaiseAndSetIfChanged(ref _Command2, value);
}

public ReactiveCommand<Unit, int?> Command3
{
get => _Command3;
set => this.RaiseAndSetIfChanged(ref _Command3, value);
}

public FakeNestedViewModel? NestedViewModel { get; set; }

public int Value
{
get => _value;
set => this.RaiseAndSetIfChanged(ref _value, value);
}

public int? Result => _result.Value;

private async Task<int?> RunAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
return cancellationToken.IsCancellationRequested ? null : 100;
}
}
}
44 changes: 44 additions & 0 deletions src/ReactiveUI.Tests/Platforms/wpf/WpfActiveContentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
// See the LICENSE file in the project root for full license information.

using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using Splat;
using Xunit;

namespace ReactiveUI.Tests.Wpf
Expand Down Expand Up @@ -198,6 +200,48 @@ public void TransitioninContentControlDpiTest()
window!.ShowDialog();
}

[StaFact]
public void ReactiveCommandRunningOnTaskThreadAllowsCanExecuteAndExecutingToFire()
{
LiveModeDetector.UseRuntimeThreads();
var window = Fixture?.App?.MockWindowFactory();
window!.WhenActivated(async d =>
{
try
{
window!.TransitioningContent.VerticalContentAlignment = VerticalAlignment.Stretch;
window!.TransitioningContent.HorizontalContentAlignment = HorizontalAlignment.Stretch;
var view = new CanExecuteExecutingView();
window!.TransitioningContent.Content = view;
await Task.Delay(5000).ConfigureAwait(true);

var isExecutingExecuted = false;
view!.ViewModel!.Command3.IsExecuting
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(value =>
{
if (value)
{
isExecutingExecuted = true;
}
}).DisposeWith(d);

int? result = null;
view!.ViewModel!.Command3.Subscribe(r => result = r);
await view!.ViewModel!.Command3.Execute();
await Task.Delay(5000).ConfigureAwait(true);
Assert.Equal(100, result);
Assert.True(isExecutingExecuted);
}
finally
{
window?.Close();
LiveModeDetector.UseDefaultModeDetector();
}
});
window!.ShowDialog();
}

#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
}
}
9 changes: 9 additions & 0 deletions src/ReactiveUI.Tests/ReactiveUI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
</Choose>

<ItemGroup>
<None Remove="Platforms\wpf\Mocks\CanExecuteMock\AlwaysFalseModeDetector.cs" />
<None Remove="Platforms\wpf\Mocks\CanExecuteMock\CanExecuteExecutingView.xaml.cs" />
<None Remove="Platforms\wpf\Mocks\CanExecuteMock\LiveModeDetector.cs" />
<None Remove="Platforms\wpf\WpfGeneralFixture.cs" />
<None Remove="Platforms\wpf\WpfGeneralFixtureApp.cs" />
</ItemGroup>
Expand All @@ -93,5 +96,11 @@
<DependentUpon>TestFormNotCanActivate.resx</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<Page Update="Platforms\wpf\Mocks\CanExecuteMock\CanExecuteExecutingView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>

</Project>
4 changes: 2 additions & 2 deletions src/ReactiveUI/ReactiveCommand/ReactiveCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,6 @@ public class ReactiveCommand<TParam, TResult> : ReactiveCommandBase<TParam, TRes
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_outputScheduler = outputScheduler ?? throw new ArgumentNullException(nameof(outputScheduler));
_exceptions = new ScheduledSubject<Exception>(outputScheduler, RxApp.DefaultExceptionHandler);

_executionInfo = new Subject<ExecutionInfo>();
_synchronizedExecutionInfo = Subject.Synchronize(_executionInfo, outputScheduler);
_isExecuting = _synchronizedExecutionInfo.Scan(
Expand All @@ -689,10 +688,11 @@ public class ReactiveCommand<TParam, TResult> : ReactiveCommandBase<TParam, TRes
}).StartWith(false).CombineLatest(_isExecuting, (canEx, isEx) => canEx && !isEx)
.DistinctUntilChanged()
.Replay(1).RefCount();

_results = _synchronizedExecutionInfo.Where(x => x.Demarcation == ExecutionDemarcation.Result)
.Select(x => x.Result);

_canExecuteSubscription = _canExecute.Subscribe(OnCanExecuteChanged);
_canExecuteSubscription = _canExecute.ObserveOn(RxApp.MainThreadScheduler).Subscribe(OnCanExecuteChanged);
}
ChrisPulman marked this conversation as resolved.
Show resolved Hide resolved

private enum ExecutionDemarcation
Expand Down