Evolution de la façon de programmer autour de Mercator

changement syntaxe mercator 10.1


Mercator 10.1 apporte une évolution importante quant à la façon de programmer autour de Mercator. Cela concerne tant les customizers que les applications externes (sites web, …).

Constamment, nous manipulons en C# des DataRows pour accéder aux données du serveur SQL. Cela se fait, par exemple, par un code : billingEngine.PIEDS["mon_champ"] = …
Cela pose plusieurs problèmes :

  • Le programmeur doit s’assurer que la colonne « mon_champ » existe bien dans la table SQL PIEDS_V/A
  • Il doit connaître son type pour, in fine, écrire par exemple ceci : Convert.ToDouble(billingEngine.PIEDS["mon_champ"])
  • Il ne peut pas utiliser IntelliSense pour avoir une suggestion de saisie rapide du nom de colonne « mon_champ »
  • Le compilateur ne va jamais détecter que « mon_champ » n’existe pas. Un filet de sécurité important est donc perdu !
  • Le tout est long à écrire [] " + Convert.To.. Or, le gain de temps est toujours appréciable…

Partant du même constat, Microsoft avait fourni depuis longtemps diverses solutions permettant de créer de façon plus ou moins automatique des classes d’objets correspondant à la structure de la base de données. Les solutions s’appellent :

  • Linq to SQL
  • Entity Framework

Elles présentaient toutefois un très gros inconvénient pour Mercator : il est impossible de générer ces classes en dehors de l’environnement de développement, donc « at run-time ».
Or, tous les Mercator sont différents. Ils n’ont pas la même base de données SQL et, de plus, cette dernière évolue constamment.
Mercator Majuro a d’ailleurs amené une grande Agilité dans notre relation à la base de données : il n’est plus obligatoire de faire sortir les utilisateurs de Mercator pour modifier le schéma de la base de données.
Il eut donc été regrettable de perdre à nouveau cette souplesse, sous prétexte de gagner en confort en utilisant l’un des deux outils cités ci-dessus.

A noter qu’Entity Framework est extrêmement puissant et a une ambition bien plus large que les propos repris dans ce message.


Tout en préservant cette Agilité, il nous a semblé opportun de moderniser la syntaxe.
Ceci a été fait et rendu possible par le mécanisme permettant de compiler, au sein d’un dossier Mercator, le schéma de la base de données (dans MercatorDatabase.dll).
Nous distribuons une version relativement agnostique de MercatorDatabase.dll (avec les champs de base, « le minimum vital »), mais, à tout instant, il est possible de personnaliser les classes qu’elle contient. Le schéma de la base de données peut donc être mis à jour à la demande via les outils standards de Mercator (fichiers SQL Assemblies). Il peut ensuite être distribué par les outils standards de Mercator (Fichiers SQL / Assemblies).  La boucle est bouclée !


S’ajoute à ceci la constatation que l’utilisation de Linq était, elle-aussi, très intéressante et appréciable.

Linq est un jeu d’extensions qui permet d’utiliser les objets énumérables du framework .net avec une syntaxe plus compacte.
Un exemple simple est repris ici :

// avant
double d1 = 0;
foreach (DataRow dr in billingEngine.LIGNES.Select("sel=1"))
    d1 += Convert.ToDouble(dr["q"]);
// après
double d1 = billingEngine.LignesVRecords.Where(l => l.SEL).Sum(l => l.Q);

3 lignes de code deviennent une seule. Et ce code fait, en plus, exclusivement appel à des types connus par le compilateur, via IntelliSense.

L’utilisation de Linq s’est faite tardivement dans Mercator. Cette absence était causée par la contrainte de compatibilité entre les versions Aruba et Majuro. La version Aruba était limitée au Framework 2.0, alors que Linq est arrivé avec le Framework 3.5. Il n’était donc pas possible d’en disposer.


C’est ici que se rejoignent les 2 points : la structure de la DB dans des classes et Linq. En effet, toutes les collections reprises dans MercatorDatabase.dll implémentent l’interface IEnumerable. Ce qui les rend immédiatement utilisables en Linq, sans devoir passer par un Cast<>().
Le style de programmation autour de Mercator va donc profondément évoluer.

Tous les modules repris sur notre site web dans la rubrique « Exemples de codes » ont été revus et modifiés à l’aune de ce qui précède.
La comparaison Avant - Après de quelques modules est intéressante. Par exemple, le très célèbre customizer « bons cadeaux » :

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Windows.Forms;
using MercatorApi;
using MercatorUi;

namespace Billing
{
    public class Customizer : MercatorUi.ICustomizers.IBillingEngineCreated, MercatorUi.ICustomizers.IBillingEngineClosed
    {
        public void BillingEngineCreated(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.AfterInsertItem += new MercatorUi.Engine.Gescom.BillingEngine.AfterInsertItemEventHandler(AfterInsertItem);
            BillingEngine.BeforePayment += new EventHandler(BeforePayment);
            BillingEngine.AfterSave += new EventHandler(AfterSave);
        }

        public void BillingEngineClosed(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.AfterInsertItem -= new MercatorUi.Engine.Gescom.BillingEngine.AfterInsertItemEventHandler(AfterInsertItem);
            BillingEngine.BeforePayment -= new EventHandler(BeforePayment);
            BillingEngine.AfterSave -= new EventHandler(AfterSave);
        }

        void AfterInsertItem(object sender, MercatorUi.Engine.Gescom.BillingEngine.AfterInsertItemEventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            if (e.DataRowLignes["S_CLE1"].ToString().Trim() == "UTILBONCAD")
            {
                string id_boncad = Dialogs.AskString("Veuillez scanner le code-barres", "", true, 35);
                if (id_boncad != "")
                {
                    DataRow[] drcad = billingEngine.LIGNES.Select("id_boncad='" + id_boncad.Trim() + "'");
                    int nbBoncad = 0;
                    if (drcad != null)
                    {
                        foreach (DataRow drcad2 in drcad)
                        {
                            nbBoncad++;
                        }
                    }
                    if (nbBoncad == 0)
                    {
                        string reqSqlcad = "select count(*) as nbUtil, sum(q) as q, sum(pu) as pu from lignes_v where id_boncad=@id_boncad";
                        DataSet dscad = Api.Zselect(MercatorUi.Globals.RepData, reqSqlcad, new MercatorSqlParam("@id_boncad", id_boncad, SqlDbType.Char));
                        if (dscad != null)
                        {
                            if (Convert.ToInt16(dscad.Tables[0].Rows[0]["nbUtil"]) == 0)
                            {
                                Dialogs.Stop("Ce bon cadeau n'existe pas !");
                                e.CancelInsertItem = true;
                            }
                            else
                            {
                                if (Convert.ToInt16(dscad.Tables[0].Rows[0]["nbUtil"]) > 1)
                                {
                                    Dialogs.Stop("Ce bon cadeau a déjà été utilisé !");
                                    e.CancelInsertItem = true;
                                }
                                else
                                {
                                    e.DataRowLignes["Q"] = Convert.ToDouble(dscad.Tables[0].Rows[0]["q"]) * -1;
                                    e.DataRowLignes["PU"] = Convert.ToDouble(dscad.Tables[0].Rows[0]["pu"]);
                                    e.DataRowLignes["ID_BONCAD"] = id_boncad.Trim();
                                }
                            }
                        }
                        else
                        {
                            e.CancelInsertItem = true;
                        }
                    }
                    else
                    {
                        Dialogs.Stop("Ce bon cadeau a déjà été utilisé au sein de cette vente !");
                        e.CancelInsertItem = true;
                    }
                }
                else
                {
                    Dialogs.Stop("Code-barres vide !");
                    e.CancelInsertItem = true;
                }
            }
        }

        void BeforePayment(object sender, EventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            DataRow[] drBonCad = billingEngine.LIGNES.Select("s_cle1='BONCAD'");
            if (drBonCad.Length != 0)
            {
                int nLigne = 0;
                foreach (DataRow drBC in billingEngine.LIGNES.Rows)
                {
                    if (drBC["S_CLE1"].ToString() == "BONCAD")
                    {
                        billingEngine.LIGNES.Rows[nLigne]["id_boncad"] = billingEngine.PIEDS["id"].ToString() + billingEngine.LIGNES.Rows[nLigne]["dl_id"].ToString();
                    }
                    nLigne++;
                }
            }
        }

        void AfterSave(object sender, EventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            DataRow[] drBonCad = billingEngine.LIGNES.Select("s_cle1='BONCAD'");
            if (drBonCad.Length != 0)
            {
                DataSet ds = new DataSet();
                DataTable dt_lignes;
                DataTable dt_pieds;
                foreach (DataRow drBC in billingEngine.LIGNES.Rows)
                {
                    if (drBC["S_CLE1"].ToString() == "BONCAD")
                    {
                        ds = new DataSet();
                        dt_lignes = new DataTable();
                        dt_lignes = billingEngine.LIGNES.Clone();
                        dt_lignes.ImportRow(drBC);
                        dt_pieds = new DataTable();
                        dt_pieds = billingEngine.PIEDS.Table.Clone();
                        dt_pieds.ImportRow(billingEngine.PIEDS);
                        ds.Tables.Add(dt_lignes);
                        ds.Tables.Add(dt_pieds);
                        ds.Tables.Add(Api.DataTableFromDico(MercatorUi.Globals.ParamIdentif, "ParamIdentif"));
                        List<MercatorUi.Reporting.OutputDescriptor> listOutputDescriptors = new List<MercatorUi.Reporting.OutputDescriptor>();
                        listOutputDescriptors.Add(new MercatorUi.Reporting.OutputDescriptorPrint());
                        MercatorUi.Reporting.ReportingStatic.Reporting.RunReport("BonCadeau", @"<MainDir\BonCadeau.repx", ds, listOutputDescriptors);
                    }
                }
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Linq;
using MercatorApi;
using MercatorUi;

namespace Billing
{
    public class Customizer : MercatorUi.ICustomizers.IBillingEngineCreated, MercatorUi.ICustomizers.IBillingEngineClosed
    {
        public void BillingEngineCreated(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.AfterInsertItem += AfterInsertItem;
            BillingEngine.BeforePayment += BeforePayment;
            BillingEngine.AfterSave += AfterSave;
        }

        public void BillingEngineClosed(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.AfterInsertItem -= AfterInsertItem;
            BillingEngine.BeforePayment -= BeforePayment;
            BillingEngine.AfterSave -= AfterSave;
        }

        void AfterInsertItem(object sender, MercatorUi.Engine.Gescom.BillingEngine.AfterInsertItemEventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            if (e.LignesVRecord.S_CLE1 == "UTILBONCAD")
            {
                string id_boncad = Dialogs.AskString("Veuillez scanner le code-barres", "", true, 35).Trim();
                if (id_boncad != "")
                {
                    int nbBoncad = billingEngine.LignesVRecords.Where(l => l.ID_BONCAD == id_boncad).Count();
                    if (nbBoncad == 0)
                    {
                        string reqSqlcad = "select count(*) as nbUtil, sum(q) as q, sum(pu) as pu from lignes_v where id_boncad=@id_boncad";
                        DataSet dscad = Api.Zselect(MercatorUi.Globals.RepData, reqSqlcad, new MercatorSqlParam("@id_boncad", id_boncad, SqlDbType.Char));
                        if (dscad != null)
                        {
                            if (Convert.ToInt16(dscad.Tables[0].Rows[0]["nbUtil"]) == 0)
                            {
                                Dialogs.Stop("Ce bon cadeau n'existe pas !");
                                e.CancelInsertItem = true;
                            }
                            else if (Convert.ToInt16(dscad.Tables[0].Rows[0]["nbUtil"]) > 1)
                            {
                                Dialogs.Stop("Ce bon cadeau a déjà été utilisé !");
                                e.CancelInsertItem = true;
                            }
                            else
                            {
                                e.LignesVRecord.Q = Convert.ToDouble(dscad.Tables[0].Rows[0]["q"]) * -1;
                                e.LignesVRecord.PU = Convert.ToDouble(dscad.Tables[0].Rows[0]["pu"]);
                                e.LignesVRecord.ID_BONCAD = id_boncad;
                            }

                        }
                        else
                        {
                            e.CancelInsertItem = true;
                        }
                    }
                    else
                    {
                        Dialogs.Stop("Ce bon cadeau a déjà été utilisé au sein de cette vente !");
                        e.CancelInsertItem = true;
                    }
                }
                else
                {
                    Dialogs.Stop("Code-barres vide !");
                    e.CancelInsertItem = true;
                }
            }
        }

        void BeforePayment(object sender, EventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            billingEngine.LignesVRecords.Where(l => l.S_CLE1 == "BONCAD").ToList().ForEach(l => l.ID_BONCAD = billingEngine.PiedsVRecord.ID + l.DL_ID);
        }

        void AfterSave(object sender, EventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            if (billingEngine.LignesVRecords.Where(l => l.S_CLE1 == "BONCAD").Count() != 0)
            {
                DataSet ds = new DataSet();
                DataTable dt_lignes;
                DataTable dt_pieds;
                foreach (DataRow drBC in billingEngine.LIGNES.Select("S_CLE1='BONCAD'"))
                {
                    ds = new DataSet();
                    dt_lignes = new DataTable();
                    dt_lignes = billingEngine.LIGNES.Clone();
                    dt_lignes.ImportRow(drBC);
                    dt_pieds = new DataTable();
                    dt_pieds = billingEngine.PIEDS.Table.Clone();
                    dt_pieds.ImportRow(billingEngine.PIEDS);
                    ds.Tables.Add(dt_lignes);
                    ds.Tables.Add(dt_pieds);
                    ds.Tables.Add(Api.DataTableFromDico(MercatorUi.Globals.ParamIdentif, "ParamIdentif"));
                    List<MercatorUi.Reporting.OutputDescriptor> listOutputDescriptors = new List<MercatorUi.Reporting.OutputDescriptor>();
                    listOutputDescriptors.Add(new MercatorUi.Reporting.OutputDescriptorPrint());
                    MercatorUi.Reporting.ReportingStatic.Reporting.RunReport("BonCadeau", @"<MainDir\BonCadeau.repx", ds, listOutputDescriptors);
                }
            }
        }
    }
}
 

Toutes les nouvelles classes ont un nom qui termine par Record ou Records. Elles sont donc très faciles à trouver dans Intellisense. Notez que ces classes ne dupliquent en rien l’information. Il s’agit seulement d’une nouvelle façon de pointer vers la/les mêmes données que précédemment.

Bien sûr, il n’est pas obligatoire de réécrire tous les codes existants avec la nouvelle syntaxe. Cette nouvelle notation a pour but d’améliorer le confort du programmeur et relève donc du choix de chacun.