Click here to Skip to main content
15,885,366 members
Articles / Desktop Programming / WPF

200% Reflective Class Diagram Creation Tool

Rate me:
Please Sign up or sign in to vote.
4.92/5 (200 votes)
20 Feb 2014CPOL22 min read 579K   11.5K   437  
WPF: Version II of my 100% Reflective class diagram creation tool.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

using Cinch;
using GraphSharp.Controls;
using MEFedMVVM.ViewModelLocator;
using SUT.PrintEngine.Utils;


namespace AutoDiagrammer
{
    public class PocGraphLayout : GraphLayout<PocVertex, PocEdge, PocGraph> 
    {

    }

    [ExportViewModel("MainWindowViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class MainWindowViewModel : ViewModelBase
    {
        #region Data

        private PocGraphLayout graphLayout = new PocGraphLayout();
        private PocGraph graph;
        private IViewAwareStatus viewAwareStatusService;
        private IMessageBoxService messageBoxService;
        private IUIVisualizerService uiVisualizerService;
        private IOpenFileService openFileService;
        private ISaveFileService saveFileService;
        private ISavePNGFileService savePNGService;
        private IPrintPNGFileService printPNGFileService;
        private IGraphPrintableWindow graphPrintableWindow;
        private IAssemblyManipulationService assemblyManipulationService;
        private List<AssemblyTreeViewModel> treeValues = new List<AssemblyTreeViewModel>();
        private List<PocVertex> notAssociatedVertices = new List<PocVertex>();
        private PocVertex selectedNotAssociatedVertex = null;
        private bool hasNotAssociatedVertices = false;

        private bool hasActiveGraph = false;
        private bool hasActiveAssembly = false;
        private bool isGenerallyBusy = false;

        private string mainWaitText;
        private string mainErrorMessage;
        private AsyncType mainAsyncState = AsyncType.Content;

        private string drawerWaitText;
        private string drawerErrorMessage;
        private AsyncType drawerAsyncState = AsyncType.Content;

        #endregion

        #region Ctor
        [ImportingConstructor]
        public MainWindowViewModel(
            IViewAwareStatus viewAwareStatusService, 
            IMessageBoxService messageBoxService,
            IUIVisualizerService uiVisualizerService,
            IOpenFileService openFileService,
            ISaveFileService saveFileService,
            ISavePNGFileService savePNGService,
            IPrintPNGFileService printPNGFileService,
            IAssemblyManipulationService assemblyManipulationService
            )
        {
            

            SettingsViewModel.Instance.SetGraphObject(graphLayout);
            this.assemblyManipulationService = assemblyManipulationService;


            this.viewAwareStatusService = viewAwareStatusService;
            this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
            this.messageBoxService = messageBoxService;
            this.uiVisualizerService = uiVisualizerService;
            this.openFileService = openFileService;
            this.saveFileService = saveFileService;
            this.savePNGService = savePNGService;
            this.printPNGFileService = printPNGFileService;

            
            //Commands
            SaveSettingsAsXmlCommand = new SimpleCommand<Object, Object>(ExecuteSaveSettingsAsXmlCommand);
            ShowSettingsWindowCommand = new SimpleCommand<Object, Object>(NotBusyCheck,ExecuteShowSettingsWindowCommand);
            OpenFileCommand = new SimpleCommand<Object, Object>(NotBusyCheck, ExecuteOpenFileCommand);
            OpenFileDragCommand = new SimpleCommand<Object, Object>(NotBusyCheck, ExecuteOpenFileDragCommand);
            CommenceDrawingCommand = new SimpleCommand<Object, Object>(CanExecuteCommenceDrawingCommand, ExecuteCommenceDrawingCommand);
            ReLayoutCommand = new SimpleCommand<Object, Object>(NotBusyCheckAndNotDrawingAndHasGraphCheck, ExecuteReLayoutCommand);
            SaveCommand = new SimpleCommand<Object, Object>(NotBusyCheckAndNotDrawingAndHasGraphCheck, ExecuteSaveCommand);
            PrintFileCommand = new SimpleCommand<Object, Object>(NotBusyCheckAndNotDrawingAndHasGraphCheck, ExecutePrintFileCommand);
            ShowSpecificNotAssociatedVertexCommand = new SimpleCommand<Object, Object>(ExecuteShowSpecificNotAssociatedVertexCommand);
            AboutCommand = new SimpleCommand<Object, Object>(NotBusyCheck, ExecuteAboutCommand);
            HelpCommand = new SimpleCommand<Object, Object>(NotBusyCheck, ExecuteHelpCommand);

            CollapseVertexRegionsCommand = new SimpleCommand<Object, Object>(NotBusyCheckAndNotDrawingAndHasGraphCheck, ExecuteCollapseVertexRegionsCommand); 

            Mediator.Instance.Register(this);
            
        }
        #endregion

        #region Mediator Message Sinks

        [MediatorMessageSink("TreeStateChangedMessage")]
        void OnTreeStateChangedMessage(bool dummy)
        {
            assemblyManipulationService.CalculateSelectedTreeValues();
        }

        #endregion

        #region Private Methods

        private void ProcessAssemblyFile(string fileName)
        {
            if (DotNetObject.IsValidDotNetAssembly(fileName))
            {

                MainAsyncState = AsyncType.Busy;
                ApplicationHelper.DoEvents();
                assemblyManipulationService.ReInitialise();

                Task task = assemblyManipulationService.LoadNameSpacesAndTypes(fileName);

                int timeout = SettingsViewModel.Instance.DllLoadingTimeOutInSeconds * 1000;
                bool finishedOk = task.Wait(timeout); // wait 20 seconds before timing out

                if (finishedOk)
                {
                    TreeValues = assemblyManipulationService.TreeValues;
                    HasActiveAssembly = true;
                    MainAsyncState = AsyncType.Content;
                }
                else
                {
                    messageBoxService.ShowError(String.Format(
                        "The loading and reflecting of the Dll/Exe took longer than {0} seconds, maybe try increase this setting and try again",
                        SettingsViewModel.Instance.DllLoadingTimeOutInSeconds));
                }
            }
            else
            {
                HasActiveAssembly = false;
                messageBoxService.ShowError(String.Format("The file {0} is not a valid .NET Assembly", fileName));
            }
        }


        private bool NotBusyCheck(object parameter)
        {
            return !(MainAsyncState == AsyncType.Busy) && !(DrawerAsyncState == AsyncType.Busy) &&
                !isGenerallyBusy;
        }

        private bool NotBusyCheckAndNotDrawingCheck(object parameter)
        {
            return !(MainAsyncState == AsyncType.Busy) && !(DrawerAsyncState == AsyncType.Busy) &&
                HasActiveAssembly;
        }

       

        private bool NotBusyCheckAndNotDrawingAndHasGraphCheck(object parameter)
        {
            return !(MainAsyncState == AsyncType.Busy) && !(DrawerAsyncState == AsyncType.Busy) &&
                HasActiveAssembly && hasActiveGraph;
        }
 
        private void ViewAwareStatusService_ViewLoaded()
        {
            SettingsViewModel.Instance.RehydrateSettingsFromXmlCommand.Execute(null);

            IGraphPrintableWindow graphPrintableWin = 
                this.viewAwareStatusService.View as IGraphPrintableWindow;
            if (graphPrintableWin != null)
            {
                graphPrintableWindow = graphPrintableWin;
            }

        }

        private void AddItemsToGraph(GraphResults graphResults)
        {

            NotAssociatedVertices = graphResults.Vertices
                                    .Where(v => v.NumberOfEdgesFromThisVertex == 0 &&
                                                v.NumberOfEdgesToThisVertex == 0)
                                    .OrderBy(x => x.Name).ToList();
            HasNotAssociatedVertices = NotAssociatedVertices.Any();


            graph = new PocGraph(true);
            graphLayout.Graph = graph;

            graph.Clear();

            List<PocVertex> vertices = graphResults.Vertices
                .Where(v => v.NumberOfEdgesFromThisVertex > 0 ||
                            v.NumberOfEdgesToThisVertex > 0).ToList();

            foreach (PocVertex vertex in vertices)
            {
                if (vertex != null)
                    graph.AddVertex(vertex);
            }

            foreach (PocEdge edge in graphResults.Edges)
            {
                if(edge != null)
                    graph.AddEdge(edge);
            }

            NotifyPropertyChanged(graphLayoutArgs);
        }

        #endregion

        #region Public Properties

        public ICommand ShowSettingsWindowCommand { get; private set; }
        public ICommand SaveSettingsAsXmlCommand { get; private set; }
        public ICommand ReLayoutCommand { get; private set; }
        public ICommand AboutCommand { get; private set; }
        public ICommand SaveCommand { get; private set; }
        public ICommand OpenFileCommand { get; private set; }
        public ICommand OpenFileDragCommand { get; private set; }
        public ICommand PrintFileCommand { get; private set; }
        public ICommand CommenceDrawingCommand { get; private set; }
        public ICommand CancelDrawingCommand { get; private set; }
        public ICommand ShowSpecificNotAssociatedVertexCommand { get; private set; }
        public ICommand HelpCommand { get; private set; }
        public ICommand CollapseVertexRegionsCommand { get; private set; }


        


        /// <summary>
        /// NotAssociatedVertices
        /// </summary>
        static PropertyChangedEventArgs notAssociatedVerticesArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.NotAssociatedVertices);

        public List<PocVertex> NotAssociatedVertices
        {
            get { return notAssociatedVertices; }
            private set
            {
                notAssociatedVertices = value;
                NotifyPropertyChanged(notAssociatedVerticesArgs);
            }
        }

        /// <summary>
        /// SelectedNotAssociatedVertex
        /// </summary>
        static PropertyChangedEventArgs selectedNotAssociatedVertexArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.SelectedNotAssociatedVertex);

        public PocVertex SelectedNotAssociatedVertex
        {
            get { return selectedNotAssociatedVertex; }
            set
            {
                selectedNotAssociatedVertex = value;
                NotifyPropertyChanged(selectedNotAssociatedVertexArgs);
            }
        }

        /// <summary>
        /// HasNotAssociatedVertices
        /// </summary>
        static PropertyChangedEventArgs hasNotAssociatedVerticesArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.HasNotAssociatedVertices);

        public bool HasNotAssociatedVertices
        {
            get { return hasNotAssociatedVertices; }
            private set
            {
                hasNotAssociatedVertices = value;
                NotifyPropertyChanged(hasNotAssociatedVerticesArgs);
            }
        }






        
        /// <summary>
        /// TreeValues
        /// </summary>
        static PropertyChangedEventArgs treeValuesArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.TreeValues);

        public List<AssemblyTreeViewModel> TreeValues
        {
            get { return treeValues; }
            private set
            {
                treeValues = value;
                NotifyPropertyChanged(treeValuesArgs);
            }
        }



        /// <summary>
        /// HasActiveAssembly
        /// </summary>
        static PropertyChangedEventArgs hasActiveAssemblyArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.HasActiveAssembly);

        public bool HasActiveAssembly
        {
            get { return hasActiveAssembly; }
            private set
            {
                hasActiveAssembly = value;
                NotifyPropertyChanged(hasActiveAssemblyArgs);
            }
        }


        /// <summary>
        /// MainAsyncState
        /// </summary>
        static PropertyChangedEventArgs mainAsyncStateArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.MainAsyncState);

        public AsyncType MainAsyncState
        {
            get { return mainAsyncState; }
            private set
            {
                mainAsyncState = value;
                NotifyPropertyChanged(mainAsyncStateArgs);
            }
        }



        /// <summary>
        /// MainWaitText
        /// </summary>
        static PropertyChangedEventArgs mainWaitTextArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.MainWaitText);


        public string MainWaitText
        {
            get { return mainWaitText; }
            private set
            {
                mainWaitText = value;
                NotifyPropertyChanged(mainWaitTextArgs);
            }
        }


        /// <summary>
        /// MainErrorMessage
        /// </summary>
        static PropertyChangedEventArgs mainErrorMessageArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.MainErrorMessage);


        public string MainErrorMessage
        {
            get { return mainErrorMessage; }
            private set
            {
                mainErrorMessage = value;
                NotifyPropertyChanged(mainErrorMessageArgs);
            }
        }


        /// <summary>
        /// DrawerAsyncState
        /// </summary>
        static PropertyChangedEventArgs drawerAsyncStateArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.DrawerAsyncState);

        public AsyncType DrawerAsyncState
        {
            get { return drawerAsyncState; }
            private set
            {
                drawerAsyncState = value;
                NotifyPropertyChanged(drawerAsyncStateArgs);
            }
        }



        /// <summary>
        /// DrawerWaitText
        /// </summary>
        static PropertyChangedEventArgs drawerWaitTextArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.DrawerWaitText);


        public string DrawerWaitText
        {
            get { return drawerWaitText; }
            private set
            {
                drawerWaitText = value;
                NotifyPropertyChanged(drawerWaitTextArgs);
            }
        }


        /// <summary>
        /// DrawerErrorMessage
        /// </summary>
        static PropertyChangedEventArgs drawerErrorMessageArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.DrawerErrorMessage);


        public string DrawerErrorMessage
        {
            get { return drawerErrorMessage; }
            private set
            {
                drawerErrorMessage = value;
                NotifyPropertyChanged(drawerErrorMessageArgs);
            }
        }


        /// <summary>
        /// GraphLayout
        /// </summary>
        static PropertyChangedEventArgs graphLayoutArgs =
            ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.GraphLayout);
        


        public PocGraphLayout GraphLayout
        {
            get { return graphLayout; }
        }
        #endregion

        #region Command Handlers
        private void ExecuteHelpCommand(Object parameter)
        {
            try
            {
                uiVisualizerService.ShowDialog("HelpPopup", null);
            }
            catch(Exception ex)
            {
                messageBoxService.ShowError(ex.InnerException.Message);
            }
        }


        #region CollapseVertexRegionsCommand
        private void ExecuteCollapseVertexRegionsCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                switch (parameter.ToString())
                {
                    case "All":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseAll = !vertex.CollapseAll;
                        }
                        break;
                    case "Constructors":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseConstructors = !vertex.CollapseConstructors;
                        }
                        break;
                    case "Fields":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseFields = !vertex.CollapseFields;
                        }
                        break;
                    case "Properties":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseProperties = !vertex.CollapseProperties;
                        }
                        break;
                    case "Methods":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseMethods = !vertex.CollapseMethods;
                        }
                        break;
                    case "Events":
                        foreach (PocVertex vertex in graph.Vertices)
                        {
                            vertex.CollapseEvents = !vertex.CollapseEvents;
                        }
                        break;
                }


                GraphLayout.Relayout();
            }
            finally
            {
                isGenerallyBusy = false;
            }   

        }
        #endregion

        #region ShowSpecificNotAssociatedVertexCommand
        private void ExecuteShowSpecificNotAssociatedVertexCommand(Object parameter)
        {
            uiVisualizerService.ShowDialog("NonAssociatedVertexPopup", this.SelectedNotAssociatedVertex);

        }
        #endregion

        #region CommenceDrawingCommand
        private bool CanExecuteCommenceDrawingCommand(Object parameter)
        {
            if (assemblyManipulationService == null || assemblyManipulationService.TreeValues == null)
                return false;

            if (!assemblyManipulationService.SelectedTreeValues.Any())
                return false;

            return NotBusyCheck(parameter);
        }

        private void ExecuteCommenceDrawingCommand(Object parameter)
        {
            try
            {

                if (assemblyManipulationService.SelectedTreeValues.Count > SettingsViewModel.Instance.MaximumNumberOfClassesToAllowOnDiagram)
                {
                    messageBoxService.ShowError("Can not show that many classes, it exceeds the MaximumNumberOfClassesToAllowOnDiagram setting\r\n" +
                                                "Which you may change, but be advised this may cause the diagram drawing/layout time to be unacceptable");

                    return;
                }


                DrawerAsyncState = AsyncType.Busy;
                isGenerallyBusy = true;
                hasActiveGraph = false;
                ApplicationHelper.DoEvents();

                Task<GraphResults> task =
                    assemblyManipulationService.CreateGraph();

                int timeout = SettingsViewModel.Instance.GraphDrawingTimeOutInSeconds * 1000;

                bool finishedOk = task.Wait(timeout); // wait 20 seconds before timing out

                if (finishedOk)
                {
                    AddItemsToGraph(task.Result);

                    graphPrintableWindow.ZoomToFit();

                    //TODO Need to also show the non connected ones in a ComboBox
                    //which will launch the popup
                    hasActiveGraph = true;

                }
                else
                {
                    messageBoxService.ShowError(String.Format(
                        "The generating of the class diagram took longer than {0} seconds, maybe try increase this setting and try again",
                        SettingsViewModel.Instance.GraphDrawingTimeOutInSeconds));
                }
            }
            catch (AggregateException AggEx)
            {
                messageBoxService.ShowError(AggEx.InnerException.Message);
            }
            catch (Exception ex)
            {
                messageBoxService.ShowError("Class diagram could not be created\r\n" + ex.Message);
            }
            finally
            {
                DrawerAsyncState = AsyncType.Content;
                ApplicationHelper.DoEvents();
                isGenerallyBusy = false;
                if (graph != null)
                {
                    if (graph.VertexCount == 0)
                        messageBoxService.ShowInformation("Could not find any connected classes in your chosen classes\r\n\r\n" +
                            "So please examine the class definitions using the 'Not Associated Items' drop down in the top right of this application");
                }
            }
        }
        #endregion

        #region ShowSettingsWindowCommand
        private void ExecuteShowSettingsWindowCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                bool? result = uiVisualizerService.ShowDialog("SettingsWindowPopup", SettingsViewModel.Instance);
                if (result.HasValue && result.Value)
                {
                    GraphLayout.Relayout();
                }
            }
            finally
            {
                isGenerallyBusy = false;
            }
            
        }
        #endregion

        #region SaveSettingsAsXmlCommand
        private void ExecuteSaveSettingsAsXmlCommand(Object parameter)
        {
            SettingsViewModel.Instance.SaveSettingsAsXmlCommand.Execute(parameter);
        }
        #endregion

        #region ReLayoutCommand
        private void ExecuteReLayoutCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                GraphLayout.Relayout();
            }
            finally
            {
                isGenerallyBusy = false;
            }
            
        }
        #endregion

        #region AboutCommand
        private void ExecuteAboutCommand(Object parameter)
        {
            uiVisualizerService.ShowDialog("AboutWindowPopup", new AboutViewModel());
        }
        #endregion

        #region SaveCommand
        private void ExecuteSaveCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                saveFileService.InitialDirectory = @"c:\temp";
                saveFileService.OverwritePrompt = true;
                saveFileService.Filter = "*.PNG | PNG Files";

                bool? result = saveFileService.ShowDialog(null);
                String filePath = saveFileService.FileName;

                if (!filePath.ToLower().EndsWith(".png"))
                    filePath += ".png";

                if (result.HasValue && result.Value)
                {
                    FrameworkElement visual = graphPrintableWindow.GetGraphToPrint;
                    Double currentZoom = graphPrintableWindow.Zoom;
                    graphPrintableWindow.Zoom = 1.0;

                    if (savePNGService.Save(filePath, visual))
                    {
                        messageBoxService.ShowInformation(string.Format("Sucessfully saved file to {0}", filePath));
                    }
                    else
                    {
                        messageBoxService.ShowError(string.Format("Error saving file {0}", filePath));
                    }

                    graphPrintableWindow.Zoom = currentZoom;
                }
            }
            finally
            {
                isGenerallyBusy = false;
            }
            


        }
        #endregion


        #region OpenFileCommand
        private void ExecuteOpenFileCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                openFileService.InitialDirectory = @"C:\";
                openFileService.Filter = "Assemblies (*.dll)|*.dll|Executables (*.exe)|*.exe";
                openFileService.FileName = "";

                bool? result = openFileService.ShowDialog(null);
                if (result.HasValue && result.Value)
                {
                    if (!openFileService.FileName.Equals(string.Empty))
                    {
                        ProcessAssemblyFile(openFileService.FileName);
                    }
                }
            }
            catch (AggregateException AggEx)
            {
                MainAsyncState = AsyncType.Content;
                HasActiveAssembly = false;
                messageBoxService.ShowError(AggEx.InnerException.Message);
            }
            catch (Exception ex)
            {
                MainAsyncState = AsyncType.Content;
                HasActiveAssembly = false;
                messageBoxService.ShowError(ex.Message);

            }
            finally
            {
                isGenerallyBusy = false;
                MainAsyncState = AsyncType.Content;
                ApplicationHelper.DoEvents();
            }
        }
        #endregion

        #region OpenFileDragCommand
        private void ExecuteOpenFileDragCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                if (parameter != null)
                {
                    ProcessAssemblyFile(parameter.ToString());
                }
            }
            catch (AggregateException AggEx)
            {
                MainAsyncState = AsyncType.Content;
                HasActiveAssembly = false;
                messageBoxService.ShowError(AggEx.InnerException.Message);
            }
            catch (Exception ex)
            {
                MainAsyncState = AsyncType.Content;
                HasActiveAssembly = false;
                messageBoxService.ShowError(ex.Message);

            }
            finally
            {
                isGenerallyBusy = false;
                MainAsyncState = AsyncType.Content;
                ApplicationHelper.DoEvents();
            }
        }
        #endregion
     
        #region PrintFileCommand
        private void ExecutePrintFileCommand(Object parameter)
        {
            isGenerallyBusy = true;
            try
            {
                Exception printingEx = printPNGFileService.Print(graphLayout);
                if (printingEx != null)
                {
                    messageBoxService.ShowError("There was an error printing graph");
                }
            }
            finally
            {
                isGenerallyBusy = false;
            }
        }
        #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