Click here to Skip to main content
15,879,474 members
Articles / Desktop Programming / WPF

WPF: A Most Useful Threading Component

Rate me:
Please Sign up or sign in to vote.
4.97/5 (54 votes)
30 Dec 2009CPOL19 min read 121K   1.2K   147  
A threading component that shows failures/busy status and data.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Configuration;

namespace ThreadingComponent
{
    /// <summary>
    /// Host that deals with what to show when a threading operation
    /// is busy or fails using the AdornerLayer, and will also show 
    /// the data associated with the threading operation which is expected
    /// to be the actual Content for the ThreadableHostControl. So typical
    /// usage maybe something like the example below.
    /// 
    /// It should be noted that this control DOES use the AdornerLayer, which
    /// from time to time can be tricky to obtain. As such there is the possibility
    /// that the AdornerLayer may be null, and as such this control would not really
    /// work as planned. To deal with this the user can choose how this can be handled
    /// by using the App.Config setting "shouldThrowExceptionOnNullAdornerLayer" which
    /// directs the app to throw an Exception if the AdornerLayer can not be obtained.
    /// 
    /// If the user chooses to set the "shouldThrowExceptionOnNullAdornerLayer" App.Config
    /// value to "false" the threading should all work as expected, it is just the 
    /// busy or fails Adorners will not be shown, and the user will have to work out another way
    /// of dealing with the IsBusy and Failed states of the background threading operation
    /// <example>
    /// <![CDATA[
    /// 
    ///    <local:ThreadableHostControl HorizontalAlignment="Stretch" 
    ///         VerticalAlignment="Stretch"
    ///         ThreadableItem="{Binding ThreadVM}">
    ///        <ListView Background="WhiteSmoke" BorderBrush="Black" BorderThickness="5"
    ///                  ItemsSource="{Binding ThreadVM.Data.DataObject}">
    ///            <ListView.View>
    ///                <GridView>
    ///                    <GridViewColumn Header="Text" Width="500" 
    ///                         DisplayMemberBinding="{Binding Path=Text}"/>
    ///                    <GridViewColumn Header="Age" Width="100" 
    ///                         DisplayMemberBinding="{Binding Path=Age}"/>
    ///                </GridView>
    ///            </ListView.View>
    ///        </ListView>
    ///        
    ///    </local:ThreadableHostControl>
    /// ]]>
    /// </example>
    /// </summary>
    public partial class ThreadableHostControl : UserControl
    {
        #region Data
        private PropertyObserver<ThreadableItemViewModelBase> threadableItemObserver;
        private AdornerLayer adornerLayer;
        private BusyAdorner busyAdorner;
        private FailedAdorner failedAdorner;
        private Boolean shouldThrowExceptionOnNullAdornerLayer = true;
        #endregion

        #region Constructor
        public ThreadableHostControl()
        {
            InitializeComponent();

            //work out whether the user chose to ignore a null AdornerLayer
            //which obviously impacts how the code works, but the Background
            //thread will still run, its just that the Adorners that this class
            //manages will not be shown. The default behaviour for the
            //shouldThrowExceptionOnNullAdornerLayer flag is true
            Boolean.TryParse(
                ConfigurationSettings.AppSettings.Get(
                "ShouldThrowExceptionOnNullAdornerLayer"),
                out shouldThrowExceptionOnNullAdornerLayer);

        }
        #endregion

        #region Overrides
        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);
            SafeResize(busyAdorner, sizeInfo.NewSize);
            SafeResize(failedAdorner, sizeInfo.NewSize);
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// Watches the IsBusy/Failed INPC properties of the 
        /// ThreadableItemViewModelBase and when they change, 
        /// swaps in/out the correct Adorner to suit the current 
        /// state of the ThreadableItemViewModelBase
        /// </summary>
        /// <param name="item">The ThreadableItemViewModelBase to 
        /// watch INPC changes on</param>
        private void SetupPropertyWatcher(ThreadableItemViewModelBase item)
        {
            if (threadableItemObserver != null)
            {
                threadableItemObserver.UnregisterHandler(n => n.IsBusy);
                threadableItemObserver.UnregisterHandler(n => n.Failed);
            }
            threadableItemObserver = new PropertyObserver<ThreadableItemViewModelBase>(item);
            threadableItemObserver.RegisterHandler(n => n.IsBusy, this.IsBusyChanged);
            threadableItemObserver.RegisterHandler(n => n.Failed, this.FailedChanged);
        }

        /// <summary>
        /// Safely removes all CustomAdornerBase from the the AdornerLayer
        /// </summary>
        /// <param name="adorners">Collection of CustomAdornerBase to remove</param>
        private void SafeRemoveAll(List<CustomAdornerBase> adorners)
        {
            foreach (CustomAdornerBase adorner in adorners)
                SafeRemove(adorner);
        }

        /// <summary>
        /// Causes a repaint
        /// </summary>
        private void InvalidateControl()
        {
            this.InvalidateVisual();
            this.InvalidateArrange();
            this.InvalidateMeasure();
        }


        /// <summary>
        /// Shows the BusyAdorner
        /// </summary>
        private void IsBusyChanged(ThreadableItemViewModelBase vm)
        {
            adornerLayer = AdornerLayer.GetAdornerLayer(this);

            //If the users chose to throw an Exception on a null AdornerLayer
            //throw an Exception. The user may change this setting in the App.Config
            //which obviously impacts how the code works, but the Background
            //thread will still run, its just that the Adorners that this class
            //manages will not be shown. Which is not how the code was intended
            //to work. It would be better to find out why the AdornerLayer is null
            if (shouldThrowExceptionOnNullAdornerLayer && adornerLayer == null)
                throw new NotSupportedException(
                    "The ThreadableHostControl will only work correctly\r\n" +
                    "if there is an AdornerLayer found and it is not null");

            if (adornerLayer != null)
            {
                if (vm.IsBusy)
                {
                    SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner,busyAdorner } );
                    busyAdorner = new BusyAdorner(this);
                    adornerLayer.Add(busyAdorner);
                }
                else
                {
                    SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
                }
            }
            //repaint
            InvalidateControl();
        }

        /// <summary>
        /// Shows the FailedAdorner
        /// </summary>
        private void FailedChanged(ThreadableItemViewModelBase vm)
        {
            if (vm.IsBusy)
                return;

            //If the users chose to throw an Exception on a null AdornerLayer
            //throw an Exception. The user may change this setting in the App.Config
            //which obviously impacts how the code works, but the Background
            //thread will still run, its just that the Adorners that this class
            //manages will not be shown. Which is not how the code was intended
            //to work. It would be better to find out why the AdornerLayer is null
            adornerLayer = AdornerLayer.GetAdornerLayer(this);

            if (shouldThrowExceptionOnNullAdornerLayer && adornerLayer == null)
                throw new NotSupportedException(
                    "The ThreadableHostControl will only work correctly\r\n" +
                    "if there is an AdornerLayer found and it is not null");

            if (adornerLayer != null)
            {
                if (vm.Failed)
                {
                    SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
                    failedAdorner = new FailedAdorner(this, vm.ErrorMessage);
                    adornerLayer.Add(failedAdorner);
                }
                else
                {
                    SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
                }
            }
            //repaint
            InvalidateControl();
        }

        private void SafeRemove(Adorner adorner)
        {
            if (adorner != null)
                adornerLayer.Remove(adorner);
        }

        private void SafeResize(IResizableAdornerControl adorner, Size newSize)
        {
            if (adorner != null)
                adorner.ResizeToFillAvailableSpace(newSize);
        }
        #endregion

        #region DPs

        #region ThreadableItem

        /// <summary>
        /// ThreadableItem Dependency Property
        /// </summary>
        public static readonly DependencyProperty ThreadableItemProperty =
            DependencyProperty.Register("ThreadableItem", 
                typeof(ThreadableItemViewModelBase), typeof(ThreadableHostControl),
                    new FrameworkPropertyMetadata((ThreadableItemViewModelBase)null,
                     new PropertyChangedCallback(OnThreadableItemChanged)));

        /// <summary>
        /// Gets or sets the ThreadableItem property.  
        /// </summary>
        public ThreadableItemViewModelBase ThreadableItem
        {
            get { return (ThreadableItemViewModelBase)GetValue(ThreadableItemProperty); }
            set { SetValue(ThreadableItemProperty, value); }
        }

        /// <summary>
        /// Handles changes to the ThreadableItem property.
        /// </summary>
        private static void OnThreadableItemChanged(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue != null)
            {
                ((ThreadableHostControl)d).SetupPropertyWatcher(
                    (ThreadableItemViewModelBase)e.NewValue);
            }
        }
        #endregion
        #endregion

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions