Affichage des articles dont le libellé est C#. Afficher tous les articles
Affichage des articles dont le libellé est C#. Afficher tous les articles

lundi 12 décembre 2011

Font pour Code Barre

Si vous avez besoin d'imprimer des codes barres, vous pouvez toujours jeter un oeil sur le site Free Linear Barcode Image Online Generator (IDAutomation).
Outre la mise a disposition d'un outil en ligne, le site de IDAutomation propose également un zone de téléchargement permettant de se procurer les fonts nécessaire.

J'ai aussi noté la mise à disposition de la font UPC, EAN & GTIN qui devrait permettre de générer des codes barres EAN (13 positions). Cependant, je ne suis pas arrivé à produire de code barre lisible par un scanner.

Par contre, The Code Project publie l'article "Creating EAN-13 BarCode" sur la génération des codes barres en C#.
Malheureusement rien d'utilisable directement en PowerShell (snif)

Ressources:

mardi 25 janvier 2011

ADO.NET - Aide mémoire - 4/4 : Tutoriel

Le présent article fait parti d'une suite logique de 4 articles: "ADO.NET - Aide mémoire - 1/4 : Execution, Parametres, StoredProc, DataReader",  "ADO.NET - Aide mémoire - 2/4 : DataSet", "ADO.NET - Aide mémoire - 2/4 : DataView" et "ADO.NET - Aide mémoire - 4/4 : Tutoriel"

eTutorial.org publie un Tuto Ado.Net plus que largement complet, le genre de référence que l'on place dans les perles du Net. Ce tutoriel prend sa source dans les excellents ouvrages de O'Reilly. Il faudra cependant composer avec l'affichage des publicités.

Admirez donc la magnifique table des matières que j'ai repris ci-dessous.
Si vous vous posez une question, tentez d'abord de trouver une réponse dans le tutoriel de eTutorial.

Preface
Audience

Contents of This Book
What's on the CD-ROM
Conventions Used in This Book
Comments and Questions
Acknowledgments

Part I: ADO.NET Tutorial

Chapter 1. Introduction


1.1 ADO.NET Data Providers
1.2 Connected and Disconnected Data

Chapter 2. .NET Data Providers

2.1 Data Providers
2.2 Selecting a Data Provider
2.3 Creating a Custom Data Provider

Chapter 3. Connections

3.1 Connection Object Overview
3.2 The Connection String
3.3 Opening and Closing Connections
3.4 Connection Pooling

Chapter 4. Commands


4.1 Command Object Overview
4.2 Creating and Executing a Command
4.3 Parameter Object Overview
4.4 Parameterized Commands
4.5 Commands with Stored Procedures
4.6 Commands and Data Definition Language (DDL)

Chapter 5. DataReaders

5.1 DataReader Object Overview
5.2 Performing a Query with a DataReader
5.3 Stored Procedures with the DataReader
5.4 DataReaders and Schema Information

Chapter 6. DataSets

6.1 Creating an Untyped DataSet
6.2 Working with Tables in the DataSet
6.3 Adding and Removing Relations
6.4 Adding Custom Information
6.5 Cloning the Schema
6.6 Copying the DataSet
6.7 Merging Two DataSets
6.8 Removing All Data
6.9 Resetting the DataSet
6.10 Committing and Discarding Changes

Chapter 7. DataTables

7.1 Creating a DataTable
7.2 Working with Columns
7.3 Constraints
7.4 Primary Key
7.5 Rows
7.6 Loading Data
7.7 Committing and Discarding Changes
7.8 Cloning the Schema of the Table
7.9 Copying the Table
7.10 Selecting a Subset of Rows
7.11 Performing Aggregate Calculations
7.12 Removing All Data
7.13 Resetting the Table
7.14 Identifying Errors in the Table
7.15 DataTable Events

Chapter 8. DataColumns

8.1 Creating DataColumns
8.2 Creating AutoIncrement Columns
8.3 Creating Expression Columns
8.4 Handling Null Values
8.5 Mapping .NET Data Provider Types to .NET Framework Types

Chapter 9. DataRows

9.1 Creating a DataRow
9.2 Updating Rows
9.3 Deleting Rows
9.4 Using Row State Information
9.5 Using Row Version Information
9.6 Accepting or Rejecting Changes to Rows
9.7 Navigating Parent and Child Rows
9.8 Using Row Error Information

Chapter 10. Constraints

10.1 Constraint Object Overview
10.2 The UniqueConstraint
10.3 The ForeignKeyConstraint

Chapter 11. DataRelations
11.1 DataRelation Object Overview
11.2 Navigating Relational Data

Chapter 12. DataViews and Data Binding


12.1 The DataView and DataViewManager
12.2 Sorting and Filtering
12.3 Accessing Data Through a DataView
12.4 Windows Data Binding
12.5 ASP.NET Data Binding

Chapter 13. Strongly Typed DataSets

13.1 Creating a Strongly Typed DataSet
13.2 Adding a Row
13.3 Editing a Row
13.4 Finding a Row
13.5 Null Data
13.6 Navigating Hierarchical Data
13.7 Annotations

Chapter 14. DataAdapters


14.1 Creating DataAdapter Object
14.2 Retrieving Data from the Data Source
14.3 Retrieving Schema Information from the Data Source
14.4 Updating the Data Source
14.5 Mapping Tables and Columns
14.6 AcceptChangesDuringFill
14.7 ContinueUpdateOnError
14.8 DataAdapter Events

Chapter 15. Updating the Data Source

15.1 SqlCommandBuilder Class Overview
15.2 Updating a Data Source Using Command Builder
15.3 Updating a Data Source Using Custom Logic
15.4 Refreshing Data After Updating
15.5 Retrieving Updated Values from the Data Source
15.6 Updating Data in Related Tables
15.7 Handling Concurrency Issues
15.8 Optimization

Chapter 16. Transactions


16.1 Manual Transactions
16.2 Isolation Levels
16.3 Savepoints
16.4 Nested Transactions
16.5 Transactions Using a DataAdapter
16.6 Automatic Transactions

Chapter 17. XML and the DataSet

17.1 DataSet XML Methods
17.2 Shaping DataSet XML
17.3 Other .NET XML Classes
17.4 XmlDataDocument Object Overview
17.5 Using the Data Objects to Edit XML
17.6 SQL Server 2000 XML

Part II: ADO.NET Core Classes

Chapter 18. The Connection Class

18.1 Comments/Troubleshooting
18.2 Properties Reference
18.3 Methods Reference
18.4 Events Reference

Chapter 19. The Command Class


19.1 Comments/Troubleshooting
19.2 Properties Reference
19.3 Collections Reference
19.4 Methods Reference

Chapter 20. The Parameter Class

20.1 Comments/Troubleshooting
20.2 Properties Reference

Chapter 21. The DataReader Class

21.1 Comments/Troubleshooting
21.2 Properties Reference
21.3 Methods Reference

Chapter 22. The DataSet Class

22.1 Comments/Troubleshooting
22.2 Properties Reference
22.3 Collections Reference
22.4 Methods Reference
22.5 Events Reference
 

Chapter 23. The DataTable Class

23.1 Comments/Troubleshooting
23.2 Properties Reference
23.3 Collections Reference
23.4 Methods Reference
23.5 Events Reference

Chapter 24. The DataColumn Class

24.1 Comments/Troubleshooting
24.2 Properties Reference
24.3 Collections Reference

Chapter 25. The DataRow Class

25.1 Comments/Troubleshooting
25.2 Properties Reference
25.3 Collections Reference
25.4 Methods Reference

Chapter 26. The Constraint Class


26.1 Comments/Troubleshooting
26.2 Properties Reference
26.3 Collections Reference

Chapter 27. The DataRelation Class

27.1 Comments/Troubleshooting
27.2 Properties Reference
27.3 Collections Reference

Chapter 28. The DataView Class

28.1 Comments/Troubleshooting
28.2 Properties Reference
28.3 Methods Reference
28.4 Events Reference

Chapter 29. The DataAdapter Class


29.1 Comments/Troubleshooting
29.2 Properties Reference
29.3 Collections Reference
29.4 Methods Reference
29.5 Events Reference

Chapter 30. The CommandBuilder Class

30.1 Comments/Troubleshooting
30.2 Properties Reference
30.3 Methods Reference

Chapter 31. The Transaction Class

31.1 Comments/Troubleshooting
31.2 Properties Reference
31.3 Methods Reference

Part III: API Quick Reference

Chapter 32. How to Use This Quick Reference

32.1 Finding a Quick-Reference Entry
32.2 Reading a Quick-Reference Entry

Chapter 33. Converting from C# to VB Syntax

33.1 General Considerations
33.2 Classes
33.3 Structures
33.4 Interfaces
33.5 Class, Structure, and Interface Members
33.6 Delegates
33.7 Enumerations

Chapter 34. The System.Data Namespace


Chapter 35. The System.Data.Common Namespace

Chapter 36. The System.Data.SqlClient Namespace

Chapter 37. The System.Data.OleDb Namespace

Chapter 38. The System.Data.SqlTypes Namespace

Part IV: Appendixes

Appendix A. ADO.NET Providers

A.1 The SQL Server Provider
A.2 The OLE DB Provider
A.3 The ODBC .NET Provider
A.4 The Oracle .NET Provider
A.5 The ODP.NET Provider

Appendix B. ADO.NET XML Extensions

B.1 codegen Namespace
B.2 msdata Namespace
B.3 diffgr Namespace

Appendix C. Microsoft Data Engine (MSDE)

C.1 Installing MSDE
C.2 MSDE Essentials
C.3 Adding the Northwind Data
C.4 Migrating MSDE to SQL Server

Type, Method, Property, and Field Index

ADO.NET - Aide mémoire - 3/4 : DataView

Le présent article fait parti d'une suite logique de 4 articles: "ADO.NET - Aide mémoire - 1/4 : Execution, Parametres, StoredProc, DataReader",  "ADO.NET - Aide mémoire - 2/4 : DataSet", "ADO.NET - Aide mémoire - 2/4 : DataView" et "ADO.NET - Aide mémoire - 4/4 : Tutoriel"

DataView
Un DataView permet de créer différentes vues des données stockées dans une DataTable, une aptitude qui est souvent utilisée dans les opérations de data-binding. En utilisant un DataView, il est possible d'exposer les données d'une table avec un ordre de tri différent, il est également possible de filtrer les données par état (avant/après modification) ou encore filtrer sur base d'une expression (équivalent d'une clause "where" en sql).

Un DataView fournit une vue dynamique des données stockées dans la DataTable sous-jacente: le contenu, l'ordre de tri, et aspects relationnels reflètent les modifications tels qu'elles se présentent. Cette caractéristique diffère de méthode "Select" de la DataTable qui produit un tableau de DataRow depuis la DataTable sur base d'un filtre et/ou tri particulier: ce contenu reproduit les modifications dans la table sous-jacente, mais les aspects relationnels et l'ordonnancement restent statiques. Les capacités dynamiques du DataView en font un candidat idéal pour l'utilisation de data-binding.

Un DataView procure une vue dynamique d'un ensemble de données unique, un peu comme les vues d'une base de données, auquel il est possible d'appliquer différents ordres de tri et critères de sélection. Au contraire d'un vue en DB, un DataView ne peut pas être manipulé comme une table et ne peut pas fournir une jointure entre plusieurs tables. Vous ne pouvez pas non plus exclure des colonnes qui existent dans la table source, ni ajouter des colonnes tels que des colonnes calculées qui ne se trouveraient pas dans la table source.

Vous pouvez utiliser une DataViewManager pour gérer la configuration des vues pour toutes les tables d'un DataSet. Le DataViewManager procure la possibilité de gérer la configuration du DefaultView de chaque table de façon pratique. Lorsqu'un contrôle est lié (bind) à plus d'une table d'un DataSet, faire le binding vers un DataViewManager est alors un choix idéal.

Voici quelques références complémentaires:
  • Page reprenant les Articles de Microsoft concernant les DataViews, articles énumérés et décrits ci-dessous.
  • Creating a DataView (ADO.NET)
    Décrit comment créer un DataView pour un DataTable
  • Sorting and Filtering Data (ADO.NET)
    Montre comment initialiser les propriétés d'un DataView pour retourner un sous-ensemble des enregistrements de données qui satisfont les critères de filtrage, ou qui retourne les données dans un ordre particulier.
  • DataRows and DataRowViews (ADO.NET)
    Explique comment accéder aux données exposées par le DataView.
  • Finding Rows (ADO.NET)
    Explique comment retrouver un enregistrement particulier dans un DataView
  • ChildViews and Relations (ADO.NET)
    Décrit comment créer une vue à partir d'une relation master-detail (parent-child) en utilisant un DataView.
  • Modifying DataViews (ADO.NET)
    Explique comment modifier les données de la DataTable sous-jacente à partir d'un DataView, incluant également l'activation/désactivation des mise-à-jours (updates).
  • Handling DataView Events (ADO.NET)
    Explique comment utiliser l'évènement ListChanged pour recevoir une notification lorsque le contenu ou l'ordre de tri d'un DataView est en cours de modification.
  • Managing DataViews (ADO.NET)
    Explique comment utiliser un DataViewManager pour gérer les configurations des DataView pour chaque table du DataSet.
Exemple de relation Master-Detail et ChildView
Directement issu d'un des articles MSDN, voici un petit morceau de code qui montre comment définir une relation Master-Detail dans un DataSet mais aussi comment l'exploiter pour créer un ChildView (CreateChildView)
DataTable catTable = catDS.Tables["Categories"];
DataTable prodTable = catDS.Tables["Products"];

// Create a relation between the Categories and Products tables.
DataRelation relation = catDS.Relations.Add("CatProdRel", 
    catTable.Columns["CategoryID"],
    prodTable.Columns["CategoryID"]);

// Create DataViews for the Categories and Products tables.
DataView catView = new DataView(catTable, "", "CategoryName", 
  DataViewRowState.CurrentRows);
DataView prodView;

// Iterate through the Categories table.
foreach (DataRowView catDRV in catView)
{
  Console.WriteLine(catDRV["CategoryName"]);

  // Create a DataView of the child product records.
  prodView = catDRV.CreateChildView(relation);
  prodView.Sort = "ProductName";

  foreach (DataRowView prodDRV in prodView)
    Console.WriteLine("\t" + prodDRV["ProductName"]);
}

Autre Exemple issus de MSDN - DataViewManager, chargement Dataset.

Cet autre exemple de MSDN (légèrement adapté) est censé démontrer l'usage du DataViewManager.
Personnellement, je crois qu'il y a moyen de mieux faire.
Par contre, ce qui est beaucoup intéressant, c'est le chargement de plusieurs tables et de leurs relations dans un seul dataset. Tables de la DB AdventureWorks.

Fichier source: AdoNet_DataViewManager.cs.

using System;using System.Collections.Generic;using System.Data;using System.Data.SqlClient;using System.Windows.Forms;
public class MyClass
{
    public static void RunSnippet()
    {
        // Adventure Works creation script and database CAN BE DOWNLOADED from CodePlex
        //    http://sqlserversamples.codeplex.com/
        //
        SqlConnection connection = new SqlConnection( "Data Source=localhost;Initial Catalog=AdventureWorksLT2008;Integrated Security=SSPI;" );
        // Assumes connection is a valid SqlConnection to Northwind.
        // Create a Connection, DataAdapters, and a DataSet.
        SqlDataAdapter custDA = new SqlDataAdapter(
        "SELECT CustomerID, CompanyName FROM SalesLT.Customer", connection);
        SqlDataAdapter orderDA = new SqlDataAdapter(
        "SELECT SalesOrderID, CustomerID FROM SalesLT.SalesOrderHeader", connection);
        SqlDataAdapter ordDetDA = new SqlDataAdapter(
        "SELECT SalesOrderID, ProductID, OrderQty FROM SalesLT.SalesOrderDetail", connection);
        
        DataSet custDS = new DataSet();
        
        // Open the Connection.
        connection.Open();

        // Fill the DataSet with schema information and data.
        custDA.MissingSchemaAction = MissingSchemaAction.AddWithKey;
        orderDA.MissingSchemaAction = MissingSchemaAction.AddWithKey;
        ordDetDA.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    
        custDA.Fill(custDS, "Customers");
        orderDA.Fill(custDS, "Orders");
        ordDetDA.Fill(custDS, "OrderDetails");
    
        // Close the Connection.
        connection.Close();
    
        // Create relationships.
        custDS.Relations.Add("CustomerOrders",
            custDS.Tables["Customers"].Columns["CustomerID"],
            custDS.Tables["Orders"].Columns["CustomerID"]);
    
        custDS.Relations.Add("OrderDetails",
            custDS.Tables["Orders"].Columns["SalesOrderID"],
            custDS.Tables["OrderDetails"].Columns["SalesOrderID"]);
    
        // Create default DataView settings.
        DataViewManager viewManager = new DataViewManager(custDS);
        
        foreach (DataViewSetting viewSetting in viewManager.DataViewSettings)
        viewSetting.ApplyDefaultSort = true;
        
        viewManager.DataViewSettings["Customers"].Sort = "CompanyName";
        
        // Bind to a DataGrid.
        //   Note: N'affichera absolument rien sous snippet compiler
        System.Windows.Forms.DataGrid grid = new System.Windows.Forms.DataGrid();
        grid.SetDataBinding(viewManager, "Customers");

    }
    
    #region Helper methods
    ...
    #endregion
}

mercredi 19 janvier 2011

ADO.NET - Aide mémoire - 2/4 : DataSet

Le présent article fait parti d'une suite logique de 4 articles: "ADO.NET - Aide mémoire - 1/4 : Execution, Parametres, StoredProc, DataReader",  "ADO.NET - Aide mémoire - 2/4 : DataSet", "ADO.NET - Aide mémoire - 2/4 : DataView" et "ADO.NET - Aide mémoire - 4/4 : Tutoriel"

Cet aide mémoire se focalisera plutôt sur l'utilisation générale des DataSets.
On y découvrira:
  • L'utilisation du SqlCommand, SqlDataAdapter et DataSet afin de lire les données (via DataTable et DataRow).
  • Les méthodes de lectures via:
    • DataTable.Rows : collection of DataRow
    • DataTable.Select : Permettant le filtrage et le tri d'un sous ensemble des enregistrements. Aussi une collection de DataRow.
    • DataTable.DefaultView : DataView permettant également le filtrage et le tri d'un sous ensemble. DataView contenant une collection de DataRowView.
  • Mise à jour des données d'une DataTable dans un Dataset (et sauvegarde en DB).
eTutorial.org

    En référence, je mentionnerais également:
    DataViews
    Les DataViews offrent de nombreuses possibilités avancées qui va bien au-delà de ce qui est présenté dans l'exemple de cet article.
    L'article "ADO.NET - Aide mémoire - 3/4 : DataViews" reprend une description et quelques références Microsoft.

    Exemple 
    Voici l'exemple dont seulement les parties les plus pertinentes sont reprises dans l'article (voir fichier source pour la totalité du code).
    Cet exemple utilise Snippet Compiler comme d'habitude.
    Fichier Source: AdoNet_DataSet.cs
    using System;using System.Collections.Generic;using System.Data;using System.Data.SqlClient;
    public class MyClass
    {
        public static void RunSnippet()
        {
            DropTestDatabase(); // Drop the test database if any exists
            CreateTestDatabase();
            
            DisplaySortedDataset();
            UpdateDataSet();
        }
        
        public static void DisplaySortedDataset () {
            WL( "=== DATASET read/filter/sort operation =================" );
            SqlConnection sqlConn = GetTestConnection();
            sqlConn.Open();
            
            SqlCommand sqlCmd = new SqlCommand( "select * from usr", sqlConn );
            SqlDataAdapter sqlAd = new SqlDataAdapter( sqlCmd );
            DataSet ds = new DataSet();
            sqlAd.Fill( ds, "dataTableName_User" ); // give an explicit name for the sample
            // Display all DataTable.TableName in the DataSet
            foreach( DataTable tbl in ds.Tables )
                WL( String.Format( "  DataSet.Tables[i].TableName="+ tbl.TableName ));
    
            // === DataTable Rows =====================
            // Display the Data
            WL( "Display data (Via Rows)" );
            foreach( DataRow dr in ds.Tables["dataTableName_User"].Rows )
                WL( String.Format( "   User {0} (id:{1})", dr["usrname"].ToString(), dr["uid"].ToString() ));
            
            // === DataTable Select ===================
            // Sort it + redisplay it
            WL( "Display sorted data (via Select)" );
            DataRow[] sorted = ds.Tables["dataTablename_User"].Select("usrname like '%'", "uid desc");
            foreach( DataRow dr in sorted )
                WL( String.Format( "   User {0} (id:{1})", dr["usrname"].ToString(), dr["uid"].ToString() ));
            // SubSelect + sort
            WL( "Display filtered data (via Select)" );
            DataRow[] filtered = ds.Tables["dataTablename_User"].Select("usrname like '%a%'", "usrname asc");
            foreach( DataRow dr in filtered )
                WL( String.Format( "   User {0} (id:{1})", dr["usrname"].ToString(), dr["uid"].ToString() ));
    
            // === DataView ===========================
            // Sorted and filtered with DataView
            //   DataView may be very useful to work with visual componant
            //   More information on DataViews on Microsoft site @
            //     http://msdn.microsoft.com/en-us/library/fdcwwhez.aspx
            //
            WL( "Display filtered data (via DataView)" );
            DataView dv = ds.Tables["dataTableName_User"].DefaultView;
            dv.RowFilter = "usrname like '%a%'";
            dv.Sort = "uid desc";
            // In a DataView, the Rows are available via DataRowView        
            foreach( DataRowView drv in dv )
                WL( String.Format( "   User {0} (id:{1})", drv["usrname"].ToString(), drv["uid"].ToString() ));
            
            sqlConn.Close();
        }
        
        public static void UpdateDataSet () {
            WL( "=== UPDATE operation ============================" );
            SqlConnection sqlConn = GetTestConnection();
            sqlConn.Open();
            // Read the data
            SqlDataAdapter ad = new SqlDataAdapter( "Select * from USR order by usrName desc", sqlConn );
            // Create a CommandBuilder, this will automatically generate the appropriate commands
            // and set the various properties of the SqlAdapter.
            SqlCommandBuilder cmdBuilder = new SqlCommandBuilder( ad );
            // Create and fill dataset
            DataSet ds = new DataSet();
            ad.Fill( ds, "USR" );
    
            // Display the Data
            WL( "Display data BEFORE change" );
            foreach( DataRow dr in ds.Tables["USR"].Rows )
                WL( String.Format( "   User {0} (id:{1})", dr["usrname"].ToString(), dr["uid"].ToString() ));
    
            // Update Data
            DataRow[] filtered = ds.Tables["USR"].Select( "usrname like 'anatole'", "uid" );
            if( filtered.Length > 0 ) {
                filtered[0]["usrname"] = "John Doe";
            }
            
            // Add a new row 
            //   Removal is done with DataTable.Rows.RemoveAt(RowIndex) or DataTable.Rows.Remove( DataRowObject )
            DataRow added = ds.Tables["USR"].Rows.Add();
            added["usrname"] = "Florentin Célestin";
            added["uid"] = 120;
            
            // Save the DataSet
            ad.Update(ds, "USR");
            
            // ReRead the DataSet and Display result
            ds.Clear();
            ad.Fill( ds, "USR" );
            // Display the Data
            WL( "Display data AFTER change" );
            foreach( DataRow dr in ds.Tables["USR"].Rows )
                WL( String.Format( "   User {0} (id:{1})", dr["usrname"].ToString(), dr["uid"].ToString() ));
            
            sqlConn.Close();
        }
        
        #region Sample Database helper    // Connect and create the Test Database
        // See the source file for the Full code. 
        #endregion
        
        #region Helper methods    // ...    
        #endregion
    }
    

    dimanche 16 janvier 2011

    ADO.NET - Aide mémoire - 1/4 : Execution, Parametres, StoredProc, DataReader

    Parce que rien ne vaut quelques bons articles didactiques, voici une série de références qui peuvent se montrer bien utiles un jour ou l'autre.

    Le présent article fait parti d'une suite logique de 4 articles: "ADO.NET - Aide mémoire - 1/4 : Execution, Parametres, StoredProc, DataReader",  "ADO.NET - Aide mémoire - 2/4 : DataSet", "ADO.NET - Aide mémoire - 2/4 : DataView" et "ADO.NET - Aide mémoire - 4/4 : Tutoriel"

    Mettre rapidement le pied à l'étrier
    Voici une série d'articles récapitulatifs en provenance de DeveloperFusion.
    Ils reprennent toute la chaine d'objets Ado.Net pour SqlServeur et Access avec les options principales ainsi que des exemples en C# et VB.NET. Un excellent condensé pour aborder les DataReader, les appels aux Stored Procedure, les DataSets, Update/Delete/Insert, etc.
      Quelques articles et sites de références
      • ConnectionStrings.com site incroyable reprenant des centaines de configurations de connections strings pour les différents moteurs DB SqlServeur, Oracle, pervasive, MySql, etc. Un incontournable!
      • Retrieving data using a C# .Net DataReader
        Le principal intérêt de cet article réside dans la récupération d'un dataset depuis une stored procedure avec un DataReader et remplir une ListView avec les données (réclame de passer par une structure intermédiaire).
      • Configuration Settings File for providing application configuration data
        Toujours utile, ce petit article explique comment configurer des paramètre dans le fichier app.config (idéal pour stocker une ConnectionString). 
      • Juste une note pour mentionner qu'il est possible de créer d'une connexion Sql depuis le fichier de config (en utilisant les Entreprise Application Blocks ).
        Il faudra faire un peu de recherche pour compléter ce point.
      Data Access Application Block

      Par le Data Access Application Block (DAAB pour les intimes), Microsoft propose une couche logicielle pour l'accès aux données de façon générique et se défaire du fournisseur d'accès aux bases de données. Ainsi il est supposé un gain de temps et de code pour le programmeur puisqu'il y a moins de lignes à coder.
      Mais dans la réalité, ce n'est pas si simple. La généricité concerne les requêtes de base et les différences entre les fournisseurs de bases de données apparaissent rapidement. Du coup, le code se rallonge puisqu'il faut créer des instructions différentes selon le provider. (source: cet article sur developper.com)

      Cet article n'aborde pas la section "Data Access Application Block" de Microsoft patterns & practice entreprise library. C'est une solution vraiment puissante mais je ne dispose pas d'assez de temps pour m'y attarder.
      En attendant, vous pouvez vous renseigner ici:

      Exemples Ado.Net
      Voici quelques exemples (Snippet Compiler) de code rudimentaire mais démontrant néanmoins différents usages Ado.Net.

      Ce premier exemple s'attarde sur:
      • ExecuteNonQuery - pour des opérations tels que Insert, Delete, modification de schema, etc.
      • ExecuteScalar - pour récupérer une seule valeur de retour (première ligne, première colonne).
      • ExecuteReader - pour obtenir un reader permettant de passer l'ensemble des données en revue.
      • Query paramétrisé
      • Stored Procedure paramétrisée et avec valeur de retour.
      Source file: AdoNet_Samples.cs
        using System;
        using System.Collections.Generic;
        using System.Data;
        using System.Data.SqlClient;
        
        public class MyClass
        {
                public static void RunSnippet()
            {
                // Call to the various demo functions
                ExecuteNonQuery();
                ExecuteScalar();
                ExecuteReader();
                ParametrizedQuery();
                CallStoredProc(); 
            }
            
            /// <summary>
            /// Get a Sql Connection to Master Database.
            /// Always close the connection as soon as possible.
            /// </summary>
            /// <returns></returns>
            public static SqlConnection GetMasterConnection() {
                // using intégrated security to not revealing my SA password :-)
                string ConnString = "Data Source=localhost;Initial Catalog=master;Integrated Security=SSPI;";
                return new SqlConnection( ConnString );
            }
            
            public static SqlConnection GetConnection( string DBName ){
                string ConnString = String.Format("Data Source=localhost;Initial Catalog={0};Integrated Security=SSPI;", DBName );
                return new SqlConnection( ConnString );    
            }
        
            /// <summary>
            /// Demonstrate the EXECUTE Non Query.
            /// use SqlCommand.ExecuteNonQuery only when no result is espected (like DELETE, UPDATE statements)
            /// </summary>
            public static void ExecuteNonQuery(){
                WL( "--- EXECUTE Non Query ---" );
                SqlConnection SqlConn = GetMasterConnection();
                SqlConn.Open();
                WL( "Create database BLABLA" );
                SqlCommand SqlCmd = new SqlCommand( "Create database BlaBla", SqlConn );
                SqlCmd.ExecuteNonQuery();
                // see method r.RecordsAffected when used with DELETE or UPDATE statement
                
                WL( "Drop database BLABLA" );
                SqlCommand SqlCmd2 = new SqlCommand( "Drop database BlaBla", SqlConn );
                SqlCmd2.ExecuteNonQuery();
                
                SqlConn.Close(); // Close connection Asap
            }
            
            /// <summary>
            /// Demonstrate the SCALAR READING.
            /// use SqlCommand.ExecuteScalar (scalar reading) to retreive the result of the first row, first columns.
            /// </summary>
            public static void ExecuteScalar(){
                WL( "--- SCALAR READING ---" );
                SqlConnection SqlConn = GetMasterConnection();
                SqlConn.Open();
                // Read number of SqlUsers
                SqlCommand SqlCmd = new SqlCommand( "select count(*) from sys.sysusers", SqlConn );
                int iUserCount = (int)SqlCmd.ExecuteScalar();
                // see method r.HasRows to see if there is data
                WL( String.Format( "There are {0} sql users in the database", iUserCount ));
                
                SqlConn.Close(); // Close connection ASAP    
            }
            
            /// <summary>
            /// Demonstrate the DATASET READER
            /// use SqlCommand.ExecuteReader when needed to retreive a Dataset
            /// </summary>
            public static void ExecuteReader(){
                WL( "--- EXECUTE READER ---" );
                SqlConnection SqlConn = GetMasterConnection();
                SqlConn.Open();
                // Return list of user (and a null row).
                SqlCommand SqlCmd = new SqlCommand( "SELECT UID, NAME FROM SYS.SYSUSERS union select null, null", SqlConn );
                SqlDataReader r = SqlCmd.ExecuteReader();
                // see method r.HasRows to see if there is data
                WL( "Reading SqlUser list" );
                while( r.Read() ){
                    // Test nullability
                    if( r["uid"] == DBNull.Value )
                        continue;
                    // Cast SmallInt Sql datatype to Short C# type
                    WL( String.Format( "   {0} ({1})", (string)r["name"],  (short)r["uid"]  ) );
                }
                r.Close(); // close DataSet Asap
                SqlConn.Close(); // Close Connection Asap
            }
            
            /// <summary>
            /// Demonstrate how to add parameters to a query command.
            /// </summary>
            public static void ParametrizedQuery() {
                WL( "--- Parametrized Query ---" );
                SqlConnection SqlConn = GetMasterConnection();
                SqlConn.Open();
                SqlCommand SqlCmd = new SqlCommand( "SELECT id, name from sys.sysobjects where xtype = @TypeToFilter", SqlConn );
                SqlCmd.Parameters.Add( new SqlParameter( "TypeToFilter", SqlDbType.NVarChar, 5 ) );
                SqlCmd.Parameters["TypeToFilter"].Value="U"; // @ not required for parameter name
                SqlDataReader r = SqlCmd.ExecuteReader();
                while( r.Read() )
                    WL( String.Format("id:{0}, name:{1}", r["id"], r["name"] ) );
                r.Close();
                
                SqlCmd.Parameters["TypeToFilter"].Value="PC";
                r = SqlCmd.ExecuteReader();
                while( r.Read() )
                    WL( String.Format("id:{0}, name:{1}", r["id"], r["name"] ) );
                r.Close();
                
                SqlConn.Close();
            }
            
            /// <summary>
            /// Demonstrate how to call a stored procedure and retreive the return value parameter
            /// </summary>
            public static void CallStoredProc(){        
                WL( "--- STORED PROCEDURE call ---" );
                // drop existing database if exists
                SqlConnection SqlConnDropDb = GetMasterConnection();
                try {
                    // Drop database
                    WL( "X) dropping test database" );
                    SqlConnDropDb.Open();
                    SqlCommand SqlDrop = new SqlCommand( "Drop database StoredProcTest", SqlConnDropDb );
                    SqlDrop.ExecuteNonQuery();
                    WL( "Previous StoredProcTest database droped" );
                }
                catch {
                    WL( "No previous StoredProcTest database" );
                }
                finally {
                    SqlConnDropDb.Close();
                }
        
                
                WL( "1) Create database and Stored proc" );
                SqlConnection SqlConn = GetMasterConnection();
                SqlConn.Open();
                SqlCommand SqlCmd = new SqlCommand( "Create database StoredProcTest", SqlConn );
                SqlCmd.ExecuteNonQuery();
                SqlConn.Close();
                
                SqlConnection SqlConnProc = GetConnection( "StoredProcTest" );
                SqlConnProc.Open();
                SqlCommand SqlCmdProc = new SqlCommand( "CREATE TABLE USR ( uid int not null, usrname varchar(20) not null)", SqlConnProc );
                SqlCmdProc.ExecuteNonQuery();
                SqlCmdProc.CommandText = "INSERT INTO USR VALUES ( 1, 'Dominique' )";
                SqlCmdProc.ExecuteNonQuery();
                SqlCmdProc.CommandText = "INSERT INTO USR VALUES ( 2, 'Francoise' )";
                SqlCmdProc.ExecuteNonQuery();
                // Create the stored procedure de test HAVING a RETURN PARAM
                SqlCmdProc.CommandText = 
        @"create procedure UpdateUsr( @Uid as int, @NewName as varchar(20), @Result Varchar(50) OUTPUT )
        as
          Update USR 
          Set UsrName = @NewName 
          where Uid = @Uid
          
          if( @@ROWCOUNT > 0 )
            SET @Result = 'c est fait'
          ELSE
            SET @Result = 'connait pas!' ";
                SqlCmdProc.ExecuteNonQuery();
                SqlConnProc.Close();
                
                // Tester la stored procedure 
                WL( "2) Dataset before stored Proc Call" );
                SqlConnProc.Open();
                SqlCommand SqlRead = new SqlCommand( "select * from USR order by uid", SqlConnProc );
                SqlDataReader r = SqlRead.ExecuteReader();
                while( r.Read() ) 
                    WL( String.Format( "   UID: {0} - UserName: {1}", r["UID"], r["UsrName"] ) );
                r.Close();
                
                // Calling the stored procedure
                WL( "3) Call the stored Proc" );
                WL( "Update existing UID 2" );
                SqlCommand storedProcCall = new SqlCommand( "UpdateUsr", SqlConnProc );
                storedProcCall.CommandType = CommandType.StoredProcedure;
                storedProcCall.Parameters.AddWithValue( "@uid", 2 );
                storedProcCall.Parameters.AddWithValue( "@newname", "John Doe" );
                SqlParameter resultParam = new SqlParameter( "@result", SqlDbType.VarChar, 8000 );
                resultParam.Direction = ParameterDirection.Output;
                storedProcCall.Parameters.Add( resultParam  );
                storedProcCall.ExecuteNonQuery();
                WL( String.Format( "   The stored procedure returns: {0}", resultParam.Value ) );
                
        
                WL( "Update unexisting UID 100" );
                // @ is required into the parameter name BECAUSE it is identified with @
                // in the store procedure signature
                storedProcCall.Parameters["@uid"].Value = 100; 
                storedProcCall.Parameters["@newname"].Value = "Stupid User";
                storedProcCall.ExecuteNonQuery();
                WL( String.Format( "   The stored procedure returns: {0}", resultParam.Value ) );
                    
                
                
                WL( "4) Dataset after stored Proc Call" );
                r = SqlRead.ExecuteReader();
                while( r.Read() ) 
                    WL( String.Format( "UID: {0} - UserName: {1}", r["UID"], r["UsrName"] ) );
                r.Close();
                SqlConnProc.Close();
                SqlConnProc.Dispose(); // Force to release the connection from the Pool
                
            }
            
        
            
            #region Helper methods
            
            ...
            #endregion
        } 

        Les exemples concernant les DataSet et SqlDataAdapter sont disponibles dans l'article "ADO.NET - Aide mémoire - 2/2 : DataSet"

        mardi 9 novembre 2010

        Sync C# - Wait And Pulse - ProducerConsumerQueue non bloquant

        Qu'est-ce qu'une synchronisation bloquante
        Par exemple, lorsque l'on protège une section de code comme par exemple l'accès a une variable utilisée par deux threads, ont met en place une synchronisation bloquante.Un thread ne peut absolument pas accéder à la section critique (protégée par la section du lock) pendant qu'un autre thread accède à la section. Le thread ne pouvant pas accéder à la section critique est bloqué (c'est d'ailleurs sont état interne).
        Ce même cas de figure se présente également pour les threads partageant une même section critique, comme c'est le cas entre-autre pour les précédents exemples démontrant le fonctionnement interne d'une ProducerConsumerQueue (voir les articles Sync C# - ProducerConsumerQueue et Sync C# - Multi-Producer Multi-Consumer Queue).

        Le thread bloqué n'utilise plus de ressource CPU mais peut également être retiré de la pile d'exécution par le scheduler. Cette perte de temps inhérent aux opérations de scheduling/descheduling (context switching) peut devenir trop important si l'on est face à un système nécessitant une forte réactivité.
        C'est là qu'intervient Wait And Pulse.

        Description du pattern Wait And Pulse
        Wait And Pulse est une méthode de synchronisation non bloquante.
        Wait And Pulse est un pattern permettant de partager une sections critique (lock) entre deux ou plusieurs threads tout en évitant le context switching (scheduling/deschéduling).
        La section de lock est partagée (balancée) entre les threads à l'aide des instructions Monitor.Wait( LockingObject ) et Monitor.Pulse( LockingObject ).
        Pour fonctionner correctement, le pattern doit être scrupuleusement suivit... et la section critique doit impérativement commencé par un Wait.
        Losrque le wait est exécuté par un thread (le waiting thread) sur dans la section critique, le Framework libère le bloquage... ce qui permet à un autre thread d'acquérir (ou réacquérir) ce blocage jusqu'aà l'exécution d'un Pulse (lui c'est le pulsing thread).
        Donc, en pratique, le pulsing thread effectue ses traitements et fait un pulse.
        Lorsque le pulse est exécuté, le pulsing thread se bloque... libère le blocage pour le waiting thread.
        Le waiting thread réacquière le lock et effectue ses traitements... une fois fait, il réexécute un wait, ce qui libère le lock pour le pulsing thread.
        Le pulsing thread réacquière le lock et effectue ses traitements et puis fait un pulse... etc... etc... etc.

        Dans cette description, on remarque nettement que le Waiting thread et le Pulsing thread travaillent tous les deux en tandem. Et l'exécution balance d'un thread à l'autre.
        Si les explications manquent d'un peu de clarté, confrontez les au pseudo-code ci-dessous.

        Pseudo-code du pattern
        class WaitPulsedPattern {
            /* ... Déclaration privé variables protégées et partagées
              ... ex: une queue, un flag Go, une combinaison de flags, etc */
            private object locker = new object(); 
        
            public WaitPulsedPattern() { // CTor
                 /* ... Démarrage des Waiter Thread (consumer)
                   ... Exécute la méthode Worker()  */
            }
            
            public SomeProducer() {    
                /* ... méthode producer qui modifie la variable protégé
                   ...    et fait le pulse */
            
                lock( locker ) {
                    // ... modification des variables protégées
                    Monitor.Pulse( locker ); 
                }
            }
        
            public void Worker() {
                while( true ) {
                    lock( locker ) {
                        while( ! Variables_Partagées_Indique_Du_Travail )
                            Monitor.Wait( locker );            
                        CopieVariable = ... copier les informations du travail à faire ... 
                    } // lock libéré  
                    
                    // Arrêter le thread de traitement?
                    if( Conditions_D_arret )
                        return;
                    
                    // ... effectuer le travail ...
                }
            }
        }
        
        

        Wait And Pulse ProducerConsumerQueue 
        Voici une implémentation du pattern ProducerConsumerQueue (multi-consumer) utilisant Wait And Pulse.
        Fichier: Threading_ProducerConsumerQueue_WaitPulse.cs

        Quelques mots d'explication:
        • Les tâches sont empilées dans une queue (_taskQueue) protégé par l'objet de synchronisation _locker.
        • L'objet de synchronisation pour mettre en place la pattern Wait And Pulse.
        • Les threads de traitement (consumers) passe en Wait dès que la queue de traitement est vide.
        • Les threads de traitement terminent leurs processus dès qu'une tâche null est extraite de la queue (c'est l'exit condition des threads).
        • Les threads de traitement maintiennent le lock de façon aussi brève que possible.
          Il s'agit du temps entre l'acquisition lock (ou réacquisition du lock après un wait) ET le wait suivant ou la sortie du block de locking.
        • Les threads consummers (ceux qui font le traitement des tâches) sont créés dès l'instanciation de la classe. Cela assure toujours que le premier wait sera exécuté avant le premier pulse.
          Le premier thread acquière immédiatement le lock et exécute le wait (car la queue est vide).
          Ce qui à pour effet de relâcher le lock en attente d'un pulse dans le thread principal.
        • Le thread principal enqueue les tâches et utilise pulseAll pour signaler TOUS les consumers threads en attente.
        • Finalement, Dispose empile des nulls dans la queue (exit condition des consumer threads) et attends la fin de leur exécution.

        using System;
        using System.Collections.Generic;
        using System.Threading;
        
        public class WaitPulseProducerConsumerQueue : IDisposable
        {
            private object _locker = new object();
            private Queue<string> _tasksQueue = new Queue<string>();
            private Thread[] consumerThreads;
            
            public WaitPulseProducerConsumerQueue( int ConsumerCount ) 
            {
                // Create and Fire Consumer Threads
                consumerThreads = new Thread[ConsumerCount];
                for( int i = 0; i < ConsumerCount; i++ )
                    consumerThreads[i] = new Thread( new ThreadStart(ConsumeWorker) );
                // Wait/Pulse requires Wait to be executed first --> start the 
                //    threads now will ensure it.
                foreach( Thread th in consumerThreads )
                    th.Start();
            }
            
            /// <summary>
            /// Consumer for the Queue. This will run into a thread
            /// </summary>
            public void ConsumeWorker() 
            {
                while( true ) {
                    string task;
                    lock( _locker ){
                        // If the queue is empty --> wait (will release the lock)
                        if( _tasksQueue.Count == 0 ) Monitor.Wait( _locker );
                        // Dequeue task & release the lock
                        task = _tasksQueue.Dequeue();
                    }
                    // Exit condition --> exit the thread
                    if( task == null )
                        return;
                    // process the task
                    Console.WriteLine( string.Format( "Processing task {0}", task ) );
                    // Mimic processing
                    Thread.Sleep( TimeSpan.FromSeconds( 1 ) );
                }
            }
            
            public void Dispose() {
                // Inform threads to exit
                foreach( Thread th in consumerThreads ) EnqueueThreadExit();
                // Wait all thread to ends
                foreach( Thread th in consumerThreads ) th.Join();
            }
            
            /// <summary>
            /// Enqueue data + pulse worker
            /// </summary>
            /// <param name="sTask"></param>
            public void Enqueue( string sTask ) {
                // Wait to acquire the lock
                lock( _locker ) {
                    // Enqueue data
                    _tasksQueue.Enqueue( sTask );
                    // release lock to worker (until worker Waits)
                    Monitor.PulseAll( _locker );
                }
            }
            
            private void EnqueueThreadExit() 
            {
                Enqueue( null );
            }
        }
        public class MyClass
        {
            public static void RunSnippet()
            {
                const int NBR_CONSUMER_THREADS = 3;
                WaitPulseProducerConsumerQueue q = new WaitPulseProducerConsumerQueue( NBR_CONSUMER_THREADS );
                WL( "Enqueue data" );
                for( int i = 1; i<= 12; i++ )
                    q.Enqueue( string.Format( "task {0}", i ) );
                
                WL( "Disposing queue (send exit signal + joining  consumer threads)" );
                q.Dispose(); // ensure worker threads Join
                
                WL( "Queue disposed" );
            }
            
            #region Helper methods
            ...
            #endregion
        }
        

        Résultats:
        Enqueue data
        Processing task task 1
        Disposing queue (send exit signal + joining  consumer threads)
        Processing task task 2
        Processing task task 3
        Processing task task 4
        Processing task task 5
        Processing task task 6
        Processing task task 7
        Processing task task 8
        Processing task task 9
        Processing task task 10
        Processing task task 11
        Processing task task 12
        Queue disposed
        Press any key to continue...
        

        lundi 1 novembre 2010

        Sync C# - Multi-Producer Multi-Consumer Queue

        L'article "Sync C# - ProducerConsumerQueue" décrivait et implémentait le pattern ProducerConsumerQueue (lecture recommandée pour la compréhension de cet article).

        L'article ici présent est un complément qui met en place une version plus avancée du pattern où il est possible:
        • D'avoir plusieurs producer threads (soit plusieurs threads ajoutant des tâches dans la queue de traitement).
        • Mais également d'avoir plusieurs consumer threads (soit plusieurs worker threads traitant les tâches en attente dans la queue de traitement).
        Exemple - Multiple Producer, Multiple Consumer
        Le fichier Threading_MultiProducerMultiConsumerQueue.cs contient un exemple où la queue (de la classe ProducerConsumerQueue) est alimentée par plusieurs threads et où les tâches traitées par plusieurs worker threads.
        Le nombre de worker threads (consumer) étant indiqué en paramètre lors de la création de la classe ProducerConsumerQueue.

        Quelques-mots d'explication:
        • Tout comme dans le précédent article, lorsque qu'un worker thread (consumer) n'a plus de tâche à traiter dans la queue, il passe en attente de signal/évènement.
        • Tout comme dans le précédent article, un WaitHandle est utilisé pour signaler qu'une tâche est en attente de traitement. Ce WaitHandle se nomme whNewTaskAvailable.
          Comme c'est un AutoResetEvent, un seul des worker threads en attente est alerté. Si une deuxième tache arrive rapidement dans la queue, le signal est relancé et sera (plus que probablement) pris en charge par un autre worker thread en attente.
        • Tout comme le précédent article, empiler une tâche NULL permet informe via la queue que le traitement est terminé.
          Cependant, il y a ici une différence notable:
          • A) Le worker thread qui recoit la tache NULL lève le signal whExitWorkerThreads et termine son exécution.
          • B) Les autres workers threads en attente de signal et recevant le signal whExitWorkerThreads terminent leur processus. 
        • Un ManualResetEvent (WaitHandle) est utiliser pour signaler aux worker threads d'arrêter le processus (et le traitement de la queue).
          Il s'agit ici d'un Reset Event de type manuel car tous les workers threads doivent pouvoir être signalés.
          En effet, un AutoResetEvent n'aurait permis de signaler qu'un seul worker thread à la fois (puisqu'il est resetté automatiquement à chaque fois).
        • Les workers threads utilisent l'instruction WaitHandle.WaitAny pour détecter le signal/évènement whNewTaskAvailable ou whExitWorkerThreads.

        Code du ProducerConsumerQueue
        Le code complet du snippet est disponible dans le fichier Threading_MultiProducerMultiConsumerQueue.cs
        public class ProducerConsumerQueue : IDisposable {
            // Set true to see additional synchronization message
            const bool _SHOW_DEBUG_ = true;
            // Tasks queue, enqueue a NULL to exit worker thread
            Queue<string> tasks = new Queue<string>(); 
            // Will signal worker thread that new Tasks are present.
            //   AutoResetEvent because only one thread can take the task
            EventWaitHandle whNewTaskAvailable = new AutoResetEvent(false);
            // Will signal the woker Threads to terminate the execution
            //   All threads must be signaled
            EventWaitHandle whExitWorkerThreads = new ManualResetEvent(false);
            Object locker = new Object();
            List<Thread> workers = new List<Thread>();
            
            /// <summary>
            /// protect CTor without arguments
            /// </summary>
            private ProducerConsumerQueue() 
            {
            }
            
            /// <summary>
            /// CTor. Start the worker thread
            /// </summary>
            /// <param name="NbrWorkerThreads">Number of consumer threads to starts</param>
            public ProducerConsumerQueue( int NbrWorkerThreads ) {
                if( NbrWorkerThreads <= 0 )
                    throw new ArgumentOutOfRangeException("NbrWorkerThreads", NbrWorkerThreads, "Must be greater than 0" );
                foreach( int i in Enumerable.Range( 0, NbrWorkerThreads )) {
                    if( _SHOW_DEBUG_ )
                        Console.WriteLine( String.Format( "Create Worker #{0}", i+1) );
                    Thread worker = new Thread( DoWork );
                    workers.Add( worker );
                    worker.Start();
                }
            }
            
            /// <summary>
            /// Enqueue some work
            /// </summary>
            /// <param name="sTaskToPrint"></param>
            public void EnqueueTask( string sTaskToPrint ){
                lock( locker ) {
                    tasks.Enqueue( sTaskToPrint );
                    whNewTaskAvailable.Set(); // if worker was blocking (because no more work), signal that new work arrived.
                }    
            }
            
            /// <summary>
            /// Enqueue a NULL to inform the worker threads to Exit
            /// The whExitWorkerThreads is not signaled here, this ensure all the queued tasks
            /// to be processed 
            /// </summary>
            public void EnqueueExitThread() {
                EnqueueTask( null );
            }
            
            /// <summary>
            /// Dispose the Object
            /// </summary>
            public void Dispose() {
                // Signal the consumer to exit
                EnqueueExitThread();
                // Wait workers threads to finish
                foreach( Thread worker in workers )
                    worker.Join();
                // Release OS ressource
                whNewTaskAvailable.Close();
                whExitWorkerThreads.Close();
            }
            
            /// <summary>
            /// Method that implement the Consummer
            /// </summary>
            public void DoWork(){
                while(true) {
                    string task = null;
                    bool waitSignalToContinue = false; // Consumer waits for signal (new item added) to continue to consume
                    
                    // Dequeue the task
                    lock( locker ) {
                        if( tasks.Count == 0 ) {
                            // No more work, thread should wait
                            waitSignalToContinue = true;
                        }
                        else {
                            // Make a copy of the task
                            string temp = tasks.Dequeue();
                            if( temp != null ) task = String.Copy( temp  );
                        
                            // Null is used to signal Exit Worker Thread
                            if( task == null ) {
                                if( _SHOW_DEBUG_ )
                                    Console.WriteLine( String.Format("Received null task, current thread {0} set ExitWorkerThread signal and terminates", Thread.CurrentThread.GetHashCode()) );
                                // Signal ALL the worker threads to exit now;
                                whExitWorkerThreads.Set();
                                return; // Exit current thread
                            }
                        }
                                    
                    }
        
                    // Wait for signal must be performed OUTSIDE the lock !
                    if( waitSignalToContinue ) {
                        // if no more task in the list 
                        //   --> Block the thread and wait signal for new job to do    (or Exit thread)
                        if( _SHOW_DEBUG_ )
                            Console.WriteLine( String.Format( "Thread {0} WaitAny", Thread.CurrentThread.GetHashCode()) );
                        int whIdx = WaitHandle.WaitAny( new WaitHandle[] { whNewTaskAvailable, whExitWorkerThreads } );
                        switch ( whIdx ) {
                            case 0 : // whNewTaskAvailable
                                if( _SHOW_DEBUG_ )
                                    Console.WriteLine( String.Format( "Thread {0} Received NewTaskAvailable signal", Thread.CurrentThread.GetHashCode()) );
                                // New task to execute -> continue thread execution
                                break;
                            case 1 : // whExitWorkerThreads
                                if( _SHOW_DEBUG_ )
                                    Console.WriteLine( String.Format( "Thread {0} Received ExitWorkerThread signal", Thread.CurrentThread.GetHashCode()) );
                                // Exit the worker thread
                                return;
                            default:
                                throw new Exception( String.Format("Unexpected whIdx {0} value", whIdx ) );
                        } 
                    }
        
                    
                    // Execute the task
                    //   - task may be null if the Queue is empty
                    //   - An whNewTaskAvailable may also not be captured yet 
                    if( task != null ){
                        Console.WriteLine( String.Format( "Worker Thread {0} Performing task: {1}", Thread.CurrentThread.GetHashCode(), task) );
                        Thread.Sleep( TimeSpan.FromMilliseconds(500) );
                    }        
        
                }
            }
        }