Click here to Skip to main content
Click here to Skip to main content

I/O operations in WPF

By , 29 Jul 2009
Rate this:
Please Sign up or sign in to vote.

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Izhar Lotem
Software Developer
Israel Israel
Software Developer in a promising Clean-Tech company

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 29 Jul 2009
Article Copyright 2009 by Izhar Lotem
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid