65.9K
CodeProject is changing. Read more.
Home

I/O operations in WPF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (5 votes)

Jul 29, 2009

CPOL

2 min read

viewsIcon

20011

downloadIcon

181

This article presents a method for running all I/O operations of a WPF application in a single thread.

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:

/// <summary>
/// Handle an event queue which is responisble
/// for running requests outside of the Main thread.
/// It would be usually used for running I/O operations
/// </summary>
public class UIQueueManager : IDisposable
{
    #region Constants
    /// <summary>
    /// Limit the operation duration to 10 seconds
    /// </summary>
    const int MAX_OPERATION_DURATION = 10000;

    #endregion

    #region Members

    /// <summary>
    /// The working thread in which the object would run in
    /// </summary>
    Thread m_workingThread;
    /// <summary>
    /// A thread which runs and makes sure that
    /// no request is withholding the system
    /// </summary>
    Thread m_terminationThread;
    /// <summary>
    /// A lock for queues enqueue and dequeue synchronization
    /// </summary>
    object m_lock;
    /// <summary>
    /// The item currently running
    /// </summary>
    KeyValuePair<Guid, IUIQueueItem> m_currentQueueItem;
    /// <summary>
    /// The queue itself
    /// </summary>
    System.Collections.Queue m_operationsQueue;
    /// <summary>
    /// A flag which indicates if it is OK to run the queue
    /// </summary>
    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

    /// <summary>
    /// Add a queue item to the queue. The returned result is a key
    /// Which enables to remove the task from the queue
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    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;
    }

    /// <summary>
    /// Running the queue operation in a synchronos way
    /// </summary>
    public void RunQueue()
    {
        while (m_bRun)
        {
            if (m_operationsQueue.Count > 0)
            {
                KeyValuePair<Guid, IUIQueueItem> kvp = 
                  new KeyValuePair<Guid,IUIQueueItem>();
                try
                {
                    // Get a message out of the queue
                    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);
        }
    }

    /// <summary>
    /// This method runs on seperate thread.
    /// It checks that the thread has been
    /// Updated
    /// </summary>
    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))
            {
                // The thread is in the same queue.
                // The operation should be terminated
                Monitor.Enter(m_lock);
                try
                {
                    m_workingThread.Abort();
                    // Mark the current operation as failed
                    // and send notification to the user
                    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);
                }
            }
        }
    }

    /// <summary>
    /// Set the error notification in case of exception
    /// </summary>
    /// <param name="item"></param>
    /// <param name="ex"></param>
    private void SetItemError(IUIQueueItem item, Exception ex)
    {
        if (item != null)
        {
            item.IsSuccesful = false;
            item.ErrorDescription = ex.Message;
        }
    }

    /// <summary>
    /// Run the operation end. If it runs under UI mode use the begin invoke.
    /// Otherwise, run it normaly.
    /// </summary>
    /// <param name="item"></param>
    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:

/// <summary>
/// An interface for UI queue item
/// </summary>
public interface IUIQueueItem
{
    #region Properties

    /// <summary>
    /// A flag whcih determines if the operation succeeded 
    /// </summary>
    bool IsSuccesful
    {
        get;
        set;
    }

    /// <summary>
    /// A string description of the operations error
    /// </summary>
    string ErrorDescription
    {
        get;
        set;
    }

    #endregion

    #region Methods

    /// <summary>
    /// The main opeartion which runs on the queue
    /// </summary> 
    void Operation();
    /// <summary>
    /// An address to return the callback
    /// </summary>
    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:

/// <summary>
/// This is a generic UI queue item which gets
/// 2 delegates as parameters and handles them
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>    
public class UIQueueItem<T,U> : IUIQueueItem
{
    #region Delegates
    /// <summary>
    /// The base type which is ran by the requester
    /// </summary>
    /// <param name="val"></param>
    /// <returns></returns>
    public delegate T GenericRequester(U val);
    /// <summary>
    /// The generic type which is ran as a response
    /// </summary>
    /// <param name="val"></param>
    public delegate void GenericHandler(T val);

    #endregion

    #region Members

    bool m_bSuccesful;
    string m_strErrorDescription;

    /// <summary>
    /// The delegate which would be ran in the operation
    /// </summary>
    GenericRequester m_GenericRequester;
    /// <summary>
    /// The delegate which would be ran as the response
    /// </summary>
    GenericHandler m_GenericHandler;
    /// <summary>
    /// The parameter for the request
    /// </summary>
    U m_Param;
    /// <summary>
    /// The responses value
    /// </summary>
    T m_Response;

    #endregion

    #region Methods

    #region Ctor

    /// <summary>
    /// The Ctor is initiated with 2 delegates
    /// and a parameter for the delegates
    /// </summary>
    /// <param name="request"></param>
    /// <param name="handler"></param>
    /// <param name="param"></param>
    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:

/// <summary>
/// This class handles the UI Tasks.
/// </summary>
public class UITasksHandler
{
    #region Members

    /// <summary>
    /// A dictionary which holds delegates for handling the events
    /// </summary>
    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.