Click here to Skip to main content
Click here to Skip to main content
 
Add your own
alternative version

A Simple Solution to Some Problems with Asynchrony in Silverlight

, 9 Mar 2010 CPOL
A small, extensible suite of classes that elegantly solve some common problems involving asynchrony and event handling that tend to occur when Silverlight and WCF are used together.
TaskManagerDemo.zip
TaskManagerDemo
Bin
Debug
Controls
Properties
Service References
ExampleServiceReference
configuration.svcinfo
configuration91.svcinfo
ExampleService.disco
ExampleService.wsdl
Reference.svcmap
TaskManagerDemo.ExampleServiceReference.CityZipCode.datasource
ServiceReferences.ClientConfig
Themes
Utility
TaskManagerDemo.Web
App_Data
bin
ClientBin
DataContracts
ExampleService.svc
Properties
TaskManagerDemo.Web.csproj.user
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using TaskManagerDemo.Controls;
using TaskManagerDemo.ExampleServiceReference;
using TaskManagerDemo.Utility;

namespace TaskManagerDemo
{
    public partial class MainPage : UserControl
    {
        #region /* Demo of TaskManager features and usage */

        private const bool easierVersion = true;

        private ExampleServiceClient serviceClient = new ExampleServiceClient();
        private Random random = new Random();
        private int factor;

        // Declare and partially initialize the required TaskManager objects.
        private TaskManager refreshTask = new TaskManager("refresh") { AutoComplete = true };
        private TaskManager initialGetTask = new TaskManager("initialGetCities") { AutoComplete = true };
        private AsyncServiceCallManager getAllCityZipCodesTask = new AsyncServiceCallManager("getAllCityZipCodes");
        private AsyncServiceCallManager getSelectedCitiesTask = new AsyncServiceCallManager("getSelectedCities");
        private QueuedAsyncServiceCallManager getSelectedZipCodesTask = new QueuedAsyncServiceCallManager("getSelectedZipCodes");

        private List<CityZipCode> allCityZipCodeList;
        private List<CityZipCode> selectedCityList;
        private List<CityZipCode> selectedZipCodeList;
        private CityComparer cityComparer = new CityComparer();
        private ZipCodeComparer zipCodeComparer = new ZipCodeComparer();

        public MainPage()
        {
            InitializeComponent();

            App.Current.Host.Content.Resized += new EventHandler(Content_Resized);

            // Finish initializing the TaskManager objects, building a tree with two levels:
            //     refreshTask
            //     +- initialGetTask
            //     |  +- getAllCityZipCodesTask
            //     |  +- getSelectedCitiesTask
            //     +- getSelectedZipCodesTask
            initialGetTask.AddChildren(new TaskManager[] { getAllCityZipCodesTask, getSelectedCitiesTask });
            refreshTask.AddChildren(new TaskManager[] { initialGetTask, getSelectedZipCodesTask });
            getSelectedZipCodesTask.Prerequisite = initialGetTask;
            refreshTask.StatusChanged += new EventHandler<EventArgs>(refreshTask_StatusChanged);
            refreshTask.SetAllStatus(TaskStatus.NotStarted);

            serviceClient.GetAllCityZipCodesCompleted += new EventHandler<GetAllCityZipCodesCompletedEventArgs>(serviceClient_GetAllCityZipCodesCompleted);
            serviceClient.GetSelectedCitiesCompleted += new EventHandler<GetSelectedCitiesCompletedEventArgs>(serviceClient_GetSelectedCitiesCompleted);
            serviceClient.GetSelectedZipCodesCompleted += new EventHandler<GetSelectedZipCodesCompletedEventArgs>(serviceClient_GetSelectedZipCodesCompleted);

            SetWindowDimensions();
            RefreshData();

            DemoNewFeatures();
        }

        /// <summary>
        /// If run in the debugger, demonstrates modifications and added features as of March, 2010.
        /// </summary>
        private void DemoNewFeatures()
        {
            TaskManager initTaskManager = new TaskManager("init") { AutoComplete = true };
            AsyncServiceCallManager initPart1TaskManager = new AsyncServiceCallManager("initPart1");
            AsyncServiceCallManager initPart2TaskManager = new AsyncServiceCallManager("initPart2");
            initTaskManager.AddChildren(new TaskManager[] { initPart1TaskManager, initPart2TaskManager });
            TaskManager refreshAllTaskManager = new TaskManager("refresh") { AutoComplete = true };
            AsyncServiceCallManager refreshPart1TaskManager = new AsyncServiceCallManager("refreshPart1");
            AsyncServiceCallManager refreshPart2TaskManager = new AsyncServiceCallManager("refreshPart2");
            refreshAllTaskManager.AddChildren(new TaskManager[] { initTaskManager, refreshPart1TaskManager, refreshPart2TaskManager });
            refreshAllTaskManager.SetAllStatus(TaskStatus.NotStarted);

            string[] statusesArray = new string[6];

            refreshAllTaskManager.SetAllStatus(TaskStatus.InProgress);
            statusesArray[0] = DumpStatuses(refreshAllTaskManager);

            // Imagine a sequence of service calls that progressively completes the full set of tasks.
            initPart1TaskManager.Status = TaskStatus.Completed;
            initPart2TaskManager.Status = TaskStatus.Completed;
            refreshPart1TaskManager.Status = TaskStatus.Completed;
            refreshPart2TaskManager.Status = TaskStatus.Completed;
            statusesArray[1] = DumpStatuses(refreshAllTaskManager);

            // We want to leave the task tree intact, refresh often and occasionally re-initialize, with or without doing the refresh tasks.

            // Here is how we would work with the init tasks separately from the refresh tasks, without modifying the task tree.
            initTaskManager.SetAllStatus(TaskStatus.InProgress, true);
            statusesArray[2] = DumpStatuses(refreshAllTaskManager);

            // Imagine a sequence of service calls that progressively completes the init tasks.
            initPart1TaskManager.Status = TaskStatus.Completed;
            initPart2TaskManager.Status = TaskStatus.Completed;
            statusesArray[3] = DumpStatuses(refreshAllTaskManager);

            // Here is how we would work with the refresh tasks separately from the init tasks, without modifying the task tree.
            refreshAllTaskManager.SetAllStatus(TaskStatus.InProgress, node => (node != initTaskManager && initTaskManager.FindDescendant(node.Name) == null));
            statusesArray[4] = DumpStatuses(refreshAllTaskManager);

            // Imagine a sequence of service calls that progressively completes the refresh tasks.
            refreshPart1TaskManager.Status = TaskStatus.Completed;
            refreshPart2TaskManager.Status = TaskStatus.Completed;
            statusesArray[5] = DumpStatuses(refreshAllTaskManager);

            // Set a breakpoint on the method's closing curly braceto examine the statusesArray.
        }

        private string DumpStatuses(TaskManager root)
        {
            string result = string.Empty;
            root.ForEach(node =>
            {
                if (result.Length > 0)
                {
                    result += "; ";
                }
                result += node.Name + ": " + node.Status.ToString();
            });
            return result;
        }

        private void serviceClient_GetAllCityZipCodesCompleted(object sender, GetAllCityZipCodesCompletedEventArgs e)
        {
            allCityZipCodeList = e.Result.ToList();
            getAllCityZipCodesTask.Status = TaskStatus.Completed;
        }

        private void serviceClient_GetSelectedCitiesCompleted(object sender, GetSelectedCitiesCompletedEventArgs e)
        {
            selectedCityList = e.Result.ToList();
            if (selectedCityList.Count == 0)
            {
                getSelectedCitiesTask.Tag = new Exception("No selected Cities found.");
            }
            getSelectedCitiesTask.Status = TaskStatus.Completed;
        }

        private void serviceClient_GetSelectedZipCodesCompleted(object sender, GetSelectedZipCodesCompletedEventArgs e)
        {
            if (getSelectedZipCodesTask.Prerequisite.Status == TaskStatus.Completed)
            {
                selectedZipCodeList = e.Result.ToList();
                if (selectedZipCodeList.Count == 0)
                {
                    getSelectedZipCodesTask.Tag = new Exception("No selected ZIP Codes found.");
                }
                else if (selectedCityList.Count > 0)
                {
                    #region /* Code that requires selectedCityList and selectedZipCodeList both to be refreshed */

                    // Some ZIP Codes may contain more than one "city" (small town).
                    // First get all of the CityZipCode correlations having ZIP Code value found in the list of selected Cities.
                    List<CityZipCode> filteredAllZipCodeList = allCityZipCodeList
                        .Where(zipCode => selectedCityList.FirstOrDefault(city => city.ZipCode == zipCode.ZipCode) != null).ToList();
                    // Get a list of distinct Cities such that each City is found (at least once) in the above list.
                    List<CityZipCode> newSelectedCityList = selectedCityList
                        .Where(city => filteredAllZipCodeList.FirstOrDefault(zipCode => zipCode.City == city.City) != null)
                        .Distinct(cityComparer).ToList();
                    // Replace the ZIP Code property of each correlation in the latter list with a CSV containing all of the ZIP Codes found in the City.
                    newSelectedCityList.ForEach(city => city.ZipCode = GetZipCodeCsv(selectedZipCodeList.Where(zipCode => zipCode.City == city.City).ToList()));

                    // Some cities contain more than one ZIP Code.
                    // First get all of the CityZipCode correlations having City value found in the list of selected ZIP Codes.
                    List<CityZipCode> filteredAllCityList = allCityZipCodeList
                        .Where(city => selectedZipCodeList.FirstOrDefault(zipCode => zipCode.City == city.City) != null).ToList();
                    // Get a list of distinct ZIP Codes such that each ZIP Code is found (at least once) in the above list.
                    List<CityZipCode> newSelectedZipCodeList = selectedZipCodeList
                        .Where(zipCode => filteredAllCityList.FirstOrDefault(city => city.ZipCode == zipCode.ZipCode) != null)
                        .Distinct(zipCodeComparer).ToList();
                    // Replace the City property of each correlation in the latter list with a CSV containing all of the Cities found in the ZIP Code.
                    newSelectedZipCodeList.ForEach(zipCode => zipCode.City = GetCityCsv(selectedCityList.Where(city => city.ZipCode == zipCode.ZipCode).ToList()));

                    if (easierVersion)
                    {
                        // Eliminate Cities that aren't in the list of selected ZIP Codes, and vice versa.
                        selectedCityList = newSelectedCityList.Where(city => newSelectedZipCodeList.FirstOrDefault(zipCode => zipCode.City.Contains(city.City)) != null).ToList();
                        selectedZipCodeList = newSelectedZipCodeList.Where(zipCode => newSelectedCityList.FirstOrDefault(city => city.ZipCode.Contains(zipCode.ZipCode)) != null).ToList();
                    }
                    else // harder version
                    {
                        // Some Cities may not have correlated ZIP Codes (and vice versa).
                        selectedCityList = newSelectedCityList;
                        selectedZipCodeList = newSelectedZipCodeList;
                    }

                    // Scramble the order of ZIP Codes relative to Cities. (Cities remain in alphabetical order.)
                    selectedZipCodeList.Sort(delegate(CityZipCode czc1, CityZipCode czc2)
                    {
                        int czc1hc = czc1.GetHashCode();
                        int czc2hc = czc2.GetHashCode();
                        return Math.Sign(czc1hc - czc2hc);
                    });

                    if (selectedCityList.Count == 0 || selectedZipCodeList.Count == 0)
                    {
                        refreshTask.Tag = new Exception("Empty City list and/or Zip Code list.");
                    }
                    else if (selectedCityList.Count == 1 && selectedZipCodeList.Count == 1)
                    {
                        refreshTask.Tag = new Exception("Only one City and one ZIP Code - too easy!");
                    }

                    #endregion /* Code that requires selectedCityList and selectedZipCodeList both to be refreshed */
                }
                getSelectedZipCodesTask.Status = TaskStatus.Completed;
            }
            else
            {
                getSelectedZipCodesTask.EnqueueResultHandling(new GenericEventHandler(GetSelectedZipCodesCompletedHelper), sender, e);
            }
        }

        private string GetZipCodeCsv(List<CityZipCode> zipCodeList)
        {
            string result = string.Empty;
            foreach (CityZipCode zipCode in zipCodeList)
            {
                if (result.Length > 0)
                {
                    result += ",";
                }
                result += zipCode.ZipCode;
            }
            return result;
        }

        private string GetCityCsv(List<CityZipCode> zipCodeList)
        {
            string result = string.Empty;
            foreach (CityZipCode city in zipCodeList)
            {
                if (result.Length > 0)
                {
                    result += ",";
                }
                result += city.City;
            }
            return result;
        }

        private void GetSelectedZipCodesCompletedHelper(object sender, object e)
        {
            serviceClient_GetSelectedZipCodesCompleted(sender, (GetSelectedZipCodesCompletedEventArgs)e);
        }

        private void refreshTask_StatusChanged(object sender, EventArgs e)
        {
            if (refreshTask.Status == TaskStatus.Completed)
            {
                if (initialGetTask.Parent != null)
                {
                    // Rearrange the TaskManager tree after the initial set of calls. The new tree, matching the service method
                    // calls required to refresh the UI, is structured like this:
                    //     refreshTask
                    //     +- getSelectedZipCodesTask
                    //     +- getSelectedCitiesTask
                    // (It's assumed that the order of the child tasks is unimportant.)
                    // Notice also that the Prerequisite property of getSelectedZipCodesTask must be updated.
                    refreshTask.RemoveChild(initialGetTask);
                    initialGetTask.RemoveChild(getSelectedCitiesTask);
                    refreshTask.AddChild(getSelectedCitiesTask);
                    getSelectedZipCodesTask.Prerequisite = getSelectedCitiesTask;
                }

                ShowTaskErrors(refreshTask);

                CityComboBox.ItemsSource = null;
                CityComboBox.ItemsSource = selectedCityList;
                ZipCodeComboBox.ItemsSource = null;
                ZipCodeComboBox.ItemsSource = selectedZipCodeList;

                CloseInProgressDialog(this);
            }
        }

        private void RefreshButton_Click(object sender, RoutedEventArgs e)
        {
            // The redundant calls here are made deliberately for demonstration purposes, but similar redundancy can arise
            // as a side-effect of interactions among event handlers and other necessary bits of code. A simple alternative
            // to almost certainly more complicated ways of preventing or solving the problem is shown within RefreshData.
            RefreshData();
            RefreshData();
            RefreshData();
        }

        private void RefreshData()
        {
            if (refreshTask.Status != TaskStatus.InProgress)
            {
                refreshTask.SetAllStatus(TaskStatus.InProgress);

                ShowInProgressDialog(this, "In Progress ...", "Data Loading");
                if (initialGetTask.Parent != null)
                {
                    // First time only.
                    serviceClient.GetAllCityZipCodesAsync();
                }
                factor = random.Next();
                serviceClient.GetSelectedCitiesAsync(factor);
                serviceClient.GetSelectedZipCodesAsync(factor);
            }
        }

        private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (CityComboBox.SelectedItem != null && ZipCodeComboBox.SelectedItem != null)
            {
                // The following expressions are functionally equivalent:
                //bool itemsCorrelate = (((CityZipCode)CityComboBox.SelectedItem).ZipCode.Contains(((CityZipCode)ZipCodeComboBox.SelectedItem).ZipCode));
                bool itemsCorrelate = (((CityZipCode)ZipCodeComboBox.SelectedItem).City.Contains(((CityZipCode)CityComboBox.SelectedItem).City));
                IsZipCodeInCityTextBlock.Text = string.Format("ZIP Code {0} with city.", itemsCorrelate ? "correlates" : "does not correlate");
                IsZipCodeInCityTextBlock.Foreground = itemsCorrelate ? solidBlackBrush : solidRedBrush;
            }
            else
            {
                IsZipCodeInCityTextBlock.Text = string.Empty;
            }
        }

        public void ShowTaskErrors(TaskManager currentTask)
        {
            TaskManager thisTask = currentTask;
            while (thisTask != null && thisTask.Tag == null)
            {
                thisTask = TaskManager.WalkToNextFrom(thisTask);
            }
            if (thisTask != null && thisTask.Tag != null)
            {
                TaskManager nextTask = TaskManager.WalkToNextFrom(thisTask);

                object errorInfo = thisTask.Tag;
                thisTask.Tag = null;

                Exception ex = errorInfo as Exception;
                if (ex != null)
                {
                    // ShowMessageDialog shows the error message.
                    // When the user clicks OK, the click event handler calls ShowTaskErrors,
                    // passing nextTask (temporarily saved in savedNextTask) as currentTask.
                    // Thus the tree of TaskManager objects is walked and all attached error messages are displayed.
                    ShowMessageDialog(ex.Message + ex.StackTrace, "An error occurred:", nextTask);
                }
            }
        }

        #endregion /* Demo of TaskManager features and usage */
        #region /* Specific implementations of popup "in progress" dialog and message dialog */

        private SolidColorBrush solidBlackBrush = new SolidColorBrush(Colors.Black);
        private SolidColorBrush solidRedBrush = new SolidColorBrush(Colors.Red);
        private double windowHeight;
        private double windowWidth;
        private Point dialogLocation = new Point(100, 100);
        private bool isInProgressDialogVisible;
        private InProgressDialog inProgressDialog;
        private PopupWindow popupWindowForDialog;
        private InputDialog inputDialog;
        private TaskManager savedNextTask;

        private void Content_Resized(object sender, EventArgs e)
        {
            SetWindowDimensions();
        }

        private void SetWindowDimensions()
        {
            windowHeight = (App.Current.Host.Content.ActualHeight - 30);
            windowWidth = App.Current.Host.Content.ActualWidth - 50;
            if (windowWidth < this.MinWidth)
                windowWidth = this.MinWidth;
        }

        public void ShowInProgressDialog(object sender, string title, string text)
        {
            if (!isInProgressDialogVisible)
            {
                inProgressDialog = new InProgressDialog();
                inProgressDialog.TitleDialog = title;
                inProgressDialog.MessageText = text;
                inProgressDialog.ResetWidth();
                NewWindow(true, "In Progress...", sender);
                isInProgressDialogVisible = true;
            }
        }

        public void CloseInProgressDialog(object sender)
        {
            RemoveWindow("In Progress...", sender);
            isInProgressDialogVisible = false;
        }

        public void ShowMessageDialog(string text, string title, TaskManager nextTask)
        {
            savedNextTask = nextTask;
            ShowMessageDialog(text, title);
        }

        public void ShowMessageDialog(string text, string title)
        {
            double width = 250;
            double height = 120;
            GetDialogDimensions(text, title, out width, out height);

            inputDialog = new InputDialog();
            inputDialog.OK += new EventHandler<EventArgs>(InputDialogMessageDialog_OK);

            inputDialog.Width = width;
            inputDialog.Height = height;
            inputDialog.SetButtonsContent("OK", string.Empty, text, width, height);

            NewWindow(true, title, this);
        }

        private void GetDialogDimensions(string text, string title, out double width, out double height)
        {
            text = string.IsNullOrEmpty(text) ? string.Empty : text.Trim();
            title = string.IsNullOrEmpty(title) ? string.Empty : title.Trim();
            double textWidth = (text.Length > 100 ? 100 : text.Length) * 10 + 60;
            double titleWidth = title.Length * 11 + 60;
            width = Math.Min(Math.Max(textWidth, titleWidth), 400);
            double textHeight = text.Length * 11 + 30;
            textHeight = GetHeightFromText(text);
            height = Math.Min(textHeight, 400);
        }

        private double GetHeightFromText(string text)
        {
            string[] splitText = text.Split('\n');
            double height = 95;
            for (int i = 0; i < splitText.Length; i++)
            {
                int length = splitText[i].Length;
                if (length < 100)
                    height = height + 30;
                else
                    height = height + (length / 100) * 30;
            }
            return height;
        }

        private void InputDialogMessageDialog_OK(object sender, EventArgs e)
        {
            RemoveWindow(popupWindowForDialog.Title, sender);

            if (savedNextTask != null)
            {
                ShowTaskErrors(savedNextTask);
            }
        }

        public void NewWindow(bool Modal, string windowType, object source)
        {
            popupWindowForDialog = new PopupWindow();
            popupWindowForDialog.IsModal = Modal;
            popupWindowForDialog.CanResize = false;
            popupWindowForDialog.ShowStatus = false;
            popupWindowForDialog.Title = windowType;
            popupWindowForDialog.IsCloseButtonVisible = true;

            switch (windowType)
            {
                case "In Progress...":
                    popupWindowForDialog.IsCloseButtonVisible = false;
                    popupWindowForDialog.ShowCloseButton();
                    popupWindowForDialog.Content = inProgressDialog;
                    popupWindowForDialog.Height = inProgressDialog.VerticalOffsetY + 30;
                    double left = windowWidth / 2 - inProgressDialog.HorizontalOffsetX / 2;
                    double top;
                    if (dialogLocation == null)
                    {
                        top = inProgressDialog.VerticalOffsetY / 2;

                        popupWindowForDialog.OffsetX = left;
                        popupWindowForDialog.OffsetY = top;
                    }
                    else
                    {
                        top = dialogLocation.Y - inProgressDialog.VerticalOffsetY - 10;
                        if (top <= inProgressDialog.VerticalOffsetY)
                            top = inProgressDialog.VerticalOffsetY;
                        dialogLocation = new Point(left, top);

                        popupWindowForDialog.OffsetX = dialogLocation.X;
                        popupWindowForDialog.OffsetY = dialogLocation.Y;
                    }
                    LayoutRoot.Children.Add(popupWindowForDialog);
                    Grid.SetRow(popupWindowForDialog, 0);
                    Grid.SetRowSpan(popupWindowForDialog, 6);
                    Grid.SetColumn(popupWindowForDialog, 0);
                    Grid.SetColumnSpan(popupWindowForDialog, 5);
                    break;

                default:
                    popupWindowForDialog.IsCloseButtonVisible = false;
                    popupWindowForDialog.ShowCloseButton();
                    popupWindowForDialog.Content = inputDialog;
                    popupWindowForDialog.Width = inputDialog.Width;
                    popupWindowForDialog.Height = inputDialog.Height + 30;
                    if (dialogLocation == null)
                    {
                        popupWindowForDialog.OffsetX = inputDialog.Width;
                        popupWindowForDialog.OffsetY = inputDialog.Height / 2;
                    }
                    else
                    {
                        popupWindowForDialog.OffsetX = dialogLocation.X;
                        popupWindowForDialog.OffsetY = dialogLocation.Y;
                    }
                    LayoutRoot.Children.Add(popupWindowForDialog);
                    Grid.SetRow(popupWindowForDialog, 0);
                    Grid.SetRowSpan(popupWindowForDialog, 6);
                    Grid.SetColumn(popupWindowForDialog, 0);
                    Grid.SetColumnSpan(popupWindowForDialog, 5);
                    break;
            }
        }

        private void RemoveWindow(string windowName, object source)
        {
            try
            {
                List<DragDropControl> list = new List<DragDropControl>();
                bool found = false;
                foreach (UIElement u in LayoutRoot.Children)
                {
                    DragDropControl dd = u as DragDropControl;
                    if (dd != null)
                        list.Add(dd);
                }
                foreach (UIElement u in list)
                {
                    if (u.GetType() == typeof(PopupWindow))
                    {
                        PopupWindow w = u as PopupWindow;
                        if (w.Title == windowName)
                        {
                            LayoutRoot.Children.Remove(u);
                            w.Close();
                            found = true;
                        }
                        else
                        {
                            if (w.Content.GetType() == typeof(InputDialog) && source.GetType() == typeof(InputDialog))
                            {
                                InputDialog dialog = (InputDialog)w.Content;
                                InputDialog sourceDialog = (InputDialog)source;
                                if (dialog == sourceDialog)
                                {
                                    LayoutRoot.Children.Remove(u);
                                    w.Close();
                                    found = true;
                                }
                                else if (dialog.OK == null)
                                {
                                    dialog.OK += new EventHandler<EventArgs>(InputDialogMessageDialog_OK);
                                    dialog.CloseDialog();
                                }
                            }
                        }
                    }
                }
                if (!found)
                {
                    foreach (UIElement u in list)
                    {
                        if (u.GetType() == typeof(PopupWindow))
                        {
                            PopupWindow w = u as PopupWindow;
                            if (w.Content.GetType() == typeof(InputDialog))
                            {
                                InputDialog dialog = (InputDialog)w.Content;
                                if (dialog.OK == null)
                                {
                                    dialog.OK += new EventHandler<EventArgs>(InputDialogMessageDialog_OK);
                                    dialog.CloseDialog();
                                }
                            }
                        }
                    }
                }
            }
            catch { }
        }

        #endregion /* Specific implementations of popup "in progress" dialog and message dialog */
    }
}

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)

Share

About the Author

George Henry 1954
Software Developer (Senior) Colibrium Partners. LLC
United States United States
George Henry has worked as a software developer for more than 20 years. He is currently employed by Colibrium in Bellevue, Washington, USA as a Technical Analyst, working on the company's Tuo software suite - both the base product and customizations for clients.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.150128.1 | Last Updated 9 Mar 2010
Article Copyright 2009 by George Henry 1954
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid