Compare commits

..

12 Commits

Author SHA1 Message Date
Tracy Pearson
4bcfdb56b5 Added a comment giving some instructions on how to get internal method visible to this assembly 2022-09-01 09:59:39 -04:00
Tracy Pearson
f19d390b78 Task.Run() leaves a warning it will run synchronosly. 2022-09-01 08:39:24 -04:00
Tracy Pearson
2d8ea50944 Test using the Callback and DispatcherFrame 2022-09-01 00:14:30 -04:00
Tracy Pearson
e5980ecb10 Easier to read tests? More stable? 2022-08-31 23:23:41 -04:00
Tracy Pearson
c36b4581c1 code cleanup 2022-08-30 19:34:41 -04:00
Tracy Pearson
379ec7881f The tests are pasing. The UI is working. The tests sometimes hang. 2022-08-30 13:24:29 -04:00
Tracy Pearson
38df3a3ff3 comment about what the LongDelay is for. 2022-08-30 11:59:51 -04:00
Tracy Pearson
abf09308fc Working with passing tests! 2022-08-30 11:55:08 -04:00
Tracy Pearson
50f0754a0f Corrected a build error 2022-08-30 11:02:37 -04:00
Tracy Pearson
3084a49bdd Using a Func<Task> may be a better option 2022-08-30 10:56:23 -04:00
Tracy Pearson
b58a74709e Merged MainWindowViewModel changes 2022-08-27 11:59:50 -04:00
Tracy Pearson
42c192d849 Duplicated test scenario I would like to test 2022-08-27 11:50:22 -04:00
13 changed files with 277 additions and 13 deletions

View File

@@ -1,3 +1,33 @@
# WpfViewModelFirst # WpfViewModelFirst
This project is for researching Actions that need to call an async/await function. This project is for researching Actions that need to call an async/await function.
**Unit Testing:**
This area has been troubling. The MainWindowViewModel needs to set a value
of the ObservableObject on the UI thread. Doing a straight unit test will
cause a deadlock.
The first process of testing was learning that the Moq mocking framework
has the ability to trigger the action passed to Part1ViewModel. This has
extra setup and makes the test hard to read.
The next step was finding a way to wait for the ContinueWith in the method
to finish. That was accomplished with the TaskCompletionSource.
With the code being triggered by the callback, this would exhibit 3 different
behaviors. Many times it would pass. Occassionally it would fail one of the
asserts. The other times it would frustratingly get into a deadlock.
In searching for a way to test this I learned I can set an attribute on the
class to allow internal methods and fields to be seen by the test assembly.
This helped the test code be easier to read.
[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")]
Using a combination of Task.Run and DispatcherFrame the tests appear to
complete successfully every time.
**Note:** When the Task.Run is awaited the test
will deadlock.

View File

@@ -0,0 +1,129 @@
using Moq;
using System.Windows.Controls;
using System.Windows.Threading;
using WpfViewModelFirst.Part1;
using WpfViewModelFirst.Part2;
namespace WpfViewModelFirst.Tests
{
public class MainWindowViewModelTests
{
[Fact]
public void Part1WillSetPart1ViewModelToCurrentViewModel()
{
Part1ViewModel part1ViewModel = new(() => { return Task.CompletedTask; });
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
.Returns(part1ViewModel);
MainWindowViewModel main = new(mockViewModelFactory.Object);
main.Part1(null);
Assert.Equal(part1ViewModel, main.CurrentViewModel);
}
//[Fact]
//public async Task Part1CallbackIsSuccessfull()
//{
// TaskCompletionSource<bool> taskCompletionSource = new();
// Mock<IViewModelFactory> mockViewModelFactory = new();
// mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
// .Callback<Func<Task>>(c =>
// {
// Task.Run(() =>
// {
// c.Invoke();
// });
// });
// mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
// .Returns(() => new Part2ViewModel());
// MainWindowViewModel main = new(mockViewModelFactory.Object);
// main.ItHappened += () => taskCompletionSource.SetResult(true);
// main.Part1(null);
// await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
// Assert.IsType<Part2ViewModel>(main.CurrentViewModel);
// Assert.Single(main.Strings);
//}
[Fact]
public async Task Part1CallBackExample()
{
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
mockViewModelFactory.Setup(vmf => vmf.GetPart1ViewModel(It.IsAny<Func<Task>>()))
.Callback<Func<Task>>(c =>
{
Task.Run(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
c.Invoke();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
});
main.Part1(null);
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.IsType<Part2ViewModel>(main.CurrentViewModel);
Assert.Single(main.Strings);
}
// This method requires the Part1Action method to be internal not private
// The MainWindowViewModel class also needs to have an attribute allowing
// this assembly to see the internals
// [assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")]
[Fact]
public async Task Part1ActionWillSetCurrentViewModel()
{
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
Task run = new(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
main.Part1Action();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
run.Start();
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.IsType<Part2ViewModel>(main.CurrentViewModel);
}
[Fact]
public async Task Part1ActionWillSetSingle()
{
Mock<IViewModelFactory> mockViewModelFactory = new();
mockViewModelFactory.Setup(vmf => vmf.GetPart2ViewModel())
.Returns(() => new Part2ViewModel());
MainWindowViewModel main = new(mockViewModelFactory.Object);
TaskCompletionSource<bool> taskCompletionSource = new();
main.ItHappened += () => taskCompletionSource.SetResult(true);
Task run = new(() =>
{
DispatcherFrame frame = new();
frame.Dispatcher.Invoke(
DispatcherPriority.Normal, () =>
{
main.Part1Action();
frame.Continue = true;
});
Dispatcher.PushFrame(frame);
});
run.Start();
await taskCompletionSource.Task.WaitAsync(CancellationToken.None);
Assert.Single(main.Strings);
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfViewModelFirst.Tests.Part1
{
public class Part1ViewModelTests
{
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfViewModelFirst.Tests.Part2
{
public class Part2ViewModelTests
{
}
}

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WpfViewModelFirst\WpfViewModelFirst.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,13 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.3.32804.467 VisualStudioVersion = 17.3.32804.467
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfViewModelFirst", "WpfViewModelFirst\WpfViewModelFirst.csproj", "{E5611330-98DE-4571-A035-65576B2F8D4B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfViewModelFirst", "WpfViewModelFirst\WpfViewModelFirst.csproj", "{E5611330-98DE-4571-A035-65576B2F8D4B}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{90D6F27D-C403-4DF2-A570-D0DEECAC4062}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{90D6F27D-C403-4DF2-A570-D0DEECAC4062}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfViewModelFirst.Tests", "WpfViewModelFirst.Tests\WpfViewModelFirst.Tests.csproj", "{89753E08-BE0A-4FEA-B457-5486BB46EFEE}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -20,6 +22,10 @@ Global
{E5611330-98DE-4571-A035-65576B2F8D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5611330-98DE-4571-A035-65576B2F8D4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5611330-98DE-4571-A035-65576B2F8D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5611330-98DE-4571-A035-65576B2F8D4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5611330-98DE-4571-A035-65576B2F8D4B}.Release|Any CPU.Build.0 = Release|Any CPU {E5611330-98DE-4571-A035-65576B2F8D4B}.Release|Any CPU.Build.0 = Release|Any CPU
{89753E08-BE0A-4FEA-B457-5486BB46EFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89753E08-BE0A-4FEA-B457-5486BB46EFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89753E08-BE0A-4FEA-B457-5486BB46EFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89753E08-BE0A-4FEA-B457-5486BB46EFEE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -18,7 +18,7 @@ namespace WpfViewModelFirst
base.OnStartup(e); base.OnStartup(e);
MainWindow mainWindow = new() MainWindow mainWindow = new()
{ {
DataContext = new MainWindowViewModel() DataContext = new MainWindowViewModel(new ViewModelFactory())
}; };
mainWindow.Show(); mainWindow.Show();
} }

View File

@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using WpfViewModelFirst.Part1;
using WpfViewModelFirst.Part2;
namespace WpfViewModelFirst
{
public interface IViewModelFactory
{
Part1ViewModel GetPart1ViewModel(Func<Task> finishAction);
Part2ViewModel GetPart2ViewModel();
}
}

View File

@@ -10,39 +10,46 @@ using System.Windows.Threading;
using WpfViewModelFirst.Part1; using WpfViewModelFirst.Part1;
using WpfViewModelFirst.Part2; using WpfViewModelFirst.Part2;
[assembly:System.Runtime.CompilerServices.InternalsVisibleTo("WpfViewModelFirst.Tests")]
namespace WpfViewModelFirst namespace WpfViewModelFirst
{ {
public class MainWindowViewModel : ViewModelBase public class MainWindowViewModel : ViewModelBase
{ {
public MainWindowViewModel() public MainWindowViewModel(IViewModelFactory viewModelFactory)
{ {
_viewModelFactory = viewModelFactory;
StartCommand = new CustomCommand(Part1); StartCommand = new CustomCommand(Part1);
} }
private readonly IViewModelFactory _viewModelFactory;
public ICommand StartCommand { get; set; } public ICommand StartCommand { get; set; }
public void Part1(object? e) public void Part1(object? e)
{ {
CurrentViewModel = null; CurrentViewModel = null;
CurrentViewModel = new Part1ViewModel(Part1Action); CurrentViewModel = _viewModelFactory.GetPart1ViewModel(Part1Action);
} }
private void Part1Action() internal Task Part1Action()
{ {
var currentDispatcher = Dispatcher.CurrentDispatcher;
CurrentViewModel = null; CurrentViewModel = null;
LongDelay().ContinueWith(c => Dispatcher currentDispatcher = Dispatcher.CurrentDispatcher;
return LongDelay().ContinueWith((c) =>
{ {
System.Diagnostics.Debug.WriteLine(c.Result); CurrentViewModel = _viewModelFactory.GetPart2ViewModel();
CurrentViewModel = new Part2ViewModel();
currentDispatcher.Invoke(() => currentDispatcher.Invoke(() =>
{ {
Strings.Add($"{DateTime.Now}"); Strings.Add($"{DateTime.Now}");
} });
); ItHappened?.Invoke();
}, TaskContinuationOptions.OnlyOnRanToCompletion); }, TaskContinuationOptions.OnlyOnRanToCompletion);
} }
public event Action? ItHappened;
private async Task<string> LongDelay() private async Task<string> LongDelay()
{ {
// This would be the Repository call in the production application
await Task.Delay(500); await Task.Delay(500);
return "Completed"; return "Completed";
} }

View File

@@ -12,6 +12,9 @@
<RowDefinition/> <RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button Grid.Row="0" <Button Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="10"
Content="Finish" Content="Finish"
Command="{Binding FinishCommand}"/> Command="{Binding FinishCommand}"/>
</Grid> </Grid>

View File

@@ -9,9 +9,9 @@ namespace WpfViewModelFirst.Part1
{ {
public class Part1ViewModel : ViewModelBase public class Part1ViewModel : ViewModelBase
{ {
private readonly Action _finishAction; private readonly Func<Task> _finishAction;
public Part1ViewModel(Action finishAction) public Part1ViewModel(Func<Task> finishAction)
{ {
_finishAction = finishAction; _finishAction = finishAction;
FinishCommand = new CustomCommand(Finish); FinishCommand = new CustomCommand(Finish);

View File

@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfViewModelFirst
{
public class ViewModelFactory : IViewModelFactory
{
public Part1.Part1ViewModel GetPart1ViewModel(Func<Task> finishAction)
{
return new(finishAction);
}
public Part2.Part2ViewModel GetPart2ViewModel()
{
return new();
}
}
}