Introduction
This article presents a method for running all I/O operations of a WPF application in a single thread. This comes in handy when working with a single threaded context based data repository such as EntityFramework.
Problem
Being new both to WPF and EntityFramework, I had to build a client application which retrieves and inserts data from a database. Due to the fact that database operations can be time consuming and I didn’t want to hold my client's view, I wished that the operations would run on a different thread. At first, I tried using the System.ComponentModel.BackgroundWorker
, just to find out System.Data.Objects.ObjectContext
works on a single thread. This was quite a setback for me, and I had to come with an elegant solution for the problem.
Proposed solution
My solution was creating a thread which accepts requests from the main thread and returns them to the main thread. The thread is managed by a class which holds a requests queue:
public class UIQueueManager : IDisposable
{
#region Constants
const int MAX_OPERATION_DURATION = 10000;
#endregion
#region Members
Thread m_workingThread;
Thread m_terminationThread;
object m_lock;
KeyValuePair<Guid, IUIQueueItem> m_currentQueueItem;
System.Collections.Queue m_operationsQueue;
bool m_bRun;
#endregion
#region Properties
#endregion
#region Methods
#region Ctor
public UIQueueManager()
{
m_lock = new object();
m_operationsQueue = new System.Collections.Queue();
m_workingThread = new Thread(this.RunQueue);
m_terminationThread = new Thread(this.CheckQueueValidity);
m_currentQueueItem = new KeyValuePair<Guid, IUIQueueItem>();
m_bRun = true;
m_workingThread.Start();
m_terminationThread.Start();
}
#endregion
public void AddToQueue(IUIQueueItem item)
{
Guid guid = Guid.NewGuid();
try
{
Monitor.Enter(m_lock);
m_operationsQueue.Enqueue(
new KeyValuePair<Guid,IUIQueueItem>(guid, item));
}
catch
{
throw;
}
finally
{
Monitor.Exit(m_lock);
}
return;
}
public void RunQueue()
{
while (m_bRun)
{
if (m_operationsQueue.Count > 0)
{
KeyValuePair<Guid, IUIQueueItem> kvp =
new KeyValuePair<Guid,IUIQueueItem>();
try
{
try
{
Monitor.Enter(m_lock);
kvp = (KeyValuePair<Guid,
IUIQueueItem>)m_operationsQueue.Dequeue();
m_currentQueueItem = kvp;
}
catch(Exception ex)
{
SetItemError(kvp.Value, ex);
}
finally
{
Monitor.Exit(m_lock);
}
if (kvp.Value != null)
{
kvp.Value.Operation();
}
}
catch (Exception ex)
{
SetItemError(kvp.Value, ex);
}
finally
{
if (kvp.Value != null)
{
OperationEnded(kvp.Value);
}
}
}
Thread.Sleep(1);
}
}
private void CheckQueueValidity()
{
while (m_bRun)
{
Guid guid = Guid.NewGuid();
try
{
Monitor.Enter(m_lock);
guid = m_currentQueueItem.Key;
}
catch
{
}
finally
{
Monitor.Exit(m_lock);
}
Thread.Sleep(MAX_OPERATION_DURATION);
if ((guid == m_currentQueueItem.Key) && (guid != Guid.Empty))
{
Monitor.Enter(m_lock);
try
{
m_workingThread.Abort();
m_currentQueueItem.Value.IsSuccesful = false;
m_currentQueueItem.Value.ErrorDescription =
"Operation timed out";
OperationEnded(m_currentQueueItem.Value);
m_workingThread = new Thread(this.RunQueue);
m_workingThread.Start();
OperationEnded(m_currentQueueItem.Value);
}
catch
{
}
finally
{
Monitor.Exit(m_lock);
}
}
}
}
private void SetItemError(IUIQueueItem item, Exception ex)
{
if (item != null)
{
item.IsSuccesful = false;
item.ErrorDescription = ex.Message;
}
}
private void OperationEnded(IUIQueueItem item)
{
if (System.Windows.Application.Current != null)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new ThreadStart(item.OperationCompleted));
}
else
{
item.OperationCompleted();
}
return;
}
#region IDisposable Members
public void Dispose()
{
Monitor.Enter(m_lock);
if (m_workingThread != null)
{
m_workingThread.Abort();
}
if (m_terminationThread != null)
{
m_terminationThread.Abort();
}
m_bRun = false;
Monitor.Exit(m_lock);
}
#endregion
#endregion
}
The class holds two threads. One for handling all the requests, and the second for monitoring the first and validating that a request isn't 'stuck' in the queue.
The request queue item implements a simple interface:
public interface IUIQueueItem
{
#region Properties
bool IsSuccesful
{
get;
set;
}
string ErrorDescription
{
get;
set;
}
#endregion
#region Methods
void Operation();
void OperationCompleted();
#endregion
}
This interface is set to point if the operation completed successfully, and a description in case it faulted.
I’ve inherited from the interface to create a generic class that can handle the calls:
public class UIQueueItem<T,U> : IUIQueueItem
{
#region Delegates
public delegate T GenericRequester(U val);
public delegate void GenericHandler(T val);
#endregion
#region Members
bool m_bSuccesful;
string m_strErrorDescription;
GenericRequester m_GenericRequester;
GenericHandler m_GenericHandler;
U m_Param;
T m_Response;
#endregion
#region Methods
#region Ctor
public UIQueueItem(GenericRequester request,
GenericHandler handler,
U param)
{
m_bSuccesful = true;
m_strErrorDescription = string.Empty;
m_GenericRequester = request;
m_GenericHandler = handler;
m_Param = param;
}
#endregion
#region IUIQueueItem Members
public bool IsSuccesful
{
get
{
return m_bSuccesful;
}
set
{
m_bSuccesful = value;
}
}
public string ErrorDescription
{
get
{
return m_strErrorDescription;
}
set
{
m_strErrorDescription = value;
}
}
public void Operation()
{
m_Response = m_GenericRequester(m_Param);
}
public void OperationCompleted()
{
m_GenericHandler(m_Response);
}
#endregion
#endregion
}
Using the code
My example application is based on my work at Modeling MVVM. I’m running a console application as a server, and a WPF client application which shows a treeview. Run both applications next to each other. Browse the tree. You can see that each node initiates a call to the server.
This is done by using the IViewModel
mechanism. Each time a node gets the focus, it pops up an event. The event routes to an event handler class, which translates the request into a UIQueue
item:
public class UITasksHandler
{
#region Members
Dictionary<ViewModelOperationsEnum,
WorldView.ViewModel.MainViewModel.VMEventHandler> m_EventHandler;
UIQueueManager m_UIQueueManager;
#endregion
#region Methods
#region Ctor
public UITasksHandler(UIQueueManager uiQueueManager)
{
m_EventHandler = new Dictionary<ViewModelOperationsEnum,
MainViewModel.VMEventHandler>();
m_UIQueueManager = uiQueueManager;
InitEventHandlers();
}
private void InitEventHandlers()
{
m_EventHandler.Add(ViewModelOperationsEnum.GetContinentSize,
this.GetContinentSize);
m_EventHandler.Add(ViewModelOperationsEnum.GetCountryCurrency,
this.GetCounrtyCurrency);
m_EventHandler.Add(ViewModelOperationsEnum.GetCityTemprature,
this.GetCityTemprature);
}
#endregion
public void HandleEvent(IViewModel sender, object param,
ViewModelOperationsEnum viewModelEvent)
{
WorldView.ViewModel.MainViewModel.VMEventHandler handler;
if (m_EventHandler.TryGetValue(viewModelEvent, out handler))
{
handler(sender, param);
}
}
private void GetContinentSize(IViewModel sender, object param)
{
IContinentViewModel vm = sender as IContinentViewModel;
if (vm != null)
{
m_UIQueueManager.AddToQueue(new UIQueueItem<double,
string>(ClientFacade.Instance.GetContinentSize,
vm.SetSize, vm.Caption));
}
}
private void GetCounrtyCurrency(IViewModel sender, object param)
{
ICountryViewModel vm = sender as ICountryViewModel;
if (vm != null)
{
m_UIQueueManager.AddToQueue(
new UIQueueItem<WorldDataLib.Model.CurrenciesEnum,
string>(ClientFacade.Instance.GetCountriesCurrency,
vm.SetCurrency, vm.Caption));
}
}
private void GetCityTemprature(IViewModel sender, object param)
{
ICityViewModel vm = sender as ICityViewModel;
if (vm != null)
{
m_UIQueueManager.AddToQueue(new UIQueueItem<double,
string>(ClientFacade.Instance.GetCityCurrentTemprature,
vm.SetTemprature, vm.Caption));
}
}
#endregion
}
History
- July 29 2009: Initial release.