Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Le Blog de Carton Jerome

SharePoint Consultant

[Project Server] Decrypter le Transaction Comments par code

Suite à l'article de Molnar Tibor sur Project Server 2007, j'ai voulu pousser les recherches un peu plus loin. En effet, je souhaitais obtenir les informations d'un assignement. Soit, le détail de chaque modification : les champs modifiés, les commentaires, le statut. Ce qui correspond aux informations listées par PWA dans l'écran ci-dessous :

L'option base de données aurait pu suffir, en effet, un simple select sur la bonne table et le tour est joué. Mais pas de chance, Microsoft stocke ces informations dans un champ binaire donc, indécryptable. C'est pourquoi, j'ai du suivre la piste proposé par Molnar Tibor.

Voici le code pour la partie initialisation :

Avant toute chose, je déclare chacun des types de données dont je vais avoir besoin. J'ai mis en commentaire dans quelle classe il se trouve :
//Classes
Type changeLogType;
//Type of class ChangeLog from Microsoft.Office.Project.SvrDataEdit
Type changeType;
//Type of class Change from Microsoft.Office.Project.SvrDataEdit
Type statusingType;
//Type of class Statusing from Microsoft.Office.Project.Server.BusinessLayer
Type securityType;
//Type of class Security from Microsoft.Office.Project.Server.BusinessLayer
Type PlatformContextType;
//Type of class PlatformContext from Microsoft.Office.Project.Server.Base
Type DalDataAccessType;
//Type of class DalDataAccess from Microsoft.Office.Project.DataEdit.Dal
Type IAssnDataType;
//Type of interface IAssnData from Microsoft.Office.Project.SvrDataEdit.IAssnData
Type AssnDataType;
//Type of class AssnData from Microsoft.Office.Project.SvrDataEdit.AssnData
Type ProjDataRowType;
//Type of class ProjDataRow from Microsoft.Office.Project.DataEdit.Dal.ProjDataRow
Type StatusingDalBaseType;
//Type of class StatusingDalBase from Microsoft.Office.Project.Server.DataAccessLayer.Generated.StatusingDalBase
Type DALType;
//Type of class DAL from Microsoft.Office.Project.Server.DataAccessLayer.DAL

Maintenant je déclare toutes les méthodes dont je vais avoir besoin, en commentaire le prototype de la fonction :
//Methods
MethodInfo FromBytesMethod;
//public static ChangeLog FromBytes(ByteArray bytes) method from class ChangeLog
MethodInfo FillStatusApprovalDetailsMethod;
//private void FillStatusApprovalDetails(StatusTransactionDataSet updates, DataTable changedFields)
MethodInfo ReadAssignmentHistoryMethod;
//public AssnHistoryDataSet ReadAssignmentHistory(Guid itemid, AssnHistoryItemType itemtype) method from class Statusing
MethodInfo ReadStatusApprovalDetailsMethod;
//public StatusApprovalTransactionDetailsDataSet ReadStatusApprovalDetails(Guid assignmentID);
MethodInfo ReadTransactionDetailsMethod; //public StatusApprovalTransactionDetailsDataSet ReadTransactionDetails(Guid updateID)

MethodInfo ReadStatusApprovalsInProcessMethod;
//public StatusApprovalDataSet ReadStatusApprovalsInProcess(Guid userID)
MethodInfo ReadStatusMethod;
//public StatusingDataSet ReadStatus(Guid assnid, DateTime mindate, DateTime maxdate)
MethodInfo FillAssignmentDataMethod;
//public static bool FillAssignmentData(IAssnData data, Guid guidAssn, Guid guidProj, ProjDataRow dr, int valueIndex, bool getSRA, bool useSavedStore)
MethodInfo ReadAssignmentsForApprovalGridMethod;
//public DataSet ReadAssignmentsForApprovalGrid(string ASSN_UID_LIST)
MethodInfo GetSecurityWorkerObjectMethod; //private Microsoft.Office.Project.Server.BusinessLayer.Security GetSecurityWorkerObject()

MethodInfo CheckAssignmentAccessMethod;
//public bool CheckAssignmentAccess(Guid assnGuid)

Ensuite, la déclaration des constructeurs pour chaque classe, en commentaire le prototype de la méthode :
//Constructors
ConstructorInfo PlatformContextConstructor;
//public PlatformContext(Guid siteId, SPUrlZone urlZone) constructor from public PlatformContext
ConstructorInfo StatusingConstructor;
//public Statusing(PlatformContext context) : base(context) constructor from Statusing
ConstructorInfo AssnDataConstructor;
//Default constructor
ConstructorInfo ProjDataRowConstructor;
//public ProjDataRow(DataRow dataRow)
ConstructorInfo PlatformContextConstructor2;
//public PlatformContext(bool isWindowsUser, string username, Guid userId, Guid trackingId, Guid siteId, SPUrlZone urlZone, string lcid)

Maintenant, le plus important, il faut charger l'assembly de Microsoft :
AssemblyName assemblyName = new AssemblyName("Microsoft.Office.Project.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
Assembly assembly = Assembly.Load(assemblyName);

Une fois l'assembly chargé, je peux récupérer les types de donnés dont je vais avoir besoin :
//Get types
changeLogType = assembly.GetType("Microsoft.Office.Project.SvrDataEdit.ChangeLog"
);
changeType = assembly.GetType(
"Microsoft.Office.Project.SvrDataEdit.Change"
);
statusingType = assembly.GetType(
"Microsoft.Office.Project.Server.BusinessLayer.Statusing"
);
securityType = assembly.GetType(
"Microsoft.Office.Project.Server.BusinessLayer.Security"
);
PlatformContextType = assembly.GetType(
"Microsoft.Office.Project.Server.Base.PlatformContext"
);
DalDataAccessType = assembly.GetType(
"Microsoft.Office.Project.DataEdit.Dal.DalDataAccess"
);
IAssnDataType = assembly.GetType(
"Microsoft.Office.Project.SvrDataEdit.IAssnData"
);
AssnDataType = assembly.GetType(
"Microsoft.Office.Project.SvrDataEdit.AssnData"
);
ProjDataRowType = assembly.GetType(
"Microsoft.Office.Project.DataEdit.Dal.DalDataAccess+ProjDataRow");
//+ProjDataRow because it's an internal class
StatusingDalBaseType = assembly.GetType("Microsoft.Office.Project.Server.DataAccessLayer.Generated.StatusingDalBase"
);
DALType = assembly.GetType(
"Microsoft.Office.Project.Server.DataAccessLayer.DAL"
);

Même chose pour les méthodes :
//Get Methods
FromBytesMethod = changeLogType.GetMethod("FromBytes"
);
FillStatusApprovalDetailsMethod = statusingType.GetMethod(
"FillStatusApprovalDetails"
, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
ReadAssignmentHistoryMethod = statusingType.GetMethod(
"ReadAssignmentHistory"
);
ReadStatusApprovalDetailsMethod = statusingType.GetMethod(
"ReadStatusApprovalDetails"
);
ReadTransactionDetailsMethod = statusingType.GetMethod(
"ReadTransactionDetails"
);
ReadStatusApprovalsInProcessMethod = statusingType.GetMethod(
"ReadStatusApprovalsInProcess"
);
ReadStatusMethod = statusingType.GetMethod(
"ReadStatus"
);
FillAssignmentDataMethod = DalDataAccessType.GetMethod(
"FillAssignmentData"
);
ReadAssignmentsForApprovalGridMethod = StatusingDalBaseType.GetMethod(
"ReadAssignmentsForApprovalGrid"
);
GetSecurityWorkerObjectMethod = statusingType.GetMethod(
"GetSecurityWorkerObject"
, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod);
CheckAssignmentAccessMethod = securityType.GetMethod(
"CheckAssignmentAccess", new Type[] { typeof(Guid
) });

Dernière opération d'initiatlisation, je récupère les informations sur les constructeurs que je vais utiliser :
//Get Constructors
PlatformContextConstructor = PlatformContextType.GetConstructor(new Type[] { typeof(Guid), typeof
(SPUrlZone) });
PlatformContextConstructor2 = PlatformContextType.GetConstructor(
new Type[] { typeof(bool), typeof(string), typeof(Guid), typeof(Guid), typeof(Guid), typeof(SPUrlZone), typeof(string
) });
StatusingConstructor = statusingType.GetConstructor(
new Type
[] { PlatformContextType });
AssnDataConstructor = AssnDataType.GetConstructor(
new Type
[0]);
ProjDataRowConstructor = ProjDataRowType.GetConstructor(
new Type[] { typeof(DataRow
) });

Voici le code :

Première étape, initialiser un PlatformContext. Pour cela, il faut instancier la classe PlatformContext. Il y a 2 constructeurs, le premier est utilisé lorsque l'on n'a pas de ContextInfo et le second si le ContextInfo est présent. Le type ContextInfo est défini dans Microsoft.Office.Project.Server.Library.PSContextInfo. Pour mon besoin, j'ai mis mon code dans un EventReceiver qui hérite de Microsoft.Office.Project.Server.Events.StatusingEventReceiver et, sur la méthode OnApplied(), le ContextInfo est passé en paramètre. Je vous ai quand même mis le code pour instancier un PlatformContext sans ce ContextInfo.
//Define Platform Context
Object
platformcontext;
if (contextInfo==null
)
   platformcontext = PlatformContextConstructor.Invoke(
new object
[] { siteguid, SPUrlZone.Default });
else
   platformcontext = PlatformContextConstructor2.Invoke(new object
[] { contextInfo.IsWindowsUser, contextInfo.UserName, contextInfo.UserGuid, contextInfo.TrackingGuid, contextInfo.SiteGuid, contextInfo.UrlZone, contextInfo.Lcid });

Maintenant je peux instancier mon objet Statusing .
Object statusing = StatusingConstructor.Invoke(new object[] { platformcontext });

Avant toute chose, il faut contrôler que l'on a accès aux informations. assuid correspond au guid de l'assignement (champ "ASSN_UID" de la base de données) :
//Test Security
Object security = GetSecurityWorkerObjectMethod.Invoke(statusing, null
);
bool rep = (bool)CheckAssignmentAccessMethod.Invoke(security, new object
[] { assuid });
if
(!rep)
   System.Diagnostics.
Debug.Assert(false, "L'utilisateur n'a pas accès à cet assignment !");

Si l'utilisateur a accès, on peut remplir le dataset AssnHistoryDataSet :
Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet dshistory = null;
dshistory = (Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet)ReadAssignmentHistoryMethod.Invoke(statusing,
new object[] { assuid, Microsoft.Office.Project.Server.Library.AssnHistoryItemType.Assignment });

Une fois le dataset remplit, on récupère le DataTable HistoryDataTable : (gérer le cas où dshistory == null, s'il est nul c'est sans doute que vous n'avez pas accès, normalement, nous avons testé l'accès ci-dessus)
Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet.HistoryDataTable dthistory = dshistory.History;
Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet.HistoryRow rowhistory;

Nous touchons au but Smile, maintenant nous devons récupèrer toutes les transactions (gérer le cas où dthistory.Rows.Count == 0). transuid correspond au guid de la transaction qui nous intéresse (champ  "ASSN_TRANS_UID" de la base de données). Je filtre sur une transaction mais si vous le souhaitez, vous pouvez parcourir toutes les transactions :
DataRow[] transuidrows = dthistory.Select("ASSN_TRANS_UID = '" + transuid + "'");

Pour chaque transaction, récupérer les détails (gérer le cas où transuidrows.Length == 0). Nous arrivons dans la partie la plus longue en terme de code mais la plus simple fonctionnelement parlant. En effet, nous allons parcourir toutes les row (de type HistoryRow) et, pour chacune de ces rows nous allons appeler la méthode ReadTransactionDetails afin d'obtenir les détails de la transaction. Actuellement je ne sais pas pourquoi mais, parfois cette méthode me renvoie une erreur, c'est pour cette raison que j'ai mis le try catch. Ensuite, si la méthode a bien fonctionnée, nous pouvons parcourir chaque row de type StatusApprovalTransactionDetailsRow et en extraire les informations qui nous intéresse (COLUMN_NAME et CHANGE_VALUE) :
rowhistory = (Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet.HistoryRow)transuidrows[transuidrows.Length - 1];
for (int j = 0; j < transuidrows.Length; j++)
{
   rowhistory = (Microsoft.Office.Project.Server.Schema.AssnHistoryDataSet.HistoryRow)transuidrows[j];
   
//ReadTransactionDetails() call stored procedure MSP_WEB_SP_QRY_ReadTransaction
      
Microsoft.Office.Project.Server.Schema.StatusApprovalTransactionDetailsDataSet.StatusApprovalTransactionDetailsDataTable statusApprovalTransactionDetails = null
;
   try
   {
      Microsoft.Office.Project.Server.Schema.StatusApprovalTransactionDetailsDataSet set = (Microsoft.Office.Project.Server.Schema.StatusApprovalTransactionDetailsDataSet)ReadTransactionDetailsMethod.Invoke(statusing,
new object[] {
rowhistory.ASSN_TRANS_UID });
      statusApprovalTransactionDetails = set.StatusApprovalTransactionDetails;
   }
   catch (Exception
ex)
   {
      //Sometimes there is a crash in this function...
      if (ex.InnerException ==null
)
         System.Diagnostics.
Debug.Assert(false, "ReadTransactionDetails() has crashed"
);
      else
         System.Diagnostics.Debug.Assert(false, "ReadTransactionDetails() has crashed in "
+ ex.InnerException.StackTrace);
   }
   if (statusApprovalTransactionDetails != null)
   {
      if
(statusApprovalTransactionDetails.Rows.Count == 0)
         System.Diagnostics.
Debug.Assert(false, string.Format("ReadTransactionDetails({0}) return 0 row !"
, rowhistory.ASSN_TRANS_UID.ToString()));
      for (int
k = 0; k < statusApprovalTransactionDetails.Rows.Count; k++)
      {
         Microsoft.Office.Project.Server.Schema.StatusApprovalTransactionDetailsDataSet.StatusApprovalTransactionDetailsRow rowdetails = (Microsoft.Office.Project.Server.Schema.StatusApprovalTransactionDetailsDataSet.StatusApprovalTransactionDetailsRow)statusApprovalTransactionDetails.Rows[k];
         //rowdetails.COLUMN_NAME;
         //rowdetails.CHANGE_VALUE;
      }
   }
}

Il y a d'autres choses intéressantes à récupérer : Là aussi je vous conseille le blog de Patrick Conlan dans lequel il explique à quoi correspondent les valeurs de ces champs :
row["ASSN_TRANS_STATE_ENUM"], row["ASSN_TRANS_TYPE_ENUM"], row["ASSN_TRANS_ACTION_ENUM"],
row["ASSN_TRANS_ERROR_ENUM"], rowhistory.ASSN_TRANS_COMMENT_TYPE_ENUM, rowhistory.ASSN_TRANS_DATE_ENTERED;
rowhistory.RES_NAME, rowhistory.ASSN_TRANS_COMMENT, ...
Par exmple :
switch ((byte)row["ASSN_TRANS_TYPE_ENUM"])
{
   case
0:
      //
"Submitted"
;
      break
;
   case
1:
      //
"Approved"
;
      break
;
   case
2:
      //
"Rejected"
;
      break
;
   default
:
      //"Unknown"
;
      break
;
}

Voilà, c'est fini. J'espère vous avoir apporté quelques informations. Si par hasard, quelqu'un a une explication sur le fait que la méthode ReadTransactionDetails() renvoie parfois une erreur, cela m'intéresse.
Merci.

 

PS: J'avais oublié les références à ajouter à votre projet :
Microsoft.Office.Project.Server.Library.dll
Microsoft.Office.Project.Schema.dll
Microsoft.Office.Project.Shared.dll
Microsoft.SharePoint.dll

Ainsi que les usings :
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using Microsoft.Office.Project.Server.Library;
using Microsoft.SharePoint.Administration;
using Microsoft.Office.Project.Server.Schema;
using Microsoft.Office.Project.Library;
using System.Diagnostics;

 

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: mercredi 18 février 2009 09:21 par cartonjer
Classé sous : ,

Commentaires

Pas de commentaires

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- SharePoint : Bug sur la gestion des permissions et la synchronisation Office par Blog Technique de Romelard Fabrice le 07-10-2014, 11:35

- SharePoint 2007 : La gestion des permissions pour les Workflows par Blog Technique de Romelard Fabrice le 07-08-2014, 11:27

- TypeMock: mock everything! par Fathi Bellahcene le 07-07-2014, 17:06

- Coding is like Read par Aurélien GALTIER le 07-01-2014, 15:30

- Mes vidéos autour des nouveautés VS 2013 par Fathi Bellahcene le 06-30-2014, 20:52

- Recherche un passionné .NET par Tkfé le 06-16-2014, 12:22

- [CodePlex] Projet KISS Workflow Foundation lancé par Blog de Jérémy Jeanson le 06-08-2014, 22:25

- Etes-vous yOS compatible ? (3/3) : la feuille de route par Le blog de Patrick [MVP SharePoint] le 06-06-2014, 00:30

- [MSDN] Utiliser l'approche Contract First avec Workflow Foundation 4.5 par Blog de Jérémy Jeanson le 06-05-2014, 21:19

- [ #ESPC14 ] TH10 Moving mountains with SharePoint ! par Le blog de Patrick [MVP SharePoint] le 06-01-2014, 11:30