Skip to content

Instantly share code, notes, and snippets.

@jrgcubano
Forked from hjerpbakk/AsyncMethodCaller.cs
Created December 9, 2015 15:52
Show Gist options
  • Save jrgcubano/8602120c00acaf64ae1e to your computer and use it in GitHub Desktop.
Save jrgcubano/8602120c00acaf64ae1e to your computer and use it in GitHub Desktop.
C# classes for calling methods asynchronously and continue with with other methods after execution completes. Very useful in View Models.
/// <summary>
/// Used to call methods asynchronously and continue with with other methods
/// after execution completes. Very useful in View Models.
/// </summary>
public class AsyncMethodCaller : IAsyncMethodCaller
{
/// <summary>
/// The final <see cref="Task"/> to be executed, can be awaited.
/// </summary>
protected Task CurrentTask;
/// <summary>
/// Calls the given method asynchronously, and completes with the continueWith <see cref="T:System.Action`1{T}"/>
/// if the method call succeeds or the failWith <see cref="T:System.Action`1{T}"/> if the method throws an exception.
/// </summary>
/// <typeparam name="T">The return type of the method to be called.</typeparam>
/// <param name="methodToCall">The method to be called asynchronously.</param>
/// <param name="continueWith">The method to be continued with if the previously called method executed successfully.</param>
/// <param name="failWith">The method to be continued with if the previously called method threw an exception.</param>
public void CallMethodAndContinue<T>(Func<T> methodToCall, Action<T> continueWith, Action<Exception> failWith)
{
var serviceTask = new Task<T>(methodToCall);
CurrentTask = serviceTask.ContinueWith(antecedent =>
{
if (antecedent.IsFaulted)
{
failWith(antecedent.Exception);
}
else
{
continueWith(serviceTask.Result);
}
}, TaskScheduler.FromCurrentSynchronizationContext());
serviceTask.Start();
}
/// <summary>
/// Calls the given method asynchronously, and completes with the continueWith <see cref="Action"/>
/// if the method call succeeds or the failWith <see cref="Action"/> if the method throws an exception.
/// </summary>
/// <param name="methodToCall">The method to be called asynchronously.</param>
/// <param name="continueWith">The method to be continued with if the previously called method executed successfully.</param>
/// <param name="failWith">The method to be continued with if the previously called method threw an exception.</param>
public void CallMethodAndContinue(Action methodToCall, Action continueWith, Action<Exception> failWith)
{
var serviceTask = new Task(methodToCall);
CurrentTask = serviceTask.ContinueWith(antecedent =>
{
if (antecedent.IsFaulted)
{
failWith(antecedent.Exception);
}
else
{
continueWith();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
serviceTask.Start();
}
}
public interface IAsyncMethodCaller
{
void CallMethodAndContinue<T>(Func<T> methodToCall, Action<T> continueWith, Action<Exception> failWith);
void CallMethodAndContinue(Action methodToCall, Action continueWith, Action<Exception> failWith);
}
/// <summary>
/// Helps with testing of behavior in View Models.
/// </summary>
public class TestAsyncMethodCaller : AsyncMethodCaller
{
/// <summary>
/// Fake services can wait on this <see cref="ManualResetEventSlim"/>.
/// </summary>
public ManualResetEventSlim ManualResetEvent { get; private set; }
/// <summary>
/// Creates a <see cref="ManualResetEventSlim"/> and a <see cref="SynchronizationContext"/>.
/// </summary>
public TestAsyncMethodCaller()
{
ManualResetEvent = new ManualResetEventSlim(false);
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
}
/// <summary>
/// Signals a waiting service to continue and waits for View Model-
/// tasks to finish.
/// </summary>
public void StartServiceAndWait()
{
ManualResetEvent.Set();
CurrentTask.Wait();
}
}
/**************************** TESTS *****************************/
[TestClass]
public class AsyncMethodCallerTests
{
private TestAsyncMethodCaller m_asyncMethodCaller;
private ServiceImplmentationForTest m_service;
private ViewModel m_viewModel;
private void Init(bool fail = false)
{
m_asyncMethodCaller = new TestAsyncMethodCaller();
m_service = new ServiceImplmentationForTest(m_asyncMethodCaller.ManualResetEvent, fail);
m_viewModel = new ViewModel(m_asyncMethodCaller, m_service);
}
[TestMethod]
public void CallMethodAndContinue_ServiceWithResultCompletes_ResultIsReturnedAndCallbackCalled()
{
Init();
m_viewModel.LoadContent();
Assert.AreEqual("Loading content...", m_viewModel.MessageToUser);
m_asyncMethodCaller.StartServiceAndWait();
Assert.AreEqual("Content is 1", m_viewModel.MessageToUser);
Assert.IsTrue(m_service.LoadContentCalled);
Assert.IsFalse(m_service.SaveCalled);
}
[TestMethod]
public void CallMethodAndContinue_ServiceWithResultFails_ExceptionIsReturnedAndCallbackCalled()
{
Init(true);
m_viewModel.LoadContent();
Assert.AreEqual("Loading content...", m_viewModel.MessageToUser);
m_asyncMethodCaller.StartServiceAndWait();
Assert.AreEqual("Loading content failed: Exception Message", m_viewModel.MessageToUser);
Assert.IsTrue(m_service.LoadContentCalled);
Assert.IsFalse(m_service.SaveCalled);
}
[TestMethod]
public void CallMethodAndContinue_ServiceCompletes_CallbackCalled()
{
Init();
m_viewModel.Save();
Assert.AreEqual("Saving content...", m_viewModel.MessageToUser);
m_asyncMethodCaller.StartServiceAndWait();
Assert.AreEqual("Content saved", m_viewModel.MessageToUser);
Assert.IsTrue(m_service.SaveCalled);
Assert.IsFalse(m_service.LoadContentCalled);
}
[TestMethod]
public void CallMethodAndContinue_Service_ExceptionIsReturnedAndCallbackCalled()
{
Init(true);
m_viewModel.Save();
Assert.AreEqual("Saving content...", m_viewModel.MessageToUser);
m_asyncMethodCaller.StartServiceAndWait();
Assert.AreEqual("Saving content failed: Exception Message", m_viewModel.MessageToUser);
Assert.IsTrue(m_service.SaveCalled);
Assert.IsFalse(m_service.LoadContentCalled);
}
private class ViewModel
{
private readonly IAsyncMethodCaller m_asyncMethodCaller;
private readonly IServiceInterface m_service;
public string MessageToUser { get; private set; }
public ViewModel(IAsyncMethodCaller asyncMethodCaller, IServiceInterface service)
{
m_asyncMethodCaller = asyncMethodCaller;
m_service = service;
}
public void LoadContent()
{
MessageToUser = "Loading content...";
m_asyncMethodCaller.CallMethodAndContinue(() => m_service.LoadContent(),
LoadContentCompleted,
LoadContentFailed);
}
public void Save()
{
MessageToUser = "Saving content...";
m_asyncMethodCaller.CallMethodAndContinue(() => m_service.Save(),
SaveCompleted,
SaveFailed);
}
private void LoadContentCompleted(int content)
{
MessageToUser = "Content is " + content;
}
private void LoadContentFailed(Exception exception)
{
MessageToUser = "Loading content failed: " + exception.InnerException.Message;
}
private void SaveCompleted()
{
MessageToUser = "Content saved";
}
private void SaveFailed(Exception exception)
{
MessageToUser = "Saving content failed: " + exception.InnerException.Message;
}
}
private interface IServiceInterface
{
int LoadContent();
void Save();
}
private class ServiceImplmentationForTest : IServiceInterface
{
private readonly ManualResetEventSlim m_manualResetEvent;
private readonly bool m_fail;
public bool LoadContentCalled { get; private set; }
public bool SaveCalled { get; private set; }
public ServiceImplmentationForTest(ManualResetEventSlim manualResetEvent, bool fail)
{
m_manualResetEvent = manualResetEvent;
m_fail = fail;
}
public int LoadContent()
{
m_manualResetEvent.Wait();
LoadContentCalled = true;
if (m_fail)
{
throw new Exception("Exception Message");
}
return 1;
}
public void Save()
{
m_manualResetEvent.Wait();
SaveCalled = true;
if (m_fail)
{
throw new Exception("Exception Message");
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment