#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
#if UNITTEST
using System;
using System.ComponentModel;
using System.Threading;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
/// Unit test for the threaded view model base class
[TestFixture]
public class ThreadedViewModelTest {
#region class DummyContext
/// Synchronization context that does absolutely nothing
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
/// Barebones implementation of an asynchronous result
private class SimpleAsyncResult : IAsyncResult {
/// Ehether the asynchronous operation is complete
///
/// Always true because it completes synchronously
///
public bool IsCompleted { get { return true; } }
///
/// Wait handle that can be used to wait for the asynchronous operation
///
public WaitHandle AsyncWaitHandle {
get { throw new NotImplementedException("Not implemented"); }
}
/// Custom state that can be used to pass information around
public object AsyncState {
get { throw new NotImplementedException("Not implemented"); }
}
/// Whether the asynchronous operation completed synchronously
public bool CompletedSynchronously { get { return true; } }
/// The value returned from the asynchronous operation
public object ReturnedValue;
}
#endregion // class SimpleAsyncResult
/// Whether the calling thread needs to use Invoke()
public bool InvokeRequired {
get { return true; }
}
/// Schedules the specified method for execution in the target thread
/// Method the target thread will execute when it is idle
/// Arguments that will be passed to the method
///
/// An asynchronous result handle that can be used to check on the status of
/// the call and wait for its completion
///
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
var asyncResult = new SimpleAsyncResult();
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
return asyncResult;
}
/// Waits for the asychronous call to complete
///
/// Asynchronous result handle returned by the method
///
/// The original result returned by the asychronously called method
public object EndInvoke(IAsyncResult result) {
return ((SimpleAsyncResult)result).ReturnedValue;
}
///
/// Schedules the specified method for execution in the target thread and waits
/// for it to complete
///
/// Method that will be executed by the target thread
/// Arguments that will be passed to the method
/// The result returned by the specified method
public object Invoke(Delegate method, object[] arguments) {
return method.Method.Invoke(method.Target, arguments);
}
}
#endregion // class DummyContext
#region class TestViewModel
/// View model used to unit test the threaded view model base class
private class TestViewModel : ThreadedViewModel {
///
/// Initializes a new view model, letting the base class figure out the UI thread
///
public TestViewModel() : base() {
this.finishedGate = new ManualResetEvent(initialState: false);
}
///
/// Initializes a new view model, using the specified context for the UI thread
///
/// Synchronization context of the UI thread
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
this.finishedGate = new ManualResetEvent(initialState: false);
}
/// Immediately releases all resources owned by the instance
public override void Dispose() {
base.Dispose();
if(this.finishedGate != null) {
this.finishedGate.Dispose();
this.finishedGate = null;
}
}
/// Waits until the first background operation is finished
///
/// True if the background operation is finished, false if it is ongoing
///
public bool WaitUntilFinished() {
return this.finishedGate.WaitOne(100);
}
/// Runs a background process that causes the specified error
/// Error that will be caused in the background process
public void CauseErrorInBackgroundThread(Exception error) {
RunInBackground(
delegate() { throw error; }
);
}
///
/// Assigns the specified value to the same-named property from a background thread
///
/// Value that will be assigned to the same-named property
public void AssignValueInBackgroundThread(int value) {
RunInBackground(
delegate () {
this.assignedValue = value;
this.finishedGate.Set();
}
);
}
/// Last error that was reported by the threaded view model
public Exception ReportedError {
get { return this.reportedError; }
}
/// Value that has been assigned from the background thread
public int AssignedValue {
get { return this.assignedValue; }
}
/// Called when an error occurs in the background thread
/// Exception that was thrown in the background thread
protected override void ReportError(Exception exception) {
this.reportedError = exception;
this.finishedGate.Set();
}
/// Last error that was reported by the threaded view model
private volatile Exception reportedError;
/// Triggered when the
private ManualResetEvent finishedGate;
/// Value that is assigned through the background thread
private volatile int assignedValue;
}
#endregion // class TestViewModel
/// Verifies that the threaded view model has a default constructor
[Test, Explicit]
public void HasDefaultConstructor() {
using(var mainForm = new System.Windows.Forms.Form()) {
mainForm.Show();
try {
mainForm.Visible = false;
using(new TestViewModel()) { }
}
finally {
mainForm.Close();
}
}
}
///
/// Verifies that the threaded view model can be constructed with a custom UI context
///
[Test]
public void HasCustomSychronizationContextConstructor() {
using(new TestViewModel(new DummyContext())) { }
}
/// Checks that a new view model starts out idle and not busy
[Test]
public void NewInstanceIsNotBusy() {
using(var viewModel = new TestViewModel(new DummyContext())) {
Assert.IsFalse(viewModel.IsBusy);
}
}
///
/// Verifies that errors happening in the background processing threads are
/// reported to the main thread
///
[Test]
public void ErrorsInBackgroundThreadAreReported() {
using(var viewModel = new TestViewModel(new DummyContext())) {
var testError = new ArgumentException("Mooh");
viewModel.CauseErrorInBackgroundThread(testError);
viewModel.WaitUntilFinished();
Assert.AreSame(testError, viewModel.ReportedError);
}
}
///
/// Verifies that the background thread actually executes and can do work
///
[Test]
public void BackgroundThreadExecutesTasks() {
using(var viewModel = new TestViewModel(new DummyContext())) {
viewModel.AssignValueInBackgroundThread(10042);
viewModel.WaitUntilFinished();
Assert.AreEqual(10042, viewModel.AssignedValue);
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST