I came up with the idea of a small utility class that will poll some delegate until the response received meets some condition, upon which it will notify the main thread which can take the appropriate action.
This is the first time I've ever done something like this, and I know that in general, polling is a bad idea, so I want to do this in a way that consumes minimal resources, and is really easy to use.
using System;
using System.Threading;
using System.Windows.Threading;
namespace WPF.Utilities.Common
{
/// <summary>This class allows a user to easily set up a seperate thread to poll some state,
/// and set up an event that will fire if the state meets some condition.</summary>
/// <typeparam name="T">The type of the value returned by the polling delegate.</typeparam>
public class ConditionMonitor<T>
{
#region Private Properties
protected bool Halted = false;
protected Dispatcher originThread = null;
protected Thread monitorThread = null;
#endregion
#region Delegates
/// <summary>A delegate provided by the user of this class which returns the current state,
/// to be tested against a certain condition in the IsConditionMet delegate.</summary>
public delegate T RequestState();
public RequestState RequestStateDelegate { get; set; }
/// <summary>A delegate provided by the user of this class which determines whether given the
/// current state, the polling program should execute the ConditionMet delegate.</summary>
public delegate bool IsConditionMet<T>(T state);
public IsConditionMet<T> IsConditionMetDelegate { get; set; }
/// <summary>A delegate used to handle ConditionMonitor events.</summary>
public delegate void ConditionMonitorHandler<T>(ConditionMonitor<T> source, T state);
/// <summary>An event which fires each time the state is polled (use sparingly).</summary>
public event ConditionMonitorHandler<T> RequestReceived;
/// <summary>An event which fires when the condition is met.</summary>
public event ConditionMonitorHandler<T> ConditionMet;
/// <summary>A delegate used to handle ConditionMonitor events.</summary>
public delegate void ConditionMonitorExceptionHandler<T>(ConditionMonitor<T> source, T state, Exception ex);
/// <summary>An event which fires if an exception is thrown while retrieving the state
/// or testing whether the condition is met.</summary>
public event ConditionMonitorExceptionHandler<T> RequestError;
#endregion
#region Public Properties
/// <summary>The time between requests made to the RequestStateDelegate. Default is 1 second (1000ms)</summary>
public int PollInterval_Milliseconds { get; set; }
/// <summary>Set to true to automatically halt polling once the condition is met. Default is False.</summary>
public bool HaltWhenConditionMet { get; set; }
#endregion
#region Constructors
/// <summary>Creates a new instance of a ConditionMonitor</summary>
public ConditionMonitor()
{
originThread = Dispatcher.CurrentDispatcher;
PollInterval_Milliseconds = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
HaltWhenConditionMet = false;
}
#endregion
#region Public Methods
/// <summary>Begins polling the RequestStateDelegate on a seperate thread.</summary>
public virtual void BeginMonitoring()
{
if( monitorThread != null ) throw new Exception("Previous monitoring has not yet been stopped!");
if( RequestStateDelegate == null ) throw new Exception("No delegate specified for polling - please set the RequestStateDelegate property.");
Halted = false;
monitorThread = new Thread(new ThreadStart(PollState));
monitorThread.Start();
}
/// <summary>Halts polling and ensures that no more requests will be made or events fired.</summary>
public virtual void StopMonitoring()
{
Halted = true;
}
#endregion
#region Private Methods
/// <summary>Responsible for the polling loop and invoking events back on the origin thread.</summary>
protected virtual void PollState()
{
while( !Halted )
{
T state = default(T);
bool bConditionMet = false;
try
{
state = RequestStateDelegate();
InvokeEvent(RequestReceived, state);
if( IsConditionMetDelegate != null && !Halted )
{
bConditionMet = IsConditionMetDelegate(state);
if( bConditionMet )
{
InvokeEvent(ConditionMet, state);
if( HaltWhenConditionMet ) Halted = true;
}
}
}
catch( Exception ex )
{
InvokeExceptionHandler(state, ex);
}
if( !Halted ) Thread.Sleep(PollInterval_Milliseconds);
}
monitorThread = null;
}
/// <summary>Invokes a delegate of type ConditionMonitorHandler on the origin thread.</summary>
/// <param name="toInvoke">The delegate to invoke (RequestRecieved or ConditionMet)</param>
/// <param name="state">The response from the last call to the RequestStateDelegate</param>
protected void InvokeEvent(ConditionMonitorHandler<T> toInvoke, T state)
{
if(toInvoke != null && !Halted)
originThread.BeginInvoke(toInvoke, new object[] { this, state });
}
/// <summary>Invokes the exception delegate on the origin thread.</summary>
/// <param name="state">The response from the last call to the RequestStateDelegate, or null.</param>
/// <param name="ex">The exception raised while calling the RequestStateDelegate or IsConditionMetDelegate.</param>
protected void InvokeExceptionHandler(T state, Exception ex)
{
if( RequestError != null && !Halted )
originThread.BeginInvoke(RequestError, new object[] { this, state, ex });
}
#endregion
}
}
My intended first use is going to be in a program that people in my company have running pretty much all the time. They will be working on some 'deal', and want to know if anything associated with that deal changes so they can grab the latest numbers. My RequestStateDelegate
will be a small database call to check the last modified time of the entry they're working on. I want the thread to silently poll and ONLY bother the main thread when the modified time is not equal to the modified time of the entry the user has loaded up (the IsConditionMetDelegate
will be a small method that does that check).
I just want to be sure I haven't set this up in a way where I risk spinning off more than a single thread, or end up with a thread that keeps running even after StopMonitoring
has been called. Is there anything else I need to do to clean up the thread, or is simply letting PollState
return sufficient?
I'm also worried about what happens if someone abandons the ConditionMonitor
instance without remembering to call StopMonitoring()
. The GC would dispose of the class, but what would happen to the thread is spun up - would it get silently aborted, or throw a huge fuss since it references ConditionMonitor
properties? I thought I might have to do something like this:
public ~ConditionMonitor()
{
StopMonitoring();
}
(Or something similar using IDisposable
) But will the garbage collector even consider this instance for clean up if its thread is perpetually spinning its PollState
instance method?
Revised Code
using System;
using System.Threading;
using System.Windows.Threading;
namespace WPF.Utilities.Common
{
/// <summary>This class allows a user to easily set up a seperate thread to poll some state,
/// and set up an event that will fire if the state meets some condition.</summary>
/// <typeparam name="T">The type of the value returned by the polling delegate.</typeparam>
public class ConditionMonitor<T> : IDisposable
{
#region Private Properties
private Object multiThreadLock = new Object();
private Dispatcher originThread = null;
private Thread monitorThread = null;
private volatile bool Halted = false;
#endregion
#region Delegates
/// <summary>A delegate provided by the user of this class which returns the current state,
/// to be tested against a certain condition in the IsConditionMet delegate.</summary>
public delegate T RequestState();
public RequestState RequestStateDelegate { get; set; }
/// <summary>A delegate provided by the user of this class which determines whether given the
/// current state, the polling program should execute the ConditionMet delegate.</summary>
public delegate bool IsConditionMet(T state);
public IsConditionMet IsConditionMetDelegate { get; set; }
/// <summary>A delegate used to handle ConditionMonitor events.</summary>
public delegate void ConditionMonitorHandler(ConditionMonitor<T> source, T state);
/// <summary>An event which fires each time the state is polled (use sparingly).</summary>
public event ConditionMonitorHandler RequestReceived;
/// <summary>An event which fires when the condition is met.</summary>
public event ConditionMonitorHandler ConditionMet;
/// <summary>A delegate used to handle ConditionMonitor events.</summary>
public delegate void ConditionMonitorExceptionHandler(ConditionMonitor<T> source, T state, Exception ex);
/// <summary>An event which fires if an exception is thrown while retrieving the state
/// or testing whether the condition is met.</summary>
public event ConditionMonitorExceptionHandler RequestError;
#endregion
#region Public Properties
/// <summary>The time between requests made to the RequestStateDelegate. Default is 1 second (1000ms)</summary>
public int PollInterval_Milliseconds { get; set; }
/// <summary>Set to true to automatically halt polling once the condition is met. Default is False.</summary>
public bool HaltWhenConditionMet { get; set; }
#endregion
#region Constructors
/// <summary>Creates a new instance of a ConditionMonitor</summary>
public ConditionMonitor()
{
originThread = Dispatcher.CurrentDispatcher;
PollInterval_Milliseconds = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
HaltWhenConditionMet = false;
}
#endregion
#region Public Methods
/// <summary>Begins polling the RequestStateDelegate on a seperate thread.</summary>
public void BeginMonitoring()
{
if( RequestStateDelegate == null ) throw new Exception("No delegate specified for polling - please set the RequestStateDelegate property.");
lock( multiThreadLock )
{
if( monitorThread != null ) throw new Exception("Previous monitoring has not yet been stopped!");
monitorThread = new Thread(new ThreadStart(PollState));
}
Halted = false;
monitorThread.Start();
}
/// <summary>Halts polling and ensures that no more requests will be made or events fired.</summary>
public void StopMonitoring()
{
Halted = true;
}
/// <summary>Halts the thread if it is still running so that the instance can be garbage collected.</summary>
public void Dispose()
{
StopMonitoring();
}
#endregion
#region Private Methods
/// <summary>Responsible for the polling loop and invoking events back on the origin thread.</summary>
private void PollState()
{
while( !Halted )
{
T state = default(T);
bool bConditionMet = false;
try
{
state = RequestStateDelegate();
InvokeEvent(RequestReceived, state);
if( IsConditionMetDelegate != null && !Halted )
{
bConditionMet = IsConditionMetDelegate(state);
if( bConditionMet )
{
InvokeEvent(ConditionMet, state);
if( HaltWhenConditionMet ) Halted = true;
}
}
}
catch( Exception ex )
{
InvokeExceptionHandler(state, ex);
}
if( !Halted ) Thread.Sleep(PollInterval_Milliseconds);
}
monitorThread = null;
}
/// <summary>Invokes a delegate of type ConditionMonitorHandler on the origin thread.</summary>
/// <param name="toInvoke">The delegate to invoke (RequestRecieved or ConditionMet)</param>
/// <param name="state">The response from the last call to the RequestStateDelegate</param>
private void InvokeEvent(ConditionMonitorHandler toInvoke, T state)
{
if( toInvoke != null && !Halted )
originThread.BeginInvoke(toInvoke, new object[] { this, state });
}
/// <summary>Invokes the exception delegate on the origin thread.</summary>
/// <param name="state">The response from the last call to the RequestStateDelegate, or null.</param>
/// <param name="ex">The exception raised while calling the RequestStateDelegate or IsConditionMetDelegate.</param>
private void InvokeExceptionHandler(T state, Exception ex)
{
if( RequestError != null && !Halted )
originThread.BeginInvoke(RequestError, new object[] { this, state, ex });
}
#endregion
}
}
I am no attempting to implement the polling process using a Timer
and the ThreadPool
as recommended in the answer. A followup question has been posted here: