Vincent et Stéphane, certifiés Microsoft

Par Louis-Philippe Baril, vendredi 11 décembre 2009 08:36
Catégorie : Programmation

Après de longues heures d'étude, Nmedia peut maintenant compter sur 2 nouveaux certifiés Microsoft au sein de son équipe. En effet, Vincent Beaulieu a passé avec succès l'examen menant à la certification 070-536 «Application Development Foundation». Quant à Stéphane Lépine, il a obtenu la certification 070-562 «ASP.NET Application Development». Dans son cas, il s'agit de sa deuxième réussite ! Bravo.

Linq-to-Sql et réécriture des requêtes

Par Patrick Bélanger, mercredi 9 décembre 2009 17:42
Catégorie : Programmation

Depuis déjà deux ans, nous utilisons Linq-To-Sql dans la grande majorité de nos développements basés sur Sql Server 2005 / 2008.  Dans une première phase, nous avions refactorisé la majorité de notre code pour passer de DataSets + Store procédure vers Linq-To-Sql.  Le gain en maintenance et en simplicité en valait tout simplement l’effort.  Nous n’avons rien regretté depuis.


Suite à cette phase de refactorisation, nous avons regardé pour améliorer notre approche de développement en utilisant les extensions de méthodes jumelé à la réécriture d’expressions.  Et une fois de plus, nous venons de réaliser quelque chose de phénoménale par cette approche.


Il faut d’abord comprendre que lorsque l’on écrit une requête Linq-To-Sql c’est en fait une arborescence d’expression qui est créé.  Cette arborescence décrit en termes d’objet comment on désire effectuer la requête.  Cette arborescence est ensuite envoyé à l’optimisateur Linq qui va la transformer en requête Sql.


En résumé, nous avons donc ces étapes:

    1. Requête Linq

    2. Arborescence d’expressions

    3. Optimisateur

    4. Requête Sql

    5. Base de données

 
Regardons un exemple… lorsque l’on écrit cette requête Linq : 
 

var x = from p in db.Persons 
        where p.FirstName.StartsWith("C") 
        select p; 


On obtient cette arborescence d’expression :

___ex0 --> MethodCallExpression 
    Method : MethodInfo : "Where<Person>" 
    Object : Expression : null 
    Arguments : ReadOnlyCollection<Expression> 
        ___ex1 --> ConstantExpression 
            Value : Object : "Table(Person)" 
            NodeType : ExpressionType : "Constant" 
            Type : Type : "Table<Person>" 
        ___ex2 --> UnaryExpression 
            Operand : ExpressionLambda 
                ___ex3 --> Expression<Func<Person, Boolean>> 
                    Body : ExpressionCall 
                        ___ex4 --> MethodCallExpression 
                            Method : MethodInfo : "StartsWith" 
                            Object : ExpressionMemberAccess 
                                ___ex5 --> MemberExpression 
                                    Expression : ExpressionParameter 
                                        ___ex6 --> ParameterExpression 
                                            Name : String : "p" 
                                            NodeType : ExpressionType : "Parameter" 
                                            Type : Type : "Person" 
                                    Member : MemberInfo : "System.String FirstName" 
                                    NodeType : ExpressionType : "MemberAccess" 
                                    Type : Type : "String" 
                            Arguments : ReadOnlyCollection<Expression> 
                                ___ex7 --> ConstantExpression 
                                    Value : Object : "C" 
                                    NodeType : ExpressionType : "Constant" 
                                    Type : Type : "String" 
                            NodeType : ExpressionType : "Call" 
                            Type : Type : "Boolean" 
                    Parameters : ReadOnlyCollection<ParameterExpression> 
                        ___ex6 --> ParameterExpression 
                            Name : String : "p" 
                            NodeType : ExpressionType : "Parameter" 
                            Type : Type : "Person" 
                    NodeType : ExpressionType : "Lambda" 
                    Type : Type : "Func<Person, Boolean>" 
            Method : MethodInfo : null 
            IsLifted : Boolean : "False" 
            IsLiftedToNull : Boolean : "False" 
            NodeType : ExpressionType : "Quote" 
            Type : Type : "Expression<Func<Person, Boolean>>" 
    NodeType : ExpressionType : "Call" 
    Type : Type : "IQueryable<Person>" 


Ensuite, l’optimisateur roule sur l’arborescence une fois que l’on demande à aller chercher l’info de la base de données, et transforme cette grosse arborescence en cette petite requête : 
 

SELECT [t0].[PersonID], [t0].[FirstName], [t0].[Initials], [t0].[LastName], [t0].[IMAddress], [t0].[Birthdate], [t0].[Timestamp], [t0].[ContactID], [t0].[PersonTitleID], [t0].[Sex], [t0].[ProfessionalTitle], [t0].[Notes] 
FROM [dbo].[Person] AS [t0] 
WHERE [t0].[FirstName] LIKE 'C%' 


Où nous pouvons gagner des points très intéressants, c’est en comprenant bien la nature de l’arborescence…  Car une fois que l’on comprend comment elle représente la requête Linq, on peut essayer de la modifier ! 
 

En poursuivant avec le même exemple, imaginons que l’on désire filtrer la table sans utiliser le where…

On pourrait par exemple écrire cette requête Linq :

var x = from p in db.Persons.FilterByFirstName("C") 
        select p; 


Qui sort les « p » non plus de db.Persons, mais de db.Persons.FilterByFirstName() !

Comment définir cette nouvelle méthode ?  Via les extensions de méthodes !  Une classe statique qui contient les extensions.  Chaque extensions de méthode a son premier paramètre préfixé du mot réservé « this ».

Voici le code pour cet exemple :

public static class LinqExtenders 
{ 
  public static IQueryable<Person> FilterByFirstName(this IQueryable<Person> persons, string s) 
  { 
    return from p in persons 
           where p.FirstName.StartsWith(s) 
           select p; 
  } 
} 


Simple n’est ce pas ?  Cette extension de méthode permet de modifier l’arborescence d’expression de manière très simple.  Nous avons donc une arborescence différente de la précédente qui contenait 6 expression, alors que cette nouvelle en contient le double :

___ex0 --> MethodCallExpression 
    Method : MethodInfo : "Select<Person, Person>" 
    Object : Expression : null 
    Arguments : ReadOnlyCollection<Expression> 
        ___ex1 --> MethodCallExpression 
            Method : MethodInfo : "Where<Person>" 
            Object : Expression : null 
            Arguments : ReadOnlyCollection<Expression> 
                ___ex2 --> ConstantExpression 
                    Value : Object : "Table(Person)" 
                    NodeType : ExpressionType : "Constant" 
                    Type : Type : "Table<Person>" 
                ___ex3 --> UnaryExpression 
                    Operand : ExpressionLambda 
                        ___ex4 --> Expression<Func<Person, Boolean>> 
                            Body : ExpressionCall 
                                ___ex5 --> MethodCallExpression 
                                    Method : MethodInfo : "StartsWith" 
                                    Object : ExpressionMemberAccess 
                                        ___ex6 --> MemberExpression 
                                            Expression : ExpressionParameter 
                                                ___ex7 --> ParameterExpression 
                                                    Name : String : "p" 
                                                    NodeType : ExpressionType : "Parameter" 
                                                    Type : Type : "Person" 
                                            Member : MemberInfo : "System.String FirstName" 
                                            NodeType : ExpressionType : "MemberAccess" 
                                            Type : Type : "String" 
                                    Arguments : ReadOnlyCollection<Expression> 
                                        ___ex8 --> MemberExpression 
                                            Expression : ExpressionConstant 
                                                ___ex9 --> ConstantExpression 
                                                    Value : Object : "WpfApplication1.LinqExtenders+<>c__DisplayClass0" 
                                                    NodeType : ExpressionType : "Constant" 
                                                    Type : Type : "<>c__DisplayClass0" 
                                            Member : MemberInfo : "System.String s" 
                                            NodeType : ExpressionType : "MemberAccess" 
                                            Type : Type : "String" 
                                    NodeType : ExpressionType : "Call" 
                                    Type : Type : "Boolean" 
                            Parameters : ReadOnlyCollection<ParameterExpression> 
                                ___ex7 --> ParameterExpression 
                                    Name : String : "p" 
                                    NodeType : ExpressionType : "Parameter" 
                                    Type : Type : "Person" 
                            NodeType : ExpressionType : "Lambda" 
                            Type : Type : "Func<Person, Boolean>" 
                    Method : MethodInfo : null 
                    IsLifted : Boolean : "False" 
                    IsLiftedToNull : Boolean : "False" 
                    NodeType : ExpressionType : "Quote" 
                    Type : Type : "Expression<Func<Person, Boolean>>" 
            NodeType : ExpressionType : "Call" 
            Type : Type : "IQueryable<Person>" 
        ___ex10 --> UnaryExpression 
            Operand : ExpressionLambda 
                ___ex11 --> Expression<Func<Person, Person>> 
                    Body : ExpressionParameter 
                        ___ex12 --> ParameterExpression 
                            Name : String : "p" 
                            NodeType : ExpressionType : "Parameter" 
                            Type : Type : "Person" 
                    Parameters : ReadOnlyCollection<ParameterExpression> 
                        ___ex12 --> ParameterExpression 
                            Name : String : "p" 
                            NodeType : ExpressionType : "Parameter" 
                            Type : Type : "Person" 
                    NodeType : ExpressionType : "Lambda" 
                    Type : Type : "Func<Person, Person>" 
            Method : MethodInfo : null 
            IsLifted : Boolean : "False" 
            IsLiftedToNull : Boolean : "False" 
            NodeType : ExpressionType : "Quote" 
            Type : Type : "Expression<Func<Person, Person>>" 
    NodeType : ExpressionType : "Call" 
    Type : Type : "IQueryable<Person>" 

 

Mais qui génère toujours le même Sql en bout de ligne, comme quoi l’optimisateur fait bien son travail !


SELECT [t0].[PersonID], [t0].[FirstName], [t0].[Initials], [t0].[LastName], [t0].[IMAddress], [t0].[Birthdate], [t0].[Timestamp], [t0].[ContactID], [t0].[PersonTitleID], [t0].[Sex], [t0].[ProfessionalTitle], [t0].[Notes] 
FROM [dbo].[Person] AS [t0] 
WHERE [t0].[FirstName] LIKE 'C%'

 

Et maintenant, imaginez ce que l’on peut accomplir en remplaçant le filtre simple que nous avions par un filtre plus évolué, qui construit dynamiquement des relations avec plusieurs familles de joins, ajoute dynamiquement de nouveaux prédicats à la requête, change les sources des tables en fonction de paramètre…


 

C’est ce que nous avons accomplis pour une nouvelle réussite aujourd’hui !

Voici un autre bel exemple de la puissance des requêtes Linq.  On avait déjà plusieurs centaines de requêtes Linq comme celle-ci dans le code :


from zh in dbContext.ZoneHierarchies.Filter(args) 
join z in dbContext.Zones on zh.ZoneID equals z.ZoneID 
join zl in dbContext.ZoneLocales.FilterCulture(args) on z.ZoneID equals zl.ZoneID 
join zp in dbContext.ZonePages.FilterCulture(args, "ZoneID", new string[] { }) on z.ZoneID equals zp.ZoneID 
where (((args.ParentZoneHierarchyID == null) && (!zh.ParentZoneHierarchyID.HasValue)) || 
       (args.ParentZoneHierarchyID.GetValueOrDefault(-1) == zh.ParentZoneHierarchyID)) 
         && ((args.ZoneRequestType == ZoneRequestResultType.All) 
         || (((args.ZoneRequestType == ZoneRequestResultType.OnlyPages) && (z.ZoneType == (int)ZoneType.Page)) 
         || ((args.ZoneRequestType == ZoneRequestResultType.OnlyDocuments) && (z.ZoneType == (int)ZoneType.Document)))) 
orderby zp.Order, zp.Title 
select EntityFactory.CreateZoneFileEntityFromDatabase<ZoneFileEntity>(zh, zp, zl, (ZoneType)z.ZoneType, args, dbContext); 


Pas trop compliqué, elle se lit quand même bien et indique bien son intention.  Et sur ces requêtes, on avait déjà des méthodes extensions pour filtrer en fonction de statuts, date d’activations, cultures, … 


Et là, on a implémenté dans le code des filtres existants, des filtres supplémentaires pour gérer notre nouvelle structure avec version de contenues (pouvoir supporter de retourner en arrière dans des enregistrements de tables).


Et voilà, toutes les requêtes écrites avec ces petits bouts de Linq supportent désormais le versionnage des tables !  Le petit peu de linq qui est là et se lit très bien génère la grosse requête SQL du bas, qui est assez incompréhensible à première vues.

SELECT [t0].[ZoneHierarchyID], [t0].[ZoneID], [t0].[ParentZoneHierarchyID], [t0].[ActiveFrom], [t0].[ActiveTo], [t0].[Status], [t9].[ZoneID] AS [ZoneID2], [t9].[Title], [t9].[FileName], [t9].[CultureID], [t9].[ActiveFrom] AS [ActiveFrom2], [t9].[ActiveTo] AS [ActiveTo2], [t9].[Status] AS [Status2], [t9].[ZonePageID], [t9].[Order] AS [Order], [t9].[ModificationDate], [t9].[ModifiedByName], [t9].[ModifiedByContactGuid], [t9].[PublicationVersion], [t5].[ZoneID] AS [ZoneID3], [t5].[CultureID] AS [CultureID2], [t5].[ZoneLocaleName], [t5].[ChildrenDirectoryName], [t5].[ActiveFrom] AS [ActiveFrom3], [t5].[ActiveTo] AS [ActiveTo3], [t5].[Description], [t5].[Status] AS [Status3], [t5].[ZoneLocaleID], [t5].[ModificationDate] AS [ModificationDate2], [t5].[ModifiedByName] AS [ModifiedByName2], [t5].[ModifiedByContactGuid] AS [ModifiedByContactGuid2], [t5].[PublicationVersion] AS [PublicationVersion2], [t1].[ZoneType] AS [zt] 
FROM [Content].[ZoneHierarchy] AS [t0] 
INNER JOIN [Content].[Zone] AS [t1] ON [t0].[ZoneID] = [t1].[ZoneID] 
INNER JOIN ( 
    SELECT DISTINCT [t3].[ZoneID], [t3].[CultureID], [t3].[ZoneLocaleName], [t3].[ChildrenDirectoryName], [t3].[ActiveFrom], [t3].[ActiveTo], [t3].[Description], [t3].[Status], [t3].[ZoneLocaleID], [t3].[ModificationDate], [t3].[ModifiedByName], [t3].[ModifiedByContactGuid], [t3].[PublicationVersion] 
    FROM [Content].[ZoneLocale] AS [t2] 
    CROSS JOIN ([Content].[ZoneLocale] AS [t3] 
        INNER JOIN [Version].[ZoneLocaleVersionIndex] AS [t4] ON [t3].[ZoneLocaleID] = [t4].[ZoneLocaleID]) 
    WHERE [t4].[IndexType] = 1 
    ) AS [t5] ON [t1].[ZoneID] = [t5].[ZoneID] 
INNER JOIN ( 
    SELECT DISTINCT [t7].[ZoneID], [t7].[Title], [t7].[FileName], [t7].[CultureID], [t7].[ActiveFrom], [t7].[ActiveTo], [t7].[Status], [t7].[ZonePageID], [t7].[Order], [t7].[ModificationDate], [t7].[ModifiedByName], [t7].[ModifiedByContactGuid], [t7].[PublicationVersion] 
    FROM [Content].[ZonePage] AS [t6] 
    CROSS JOIN ([Content].[ZonePage] AS [t7] 
        INNER JOIN [Version].[ZonePageVersionIndex] AS [t8] ON [t7].[ZonePageID] = [t8].[ZonePageID]) 
    WHERE [t8].[IndexType] = 1 
    ) AS [t9] ON [t1].[ZoneID] = [t9].[ZoneID] 
WHERE ((NOT ([t0].[ParentZoneHierarchyID] IS NOT NULL)) OR (-1 = [t0].[ParentZoneHierarchyID])) AND ([t0].[ActiveFrom] <= '12/09/2009 15:07:24') AND ('12/09/2009 15:07:24' <= [t0].[ActiveTo]) AND ([t5].[ActiveFrom] <= '12/09/2009 15:07:24') AND ('12/09/2009 15:07:24' <= [t5].[ActiveTo]) AND ([t5].[CultureID] = REPLACE(( 
    SELECT [t12].[value] 
    FROM ( 
        SELECT TOP (1) [t11].[value] 
        FROM ( 
            SELECT 
                (CASE 
                    WHEN 0 = 1 THEN CONVERT(NVarChar(10),'1') 
                    WHEN (([t10].[Status] & 4) = 4) AND ([t10].[CultureID] = '') THEN CONVERT(NVarChar(10),'ZZZInv') 
                    WHEN ([t10].[CultureID] = '') OR ([t10].[CultureID] = '') THEN CONVERT(NVarChar(10),REPLACE([t10].[CultureID], ' ', '1')) 
                    WHEN [t10].[CultureID] <> '' THEN CONVERT(NVarChar(10),'0') 
                    ELSE CONVERT(NVarChar(10),'1') 
                 END) AS [value], [t10].[ZoneLocaleID] 
            FROM [Content].[ZoneLocale] AS [t10] 
            ) AS [t11] 
        WHERE [t11].[ZoneLocaleID] = [t5].[ZoneLocaleID] 
        ORDER BY [t11].[value] DESC 
        ) AS [t12] 
    ), '1', '')) AND ([t9].[ActiveFrom] <= '12/09/2009 15:07:24') AND ('12/09/2009 15:07:24' <= [t9].[ActiveTo]) AND ([t9].[CultureID] = REPLACE(( 
    SELECT [t15].[value] 
    FROM ( 
        SELECT TOP (1) [t14].[value] 
        FROM ( 
            SELECT 
                (CASE 
                    WHEN 0 = 1 THEN CONVERT(NVarChar(10),'1') 
                    WHEN (([t13].[Status] & 4) = 4) AND ([t13].[CultureID] = '') THEN CONVERT(NVarChar(10),'ZZZInv') 
                    WHEN ([t13].[CultureID] = '') OR ([t13].[CultureID] = '') THEN CONVERT(NVarChar(10),REPLACE([t13].[CultureID], ' ', '1')) 
                    WHEN [t13].[CultureID] <> '' THEN CONVERT(NVarChar(10),'0') 
                    ELSE CONVERT(NVarChar(10),'1') 
                 END) AS [value], [t13].[ZoneID] 
            FROM [Content].[ZonePage] AS [t13] 
            ) AS [t14] 
        WHERE [t14].[ZoneID] = [t9].[ZoneID] 
        ORDER BY [t14].[value] DESC 
        ) AS [t15] 
    ), '1', '')) 
ORDER BY [t9].[Order], [t9].[Title] 


Observation ?  En modifiant à un seul endroit comment on appliqué les filtres sur les tables, on a modifié dynamiquement des centaines de requêtes, et un seul endroit pour en effectuer la maintenance.  Quel économie d’échelle ! 


 

Mais en toute honnêteté, c’est quand même beaucoup de travail pour aller altérer la requête.  Voici un exemple du travail qui a été accompli pour supporter les nouvelles requêtes de versionages aux bases de données.


    public static IQueryable<T> FilterVersion<T>(this IQueryable<T> t, IVersionIndexFilter version)
      where T : class
    {
      try
      {
        // Défénie le type de la table VersionIndex
        Type versionTableType = Type.GetType(typeof(T).AssemblyQualifiedName.Replace(typeof(T).FullName, string.Concat(typeof(T).FullName, VersionIndexTableExtensionName)));
        
        // Obtient la méthod GetTable typer avec le type de la table versionIndex donc GetTable<versionTableType>
        var getTableType = MethodInfoHelper.GetGenericMethod(typeof(DataContext), "GetTable", new Type[] { versionTableType }, new Type[] { }, typeof(Table<>).MakeGenericType(typeof(Table<>).GetGenericArguments()[0]), BindingFlags.Public | BindingFlags.Instance);
        // Créer un instance de la table de jointure
        var versionIndexTable = getTableType.Invoke(((Table<T>)t).Context, null);
        // NOTE(cboivin): Valide que cette table n'est pas null
        if (versionIndexTable != null)
        {
          // NOTE(cboivin): Récupère le champs de relations entre la table index et la vrai table
          string relationKey = GetTableFilterVersionRelationKey(t as Table<T>);
          // NOTE(cboivin): Défénie le type générique de la table VersionIndex
          var innerTableType = typeof(Table<>).MakeGenericType(versionTableType);
         
          // Défénie une class de conversion
          var genericClassConverter = typeof(ConvertTable<,>).MakeGenericType(typeof(T), versionTableType);
          // NOTE(cboivin): Défénie un paramètre pour accèder à la table primaire
          ParameterExpression inputTableParameter = (ParameterExpression)ParameterExpression.Parameter(typeof(T), "leftTable");
          // NOTE(cboivin): Défénie un paramètre pour accèder à la table de jointure
          ParameterExpression versionIndexTableParameter = (ParameterExpression)ParameterExpression.Parameter(versionTableType, "rigthTable");
          // NOTE(cboivin): Défénie une nouvelle expression qui construit notre class générique typer
          NewExpression genericClassExpression = Expression.New(genericClassConverter.GetConstructor(new Type[] { typeof(T), versionTableType }), new List<Expression>() { inputTableParameter, versionIndexTableParameter }, genericClassConverter.GetProperty("FromT"), genericClassConverter.GetProperty("InnerT"));
          // NOTE(cboivin): Créer un paramètre sur notre class générique
          ParameterExpression genericClassParameter = Expression.Parameter(genericClassExpression.Type, "item");
          // NOTE(cboivin): Défénie un accès sur la propriété FromT de notre class générique
          Expression inputTableMemberAccess = Expression.MakeMemberAccess(genericClassParameter, genericClassConverter.GetProperty("FromT"));
          // NOTE(cboivin): Défénie un accès sur la propriété InnerT de notre class générique
          Expression versionIndexTableMemberAccess = Expression.MakeMemberAccess(genericClassParameter, genericClassConverter.GetProperty("InnerT"));
          // NOTE(cboivin): Défénie la lambda qui donne access au fromT vers la class générique
          Expression fromTOfGenericClassLambda = Expression.Lambda(inputTableMemberAccess, genericClassParameter);
          // NOTE(cboivin): Défénie la propriété GenericFromTAccess
          Expression fromTOfGenericClassEvaluated = Expression.Quote(fromTOfGenericClassLambda);
          // NOTE(cboivin): Défénie la lambda qui contiendra nos deux propritété
          Expression genericClassWithProperty = Expression.Lambda(genericClassExpression, inputTableParameter, versionIndexTableParameter);
          // NOTE(cboivin): Défénie la class généric evaluer en expression
          Expression genericClassWithPropertyEvaluated = Expression.Quote(genericClassWithProperty);
          // NOTE(cboivin): Défénie la propriété sur la table Versionner qui servira à faire le inner join
          MemberExpression versionIndexTableKeyField = Expression.PropertyOrField(versionIndexTableParameter, relationKey);
          // NOTE(cboivin): Défénie la lambda qui relie la table à sa propriété
          Expression versionIndexTableWithKeyPropertyLambda = Expression.Lambda(versionIndexTableKeyField, versionIndexTableParameter);
          // NOTE(cboivin): Evalue l'expression qui contient la table versionner et sa propriété
          UnaryExpression versionIndexTableWithKeyEvaluated = Expression.Quote(versionIndexTableWithKeyPropertyLambda);
          // NOTE(cboivin): Défénie le champs qui servira à faire la relations entre les deux table pour la table d'entrée
          MemberExpression inputTableKeyField = Expression.PropertyOrField(inputTableParameter, relationKey);
          // NOTE(cboivin): Défénie la lambda qui contiendra la table d'entré et le champs de relations
          Expression inputTableWithKeyPropertyLambda = Expression.Lambda(inputTableKeyField, inputTableParameter);
          // NOTE(cboivin): Défénie la table et son champs de propriété qui servira à faire la relations
          UnaryExpression inputTableWithKeyEvaluated = Expression.Quote(inputTableWithKeyPropertyLambda);
          // NOTE(cboivin): Défénie la constante de table d'entrée
          ConstantExpression inputTableIQueryable = (ConstantExpression)Expression.Constant(t);
          // NOTE(cboivin): Défénie la constante de table de version
          ConstantExpression versionIndexTableIQueryable = (ConstantExpression)Expression.Constant(versionIndexTable, innerTableType);
          // NOTE(cboivin): Série d'expression générique pour retourver le method info Join
          var typeEnumerable = typeof(IEnumerable<>);
          var typeExpOuterFunc = typeof(Func<,>);
          var typeExpOuterKey = typeof(Expression<>).MakeGenericType(typeExpOuterFunc).GetGenericTypeDefinition();
          var typeInnerFunc = typeof(Func<,>);
          var typeExpInnerKey = typeof(Expression<>).MakeGenericType(typeInnerFunc).GetGenericTypeDefinition();
          var typeExpResult = typeof(Func<,,>);
          var typeExpResulSelector = typeof(Expression<>).MakeGenericType(typeExpResult).GetGenericTypeDefinition();
          // NOTE(cboivin): Récupère le method info join, et envoie les bon paramètre pour qu'il revienne typer
          var joinMethodInfo = MethodInfoHelper.GetGenericMethod(typeof(Queryable), "Join",
            new Type[] { typeof(T), versionTableType, typeof(int), genericClassConverter },
            new Type[] { typeof(IQueryable<>), typeEnumerable, typeExpInnerKey, typeExpOuterKey, typeExpResulSelector },
            typeof(IQueryable<>).GetGenericTypeDefinition());
          // NOTE(cboivin): Défénie le call expression pour le join
          var joinCallMethodInfo =
            Expression.Call(
            (Expression)null,
            joinMethodInfo,
            inputTableIQueryable,
            versionIndexTableIQueryable,
            inputTableWithKeyEvaluated,
            versionIndexTableWithKeyEvaluated,
            genericClassWithPropertyEvaluated);
          // NOTE(cboivin): Récupère le method info pour le where
          var whereMethodInfo = MethodInfoHelper.GetGenericMethod(typeof(Queryable), "Where", new Type[] { genericClassConverter }, new Type[] { typeof(IQueryable<>), typeof(Expression<>).MakeGenericType(typeof(Func<,>)).GetGenericTypeDefinition() }, typeof(IQueryable<>));
          // NOTE(cboivin): Défénie la valeurs du index type du filter
          Expression enumValue = Expression.Constant(((int)version.IndexType));
          // NOTE(cboivin): Créer un member access sur l'objet InnerT qui est la table versionIndex
          Expression indexTypeMemberAccess = Expression.MakeMemberAccess(versionIndexTableMemberAccess, versionTableType.GetProperty("IndexType"));
          // NOTE(cboivin): Défénie l'expression de comparaison binaire entre le member access et ca valeurs
          Expression indexTypeEqualExpression = BinaryExpression.Equal(indexTypeMemberAccess, enumValue);
          // NOTE(cboivin): Défénie la lambda where == sur la class généric pour l'index type
          Expression whereVersion = Expression.Lambda(indexTypeEqualExpression, genericClassParameter);
          // NOTE(cboivin): Évalue le where
          Expression whereVersionQuote = Expression.Quote(whereVersion);
          // NOTE(cboivin): Expression Call du where
          Expression whereCall = Expression.Call((Expression)null, whereMethodInfo, joinCallMethodInfo, whereVersionQuote);
          // NOTE(cboivin): Récupère le method info select
          var selectMethodInfo = MethodInfoHelper.GetGenericMethod(typeof(Queryable), "Select", new Type[] { typeof(ConvertTable<,>).MakeGenericType(t.GetType().GetGenericArguments()[0], versionTableType), typeof(T) }, new Type[] { typeof(IQueryable<>), typeof(Expression<>).MakeGenericType(typeof(Func<,>)).GetGenericTypeDefinition() }, typeof(IQueryable<>).GetGenericTypeDefinition());
          // NOTE(cboivin): Créer le select expression call
          Expression selectExpressionCall = Expression.Call((Expression)null, selectMethodInfo, whereCall, fromTOfGenericClassEvaluated);
          // NOTE(cboivin): Créer la lambda du select
          Expression<Func<T,IQueryable<T>>> selectLambda = (Expression<Func<T, IQueryable<T>>>)Expression.Lambda(selectExpressionCall, inputTableParameter);
          // NOTE(cboivin): S'assure de sortir les résultat distinctement, en IQueryable<T>
          return (from a in t.Select(selectLambda) select a).SelectMany(i=> i).Distinct();
        }
      }
      catch (Exception ex)
      {
        Console.Write(ex.Message);
      }
      return t;
    }


Alors si vous désirez suivre ce chemin, il y a un bon retour sur l’investissement, mais c’est complexe.  Il faut d’abord maîtriser la réflexivité, les génériques, les delegates lambdas, les méthodes extensions, Linq et les expressions…


Alors attelez-vous, il n’y a pas beaucoup de documentations, et c’est encore très nouveau comme approche.  Mais quel puissance ! ;)

WPF et Silverlight, Binding sur n’importe quel évènement

Par Jean-Sébastien Desfossés, vendredi 4 décembre 2009 08:36
Catégorie : Programmation

Avec le modèle MVVM, les commandes sont situées dans le ViewModel avec les propriétés. La façon de faire est de faire un Binding sur la propriété « Command » d’un contrôle. Mais qu’est-ce qu’on fait si on veut utiliser un autre évènement que celui qui est attitré à la propriété Command? Par exemple, sur un bouton la propriété « Command » est liée à « Click ». Si on veut utiliser par exemple MouseMove, est-on obligé de le faire dans le CodeBehind du UI? Et bien la réponse est : « Non ». On peut faire les choses dans les règles de l’art de MVVM. C’est ce qui est décrit dans ce qui suit.

 

Voici comment il faut faire en WPF.

Premièrement, il faut créer une Classe nommée CommandAction qui dérive de TargetTriggerAction<FrameworkElement> et que ICommandSource.

Voici le code de la classe :

public class CommandAction 
: TargetedTriggerAction<FrameworkElement>, ICommandSource
{
[Category("Command Properties")]
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}

public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command", typeof(ICommand), typeof(CommandAction),
new PropertyMetadata(
(ICommand)null, OnCommandChanged));

private static void OnCommandChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var action = (CommandAction)d;
action.OnCommandChanged((ICommand)e.OldValue, (ICommand)e.NewValue);
}
private EventHandler CanExecuteChangedHandler;
private void OnCommandChanged(ICommand oldCommand, ICommand newCommand)
{
if (oldCommand != null)
UnhookCommand(oldCommand);
if (newCommand != null)
HookCommand(newCommand);
}
private void UnhookCommand(ICommand command)
{
command.CanExecuteChanged -= CanExecuteChangedHandler;
UpdateCanExecute();
}
private void HookCommand(ICommand command)
{
CanExecuteChangedHandler = new EventHandler(OnCanExecuteChanged);
command.CanExecuteChanged += CanExecuteChangedHandler;
UpdateCanExecute();
}
private void OnCanExecuteChanged(object sender, EventArgs e)
{
UpdateCanExecute();
}
private void UpdateCanExecute()
{
if (Command != null)
{
RoutedCommand command = Command as RoutedCommand;
if (command != null)
IsEnabled = command.CanExecute(CommandParameter, CommandTarget);
else
IsEnabled = Command.CanExecute(CommandParameter);
if (Target != null && SyncOwnerIsEnabled)
Target.IsEnabled = IsEnabled;
}
}
[Category("Command Properties")]
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.Register(
"CommandParameter", typeof(object), typeof(CommandAction),
new PropertyMetadata());

[Category("Command Properties")]
public IInputElement CommandTarget
{
get { return (IInputElement)GetValue(CommandTargetProperty); }
set { SetValue(CommandTargetProperty, value); }
}

public static readonly DependencyProperty CommandTargetProperty =
DependencyProperty.Register(
"CommandTarget", typeof(IInputElement), typeof(CommandAction),
new PropertyMetadata());

[Category("Command Properties")]
public bool SyncOwnerIsEnabled
{
get { return (bool)GetValue(SyncOwnerIsEnabledProperty); }
set { SetValue(SyncOwnerIsEnabledProperty, value); }
}

public static readonly DependencyProperty SyncOwnerIsEnabledProperty =
DependencyProperty.Register(
"SyncOwnerIsEnabled", typeof(bool), typeof(CommandAction),
new PropertyMetadata());

protected override void Invoke(object o)
{
if (Command != null)
{
var command = Command as RoutedCommand;
if (command != null)
command.Execute(CommandParameter, CommandTarget);
else
Command.Execute(CommandParameter);
}
}
}

Deuxièmement il faut une référence sur Interactivity

xmlns:interactivity="clr-namespace:Microsoft.Expression.Interactivity;
assembly=Microsoft.Expression.Interactivity"

Finalement, voici comment l’intégrer

<Button>
<interactivity:Interaction.Triggers>
<interactivity:EventTrigger EventName="MouseMove">
<local:CommandAction Command="{Binding OnMouseMoveCommand}"
SyncOwnerIsEnabled="True"/>
</interactivity:EventTrigger>
</interactivity:Interaction.Triggers>
</Button>


 

(Source : http://jacokarsten.wordpress.com/2009/03/27/applying-command-binding-to-any-control-and-any-event/)

 

Voici comment il faut faire en Silverlight.

L’approche précédente est très bonne mais ne fonctionne pas en Silverlight car les EventTrigger ne sont pas disponibles. Par conséquent, voici l’approche que je suggère.

 

Premièrement, on crée une AttachedProperty qui sera distribuée à tous les enfants d’un « Container ». Dans mon exemple j’ai pris un « Grid » mais d’autres logiques pourraient être utilisées.

Exemple de l’AttachedProperty

public class AdvancedBindingGrid : Grid
{
public AdvancedBindingGrid(): base()
{
this.Loaded += new RoutedEventHandler(AdvancedBindingGrid_Loaded);
}

private void AdvancedBindingGrid_Loaded(object sender, RoutedEventArgs e)
{
if (!DesignerProperties.GetIsInDesignMode(this))
{
foreach (Control c in Children)
{
ICommand bindedCommand = AdvancedBindingGrid.GetMouseMoveCommand(c);
c.MouseMove += new MouseEventHandler(((RelayCommand)bindedCommand).HandlerExecute);
}
}
}

public static readonly DependencyProperty MouseMoveCommandProperty = DependencyProperty.RegisterAttached("MouseMoveCommand", typeof(ICommand), typeof(AdvancedBindingGrid), new PropertyMetadata(null));

public static void SetMouseMoveCommand(DependencyObject element, ICommand value)
{
element.SetValue(MouseMoveCommandProperty, value);
}

public static ICommand GetMouseMoveCommand(DependencyObject element)
{
return (ICommand)element.GetValue(MouseMoveCommandProperty);
}
}

Notez qu’ici il y a un RelayCommand qui devrait être remplacé par la classe utilisée dans votre projet. DesignerProperties sert peut que le tout compile en Design.  Finalement, l’approche ici place des propriétés fixent pour chaque évènement à prendre en charge.  Il serait possible de changer cette approche pour une collection et spécifier le nom de l’évènement.

Finalement, on intègre l’AttachedProperty:

<p:AdvancedBindingGrid>
<Button p:AdvancedBindingGrid.MouseMoveCommand="{Binding MouseModeCommand}" />
</p:AdvancedBindingGrid>

Voilà! Maintenant on peut placer un Binding sur MouseMove

Localisation des en-têtes de grille

Par Éric Sylvestre, vendredi 27 novembre 2009 14:59
Catégorie : Programmation

La localisation est une notion de plus en plus importante dans le développement d’applications. Il n’est pas toujours évident d’implanter cet aspect dans certaines situations. Cependant, dans le but de rendre la localisation la plus flexible possible, voici une solution pour donner beaucoup de liberté au programmeur. Le scénario présenté est celui de vouloir changer l’en-tête d’une colonne dans une grille. Pour les besoins de la cause, nous utiliserons l’objet XamDataGrid d’Infragistics.

 

La solution drastique pour résoudre ce problème serait de redéfinir un style complet pour la grille afin d’établir le binding sur l’en-tête des colonnes.

Une autre solution serait de créer le binding dans le « code behind » pour atteindre la propriété de l’en-tête.

Il existe cependant une autre solution. La grille d’Infragistics supporte l’attribut « DisplayName » sur une propriété.

[DisplayName(“Nom”)]
public string Name {get; set;}

On atteint rapidement la limite avec cette procédure, car on doit absolument donner une constante comme valeur, et non faire référence à une méthode, variable ou propriété. Voici cependant un moyen de localiser les propriétés.

 

 

1. Création d’un DisplayNameAttribute custom. On pourra ainsi ajouter les éléments manquants sur l’attribut.

/// <summary>
/// Classe qui prend la place du DisplayAttribute par défaut. On peut ainsi mettre un fichier de resource et une 
/// clé pour aller chercher la string désirée.
/// </summary>
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public class DisplayNameAttribute : System.ComponentModel.DisplayNameAttribute
{
 
  #region Fields
  /// <summary>
  /// Nom du fichier de ressources (sans extension) à partir de la racine du projet qui possède le fichier
  /// </summary>
  private string _resourceFile { get; set; }
 
  /// <summary>
  /// Clé dans le fichier de ressources
  /// </summary>
  private string _key { get; set; }
  #endregion Fields
 
  #region Constructors
  /// <summary>
  /// Constructeur par défaut
  /// </summary>
  public DisplayNameAttribute()
    : base()
  {
 
  }
 
  /// <summary>
  ///   Constructeur nécessaire pour associer le display name correction lors de l'affichage de la propriété.
  ///   Par défault, ce sera le nom de la propriété qui sera affichée.
  /// </summary>
  /// <param name="resourceFile">
  ///   Nom du fichier (sans extention) de ressources  à partir de la racine du projet qui possède le fichier 
  ///   où on doit aller chercher la clé.
  ///   Ex : "ResourcesDirectory.MyRessourceFile"
  /// </param>
  /// <param name="key">
  ///   Clé dans le fichier de ressources.
  ///   Ex : "FirstName"
  /// </param>
  public DisplayNameAttribute(string resourceFile, string key)
    : this()
  {
    _resourceFile = resourceFile;
    _key = key;
  }
  #endregion Constructors
 
  #region Properties
  /// <summary>
  /// Propriété qui donne le nom d'affichage de la propriété
  /// </summary>
  public override string DisplayName
  {
    get
    {
      return ResourceHelper.GetStringFromResourceFile(_resourceFile, _key);
    }
  }
  #endregion Properties
 
  #region Methods
 
  #endregion Methods
 
}

 

Un avantage très intéressant est que la recherche du fichier de ressources se fait à partir du projet qui consulte la propriété. C’est donc dire que votre classe d’entité qui se trouve dans votre couche d’application peut quand même faire appel au fichier qui se trouve dans votre couche de présentation, là où les ressources qui sont affichées se trouvent.

 

 

2. Création d’un Helper.

/// <summary>
/// Aide à obtenir des valeurs provenant des fichiers de ressources par réflection
/// </summary>
public static class ResourceHelper
{
  #region Fields
  /// <summary>
  /// Cache pour éviter de recharger le ResourceManager à chaque fois 
  /// </summary>
  private static Dictionary<string, ResourceManager> _resourceManagerCache = new Dictionary<string, ResourceManager>();
  #endregion Fields
 
  #region Methods
  /// <summary>
  /// Méthode qui va dans le fichier de resource indiqué pour aller chercher la string (via la clé) désiré
  /// </summary>
  /// <param name="resourceFile">
  ///   Nom du fichier (sans extention) de ressources où on doit aller chercher la clé.
  ///   Ex : "MyRessourceFile"
  /// </param>
  /// <param name="key">
  ///   Clé dans le fichier de ressources.
  ///   Ex : "FirstName"
  /// </param>
  /// <returns>La string demandée</returns>
  public static string GetStringFromResourceFile(string resourceFile, string key)
  {
    try
    {
      //Assembly qui a fait appel à l'attribut
      Assembly a = Assembly.GetEntryAssembly();
 
      //Namespace complet du fichier de ressrouces
      string resourceFullNamespace = a.GetName().Name + "." + resourceFile + ".resources";
 
      //Si on a déjà chargé ce fichier, on évite de le recharger
      if (_resourceManagerCache.ContainsKey(resourceFullNamespace))
      {
        return _resourceManagerCache[resourceFullNamespace].GetString(key);
      }
      else
      {
        //Sinon, on fait le chargement initial
        //--------------------------------------
 
        //Liste de toutes les ressources de l'assembly
        string[] allResxFiles = a.GetManifestResourceNames();
 
        //Établit le bon fichier avec le bon namespace
        string fullPath = allResxFiles.Where(x => String.Equals(x,
                                                                resourceFullNamespace,
                                                                StringComparison.InvariantCultureIgnoreCase)
                                                                ).FirstOrDefault();
 
        //Enlève l'extension du fichier (ne doit pas être présent pour créer le ResourceManager)
        fullPath = fullPath.Replace(".resources", String.Empty);
 
        //Manager de ressources pour le fichier demandé
        ResourceManager rm = new ResourceManager(fullPath, a);
 
        //On l'ajoute à notre cache
        _resourceManagerCache.Add(resourceFullNamespace, rm);
 
        //Retourne la clé si elle est trouvé dans le dictionnaire, sinon cela retourne une string vide
        return rm.GetString(key);
      }
    }
    catch (Exception)
    {
      //En cas d'erreur, on ne fait que retourner une string vide
      return String.Empty;
    }
  }
  #endregion Methods
}

 

3. Exemple d’utilisation

-> Découpage des classes

clip_image002

-> Classe : Persons

public class Person
{
  [DisplayNameAttribute("Resources.MyResource", "ID")]
  public int Id { get; set; }
 
  [DisplayNameAttribute("Resources.MyResource", "Title")]
  public string Title { get; set; }
 
  [DisplayNameAttribute("Resources.MyResource", "AdultAge")]
  public bool IsAdult { get; set; }
 
  [DisplayNameAttribute("Resources.MyResource", "Name")]
  public string Name { get; set; }
 
  public Person()
  {
 
  }
 
  public Person(int id, string title, bool isAdult, string name)
  {
    Id = id;
    Title = title;
    IsAdult = isAdult;
    Name = name;
  }
}

-> Window1.xaml

 

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    xmlns:igData="http://infragistics.com/DataPresenter"
    Title="Window1" Width="Auto" Height="Auto">
  <StackPanel>
    <igData:XamDataGrid DataSource="{Binding Persons}" />
  </StackPanel>
</Window>

 

-> Window1.xaml.cs

public partial class Window1 : Window
{
  //Variable privée
  private ObservableCollection<Person> _persons = null;
 
  //Liste des personnes
  public ObservableCollection<Person> Persons
  {
    get { return _persons; }
    set { _persons = value; }
  }
 
  public Window1()
  {
    InitializeComponent();
    this.DataContext = this;
 
    _persons = new ObservableCollection<Person>()
    {
      new Person(1, "Dr.", true, "Jack"),
      new Person(2, "Mme.", true, "Rita"),
      new Person(3, "Jr", false, "Dumbo")
    };
 
  }
 
}

 

-> Résultat

clip_image003

Finalement, beaucoup de contrôles supportent par défaut la propriété « DisplayName ». Avec cette petite solution, vous serez en mesure d’avoir le texte désiré pour l’affichage de vos propriétés.

Propriété MaxLength par l'attribut de validation

Par Jean-Sébastien Desfossés, vendredi 27 novembre 2009 10:44
Catégorie : Programmation

Bonjour,

Lorsqu'on met l'attribut de validation StringLength sur une propriété qui est liée à un TextBox, on peut désirer que la propriété MaxLength du TextBox soit automatiquement placée à cette valeur.  De cette façon, au lieu d'avoir un message d'erreur lorsque le nombre de caractères excède le maximum, l'usager ne peut tout simplement pas entrer plus de caractères que le maximum.

 

Pour arriver à intégrer cette logique, il n'y a pas beaucoup de points d'entrées.  Un Binding custom n'est pas chargé avec la source via le constructeur, et on n’a pas accès au TextBox au niveau de l'entité.  On ne peut pas non plus retirer le dernier caractère au nouveau de l'entité car lorsqu'un caractère serait ajouter ailleurs qu'à la fin, c'est le dernier caractère de la chaine qui serait retiré (à moins d'avoir un buffer mais ce n'est pas très propre).

 

Alors voici la solution qui a été retenu.  On appel un Converter qui s'occupe de faire la mise à jour de la propriété MaxLength.  Au niveau du Converter nous avons accès à toute l'information dont on a besoin.

 

Voici le code du Converter

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is TextBox)
{
((TextBox)value).DataContextChanged += new System.Windows.DependencyPropertyChangedEventHandler(MaxLengthConverter_DataContextChanged);
return ((TextBox)value).MaxLength;
}
return null;
}

private void MaxLengthConverter_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
{
TextBox currentTextBoxInConversion = (TextBox)sender;
Binding textBoxTextPropertyBinding = BindingOperations.GetBinding(currentTextBoxInConversion, TextBox.TextProperty);
if (textBoxTextPropertyBinding != null && e.NewValue != null)
{
PropertyInfo propertyBoundToText = e.NewValue.GetType().GetProperty(textBoxTextPropertyBinding.Path.Path);
if (propertyBoundToText != null)
{
object[] foundValidationStringLengthAttributes = propertyBoundToText.GetCustomAttributes(typeof(ValidationStringLengthAttribute), true);
if (foundValidationStringLengthAttributes != null && foundValidationStringLengthAttributes.Count() > 0)
{
int maxLength = ((ValidationStringLengthAttribute)foundValidationStringLengthAttributes[0]).Max;
currentTextBoxInConversion.MaxLength = maxLength;
}
}
}
}

Voici comment il devrait être intégré.

<xxxxxx.Resources>
<local:AutoMaxLengthConverter x:Key="MaxLengthConverter"/>
<Style TargetType="TextBox">
<Setter Property="MaxLength"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource MaxLengthConverter}}"
>
</Setter>
</Style>
</xxxxxx.Resources>

Dans l'exemple ici, aucune clef n'est spécifiée donc tous les TextBox seront affectés.  Ajouté une clef permettrait de ciblé mais empêcherait un lien à d'autre Style.  On peut également l'ajouter à un Style qui serait déjà utilisé par les TextBox.  En gros, on fait comme on veut.

La grille d’Infragistics: PropertyChanged

Par Jean-Sébastien Desfossés, mercredi 25 novembre 2009 09:42
Catégorie : Programmation

Résumé
La grille d’Infragistics est une des plus performantes et complètes sur le marché.  Il est donc normal que plusieurs de nos clients l’utilisent. Le seul inconvénient c’est que pour l’intégration à WPF, Infragistics prend en charge le « Binding » et empêche souvent les stratégies standards MVVM. Ce qui suit démontre une problématique courante ainsi que la façon de régler le problème.

Problématique
Pour charger la grille, on fait le « Binding » avec l’entité. La grille se charge ensuite automatiquement ou selon un format désiré. Par la suite, il est impossible de mettre le « Binding » en mode « PropertyChanged ». Des fois on préfèrerait une logique de « PropertyChanged » et d’autres fois c’est essentiel. Par exemple, lorsqu’une propriété Booléenne de l’entité est liée, la grille affiche une case à cocher (CheckBox). Comme la propriété n’est pas en mode PropertyChanged, il faut cliquer ailleurs après avoir coché l’information pour que la modification soit transmise à l’entité.

Solution

Dans le « Xaml » qui contient le contrôle de la grille, il faut enregistrer l’évènement « CellChanged ».

 


<igDP:XamDataGrid Name="grdExample" CellChanged="grdExample_CellChanged">

 

Ensuite dans le code, de la page, sous la méthode CellChanged, on place les lignes suivantes :

private void grdExample_CellChanged(object sender, CellChangedEventArgs e)
{
grdExample.ExecuteCommand(DataPresenterCommands.EndEditModeAndAcceptChanges);
grdExample.ExecuteCommand(DataPresenterCommands.StartEditMode);
}

(Note : L’exemple se base sur la version 9.1 ou 9.2)

WPF et Model-View-ViewModel; une introduction

Par Patrick Bélanger, vendredi 6 novembre 2009 13:42
Catégorie : Programmation

Depuis quelque temps, les développements fait sous Windows Presentation Fondation (WPF) chez Nmedia s’enlignent avec le design pattern Model-View-ViewModel (M-V-VM).

 

C’est un modèle très prometteur pour WPF.  Model-View-Controller (MVC) ou Model-View-Presenter (MVP) sont envisageable, mais M-V-VM s’intègre plus naturellement à la plateforme WPF. En s’y conformant, on fait ressortir beaucoup de ses éléments forts dont le Binding, les commandes, les Ressources, la notification de changements, la validations des erreurs, etc. D’ailleurs, c’est le pattern qui semble être utilisé à l’interne par Microsoft pour la majorité des projets WPF (comme la gamme des produits Microsoft Expression).

 

C’est aussi un design pattern utilisable pour les développements Silverlight, mais comme on peut considérer Silverlight un sous-ensemble de WPF (comme son nom de code WPF/e le suggérait) disons que c’est aussi un sous ensemble de M-V-VM.

 

Bref, le découpage est clean et repose sur les forces de WPF.  Et pour vous permettre d’en découvrir davantage, suivez les liens !

 

Une petite introduction tirée du merveilleux site Channel 9 :
http://channel9.msdn.com/shows/Continuum/MVVM/

Un article tiré du MSDN Magasine :
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Le graphique de survol du modèle M-V-VM est très bien pour quelqu'un qui commence :
http://www.orbifold.net/default/?p=550

Blog avec pas grand-chose …
http://blog.lab49.com/archives/2650

… mais qui a ce super vidéo de saissie d'écran dedans :
http://www.lab49.com/files/videos/Jason%20Dolinger%20MVVM.wmv

Model-View-ViewModel Pattern for WPF: Yet another approach
http://www.acceptedeclectic.com/2008/01/model-view-viewmodel-pattern-for-wpf.html

Aide au binding

DataBinding hiérarchique
http://msdn.microsoft.com/en-us/magazine/cc700358.aspx

Aide dans le data binding
http://dotnet.org.za/rudi/archive/2008/03/25/10-things-i-didn-t-know-about-wpf-data-binding.aspx

WPF Binding Cheat Sheet
http://www.nbdtech.com/Free/WpfBinding.pdf

Et encore du binding…
http://blogs.codes-sources.com/fredhamel/archive/2008/06/01/wpf-data-binding-quick-reference.aspx

Wildcards en jQuery

Par Vincent Beaulieu, mercredi 4 novembre 2009 11:27
Catégorie : Programmation

Une des grandes forces de jQuery réside dans ses sélecteurs qui nous permettent de différentes facons d'aller chercher un élément html dans une page que ce soit en passant par son id, son name, son type ou sa class. Il arrive cependant en .Net qu'une partie du id soit automatiquement généré, ce qui peut rendre la tâche de récupérer l'élément plus complexe.

 

Voici donc la liste des Wildcards jQuery qui nous permettraient d'aller chercher un ou plusieurs éléments avec seulement une partie du mot à rechercher.

 

$("input[@id*='keyword']) 

Retourne les éléments de type input dont le id contient le mot 'keyword'
(retournerais <input id='un_keyword_ici' /> <input id='keyword_ici' />  <input id='un_keyword' />)

 

 

$("input[@id^='keyword'])

Retourne les éléments de type input de le id commence par 'keyword'
(retournerais <input id='keyword_ici' /> )

 

 

$("input[@id&='keyword'])

Retourne les éléments de type input de le id se termine par 'keyword'
(retournerais <input id='un_keyword' /> )

WPF et utilisation des MarkupExtension

Par Jean-Sébastien Desfossés, mercredi 4 novembre 2009 09:48
Catégorie : Programmation

RÉSUMÉ
La classe MarkupExtension sert à retourner une valeur pour une propriété d’un objet en XAML. Dans l’interface en XAML, on peut mettre une propriété égale à quelque chose qui dérive de MarkupExtension au lieu de lui assigner une valeur. Par exemple, la propriété Text d’un TextBox à laquelle on assigne normalement une chaine de caractères ou bien une ressource statique peut à la place être assignée à une classe qui dérive de MarkupExtension. D’ailleurs, la classe Binding peut être assignée à une propriété en XAML à cause que BindingBase dérive de MarkupExtension.

 

QUAND L’UTILISER
Généralement, la classe Binding est la meilleure façon d’assigner une valeur à une propriété qui est dépendante d’un context. Pour des opérations plus complexe on utiliser les Converter avec le Binding. Si on a besoin d’encore plus de liberté on a accès la clases MarkupExtension.

 

UTILISATION SIMPLE
Lorsqu’on fait une classe qui dérive de MarkupExtension, on doit implémenter la méthode ProvideValue. Dans ProvideValue, un IServiceProvider est passé qui fourni le DependencyObject et la DependencyProperty sur laquelle a été assigné le MarkupExtention. Ce qui en gros permet d’avoir un contrôle total sur l’objet. Voici un exemple simple d’utilisation des MarkupExtension :

XAML

<TextBox Text="{fb:MyExt ExVal=Hi}"

CODE

public class MyExt : MarkupExtension
{
  public string ExVal { get; set; }
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    return ExVal;
  }
}

UTILISATION CONCRETE
Dans cet exemple, le but est de sécuriser l’édition d’un champ à l’aide de la propriété IsEnabled. Les informations de sécurité proviennent de la base de données. La sécurité dépend également de l’état du formulaire; par exemple, on veut qu’on utilisateur puisse entrer de l’information si le formulaire est en mode « nouveau » et restreindre l’accès lorsqu’il est en mode « Édition. » De plus, l’information n’est pas présente dans le « ViewModel »

XAML

<TextBox>
  <TextBox.IsEnabled>
    <fb:SecurityMarkupExtension 
            SecuriyState="Searchable, CanEditNew" 
            SecurityCodeSearch="Code4302" 
            SecurityCodeEdit="Code4303" 
            SecurityCodeEditNew="Code4304"/>
  </TextBox.IsEnabled>
</TextBox>

CODE: MarkupExtension

public class SecurityMarkupExtension : MarkupExtension
  {
    public SecurityMarkupExtension()
    {
      InitializeProperties();
    }
 
    public SecutityStateEnum SecuriyState { get; set; }
 
    public string SecurityCodeSearch { get; set; }
    public string SecurityCodeEdit { get; set; }
    public string SecurityCodeEditNew { get; set; }
 
    private void InitializeProperties()
    {
      SecuriyState = SecutityStateEnum.None;
    }
 
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
      IProvideValueTarget valueProvider = (IProvideValueTarget)serviceProvider;
      Binding bind = new Binding();
      bind.ConverterParameter = this;
      bind.Converter = SecurityBindingConverter.Instance;
      bind.Source = ApplicationStatusHandler.Instance;
      bind.Path = new System.Windows.PropertyPath("ApplicationState");
      bind.Mode = BindingMode.TwoWay;
 
      if (valueProvider.TargetObject is DependencyObject && valueProvider.TargetProperty is DependencyProperty)
      {
        if (!DesignerProperties.GetIsInDesignMode((DependencyObject)valueProvider.TargetObject))
        {
          BindingOperations.SetBinding((DependencyObject)valueProvider.TargetObject, (DependencyProperty)valueProvider.TargetProperty, bind);
          return valueProvider.TargetObject.GetType().GetProperty(valueProvider.TargetProperty.ToString()).GetValue(valueProvider.TargetObject, null);
        }
        return false;
      }
      else
      {
        return ((DependencyProperty)valueProvider.TargetProperty).DefaultMetadata.DefaultValue;
      }
    }

CODE: Converter

 

 

 

 

public class SecurityBindingConverter : IValueConverter
  {
    private static SecurityBindingConverter _instance;
    private static readonly object _padlock = new object();
    public static SecurityBindingConverter Instance
    {
      get
      {
        lock (_padlock)
        {
          if (_instance == null)
          {
            _instance = new SecurityBindingConverter();
          }
          return _instance;
        }
      }
    }
 
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
      if (parameter is SecurityMarkupExtension)
      {
        bool returnValue = false;
        SecurityMarkupExtension param = ((SecurityMarkupExtension)parameter);
 
        switch ((ApplicationStateEnum)value)
        {
          case ApplicationStateEnum.Search:
            if ((param.SecuriyState & SecutityStateEnum.Searchable) == SecutityStateEnum.Searchable)
            {
              returnValue = SecurityHandler.IsUserAllowed(param.SecurityCodeSearch);
            }
            else
            {
              returnValue = false;
            }
            break;
          case ApplicationStateEnum.Edition:
            if ((param.SecuriyState & SecutityStateEnum.Editable) == SecutityStateEnum.Editable)
            {
              returnValue = SecurityHandler.IsUserAllowed(param.SecurityCodeEdit);
            }
            else
            {
              returnValue = false;
            }
            break;
          case ApplicationStateEnum.Creation:
            if ((param.SecuriyState & SecutityStateEnum.CanEditNew) == SecutityStateEnum.CanEditNew)
            {
              returnValue = SecurityHandler.IsUserAllowed(param.SecurityCodeEditNew);
            }
            else
            {
              returnValue = false;
            }
            break;
        }
        return returnValue;
      }
      else
      {
        throw new Exception("SecurityBindingConverter a besoin d'un paramètre de type SecurityBinding");
      }
    }

Expérimentation BAML

Par Éric Sylvestre, mardi 3 novembre 2009 08:30
Catégorie : Programmation

INTRODUCTION

En WPF, les fichiers BAML (BinaryApplication Markup Language) sont le résultat de la compilation d’un fichier XAML. Nous verrons dans cet article comment faire la gestion des ressources à l’aide de ces fichiers.

 

Tout d’abord, présentons un peu plus les fichiers BAML et leur utilité. Ils sont situés dans le dossier obj\Debug de votre application, où chaque contrôle XAML en possèdera un. Pour visualiser le contenu de ces fichiers, voici les étapes à suivre.

 

1. Installation du Red Gate’s .Net Reflector . Il est l’un des outils les plus connus dans le milieu pour voir le contenu et explorer les assemblies .Net.

2. Installation de l’add-in BAML Viewer pour le .Net Reflector. C’est ce qui fera en sorte que vous serez en mesure de visualiser le contenu des fichiers à partir du Reflector.

3. Dans cet article, nous utiliserons une petite application nommée myPiggyBank pour faire la démonstration. Voici une capture d’écran de la vue proposée par BAML Viewer.

image 
Figure 1 Capture d'écran du .Net Reflector et du BAML Viewer sur un fichier BAML

 

APPROCHE POUR TRADUIRE VOTRE APPLICATION

Pour traduire votre application sans la retoucher à l’aide des fichiers BAML, voici la marche à suivre :

1. Ajout dans le fichier .csproj de votre projet la ligne suivante :

<UICulture>fr-CA</UICulture>

image

Figure 2 Ligne à ajouter dans le fichier .csproj

2. Enlever le commentaire sur la ligne suivante dans le fichier AssemblyInfo.cs du projet :

[assembly: NeutralResourcesLanguage("fr-CA", UltimateResourceFallbackLocation.Satellite)]

 

3. Il faut alors assigner un identifiant unique pour tous les contrôles. Cela peut se faire à la main et être long et le risque d’erreur est élevé. Il existe cependant une façon rapide de tout faire générer à la main via msbuild. Pour ce faire, allez dans votre menu Démarrer > Visual Studio 2008 > Visual Studio Tools > Visual Studio 2008 Command prompt  et entrez la ligne de commande suivante :

msbuild /t:updateuid myPiggyBank.App.csproj

* Note : Ce processus doit être fait à chaque ajout / retrait des contrôles de votre application. Une bonne pratique serait de mettre la commande dans les commandes à exécuter lors de la compilation.

4. Pour extraire les ressources de votre application, il faut alors télécharger l’utilitaire LocBaml.exe vers le dossier obj\Debug du projet. En ligne de commandes, utilisez la ligne suivante :

LocBaml /parse myPiggyBank.App.g.fr-cA.resources /out:output.csv

5. Vous pouvez maintenant modifier vos ressources sans problème. Une fois ces changements complétés, il faut générer un fichier localisé.

LocBaml /generate /trans:output.csv myPiggyBank.App.g.fr-CA.resources /out:. /cul:en-CA

 

6. Il faut maintenant compiler l’assembly.

Al.exe /out:myPiggyBank.resources.dll /cultures:en-CA /embed:myPiggyBank.App.g.fr-CA.resources


Voilà, votre application est disponible avec les ressources en en-CA. Il ne vous reste qu’à créer un dossier en-CA dans le répertoire bin\Debug de votre projet.

 

Références

http://blogs.msdn.com/jimoneil/archive/2009/01/12/b-is-for-baml.aspx

http://www.codeproject.com/KB/WPF/WPFUsingLocbaml.aspx

http://msdn.microsoft.com/en-us/library/ms754231(VS.100).aspx