Header Image

Translate

dinsdag 8 december 2015

Kopieer een groot binair bestand naar de server

De meest makkelijke manier om een binair bestand naar de server te communiceren is met behulp van de bindata componenten:
Effectie wordt deze wijze ook gebruikt om documenten in de database op te slaan in document-verwerking functionaliteit. Hier gebruik ik even de 'appendToFile'. Vreemd genoeg is in Ax 2009 (SP1) geen FileIOPermission nodig, maar dat is even niet het punt. Het punt is als het bestand te groot wordt (12MB):


"The Dynamics Ax session is no longer valid" op het moment dat de setData wordt aangeroepen. Dit gebeurd ook als je documentverwerking opslaat in de database en even het limiet hoog genoeg zet.

Je kan proberen de getData eerst toe te wijzen aan een container, maar wederom gaat bij setData dit fout.

Het bindata object heeft een methode: copyData. Leuke functie met een offset en grootte. Zou je de buffer in delen naar de server kunnen sturen, maar helaas:

Dit is zelfs erger. Deze actie zorgt ervoor dat de AOS gewoon crashed.


De enige oplossing die ik zover heb gevonden is het eerst kopieren naar een kleine buffer en die dan serverside toe te voegen:


Code:

static void Example_File2_C(Args _args)
{
    BinData clientBin    = ClassFactory::makeObjectOnClient(classnum(BinData));
    BinData clientBinTmp = ClassFactory::makeObjectOnClient(classnum(BinData));
    BinData serverBin    = ClassFactory::makeObjectOnServer(classnum(BinData));
    str fileName = @"C:\Ax\Client\file2.bin";
    str fileNameServer = @"C:\Ax\Server\file2.bin";
    int fileSize;
    int offset;
    int bufferSize = 1024*1024; // 1MB;
    ;
    #file

    clientBin.loadFile(fileName);
    fileSize = clientBin.size();
    offset = 0;
    while (offset < fileSize)
    {
        clientBinTmp.copyData(clientBin, offset, bufferSize);

        serverBin.setData(clientBinTmp.getData());
        serverBin.appendToFile(fileNameServer);

        offset += bufferSize;
    }

    info('Done!');
}

Dit behoud niet de bestandsinformatie (gemaakt op datum, NTFS beveiliging etc.) maar de data is serverside. Nog een leuke progress-bar om de gebruiker tevreden te houden en je hebt een manier. 

maandag 7 december 2015

Unable to load page–Ax portal via business connector

Een nieuwe installatie van een Ax 4.0 sharepoint omgeving op een windows 2008 machine gaf een fout bij het maken van de PDF: ‘Unable to load page.’ in de eventlog. De PDF zelf is intern zo goed als leeg::

Er zijn veel artikelen te vinden over dit onderwerp, en de fout leek veroorzaakt te worden door de Ax client, of in dit geval de business connector gezien dat het een portal is. Maar SP2 met de hotfix is geïnstalleerd en bij het starten van de setup zegt de setup-util dat de versie al geïnstalleerd zijn. De business connector DLL in de client bin bevestigt dit verhaal:

    Version 4.0.2503.1872.

Middels een simpele info in een portal webform kreeg ik echter een andere versie:

    Kernel version: 4.0.2163.0

Runtime was de versie dus 4.0.2163.0 (SP1). In de GAC (Global Asembly Cache) was inderdaad een andere DLL geïnstalleerd:

Oplossing was simpel: opnieuw registeren van de DLL in de client bin map. Maar deze productie server had geen powerhell of .NET SDK toolkit (gacutil.exe) om een DLL te registreren. Daarop heb ik tijdelijk even de toolkit van mijn laptop (Windows 10) gekopieerd naar de server::
C:\Program Files (x86)\Microsoft SDKs\Windows

Er zijn 3 mappen: gacutil zit in 2 van deze mappen(v7.0a en v8.0a). Gebruik hier de v7.0a, bij de v8.0a krijg je een melding: :
entry point was not found
De volgende DLL's heb ik geregistreerd:
  • C:\Program Files\Microsoft Dynamics AX\40\Client\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll
  • C:\Program Files\Microsoft Dynamics AX\40\Client\Bin\Microsoft.Dynamics.ClrBridge.dll
  • C:\Program Files\Microsoft Dynamics AX\40\Client\Bin\NL\Microsoft.Dynamics.BusinessConnectorNet.resources.dll
En nu is de portal het met mij eens over welke versie er is:

En de PDF wordt weer juist gegenereerd.  

vrijdag 3 juli 2015

Actie uitvoeren in een actieve Ax client van extern

Soms is het wenselijk dat Ax iets doet, wat wordt getriggerd vanuit een externe applicatie. Een proces dat communiceert met een andere proces wordt ook wel IPC (Inter-process-communication) genoemd. Een voorbeeld; de telefoon gaat. Op basis van het telefoonnummer kan worden herleid welke klant belt en je wilt direct zien wat er speelt bij een klant. Nu heeft Ax wel een TAPI interface, maar als dat niet mogelijk is…
Standaard heeft Ax een zogenaamde ‘named pipe’. Een named pipe kan je zien als een message-queue in standaard Windows die je aanmaakt en een listener op zet. Beetje vergelijkbaar met een map die actief gepolled wordt; zodra er een bestand binnenkomt wordt dit herkent en kan je er iets mee. Er staat al een goede blog van Max Belugin online die dit omschrijft: http://axcoder.blogspot.nl/2010/10/how-to-open-form-in-running-ax-from.html

Samengevat:
Elke Ax client maakt named-pipe aan met het formaat: Dynamics\Event\0S-1-5-5-x-xxxxxx
  • Dynamics\Event – dit is een vaste waarde
  • 0 – dit is de waarde ingericht in Ax. Basis \ Instellingen \ Waarschuwingen \ Waarschuwingsparameters:
image
  • S-1-5-5-x-xxxxxx – dit is een Security Id (SID) voor de logon sessie. Dit is dus _niet_ de SID van de gebruiker. Dit is even interessant: de sessie is per gebruiker uniek op een terminal server, zodat de named pipe expliciet voor die gebruiker is en niet andere gebruikers gaat aansturen. Maar: als een gebruiker meerdere Ax sessies open hebt staan (zelfde of meerdere omgevingen), dan lijkt het de langst-actieve client die de queue afhandelt. Met de vorige parameter (Doel voor drilldown) zou je wel een test-omgeving een andere Id kunnen geven. 
De blog van Max omschrijft het mooi met een voorbeeld .NET applicatie. Daar ga ik dan ook niet verder op in. Het ophalen van de logon-session-SID heb ik even bekeken, maar ik kan het zo ook niet mooier maken.

Wat iets meer in de oplossing zit van Max maar niet is verwoord, is wat er nou gebeurt in Ax:
1. De class ‘EventDrillDownPoller’, methode ‘scheduledPoll’
1 void scheduledPoll() 2 { 3 str pipeData; 4 SysStartUpCmd cmd; 5 ; 6 if (pipe) 7 { 8 pipeData = pipe.read(); 9 if ('' != pipeData) 10 { 11 cmd = SysStartUpCmd::construct(pipeData); 12 if (cmd) // INT - check null value 13 cmd.infoRun();  14 this.createPipe(); // Re-create the pipe in order to be ready for the next command. 15 } 16 infolog.addTimeOut(this, methodstr(EventDrillDownPoller, scheduledPoll), #idleTimeBetweenExternalDrillDownPolls); 17 }  18 }
Deze methode wordt regelmatig (<1 seconde) indien de Ax client is idle uitgevoerd. Regel 12 heb ik toegevoegd; de SysStartupCmd kan namelijk null terug geven als het commando niet herkend wordt. Op zich is de nullpointer fout iets dat je een gebruiker niet wilt geven, maar het echte probleem is dan dat de createPipe en addTimeOut niet aangeroepen wordt en daarmee stopt deze queue. Het is ook best-practice om data (en commando’s) van externe applicaties te valideren voordat je deze verwerkt, zou ik zeggen.

2. De SysStartUpCmd en afstammeling doet eigenlijk het werk:

1 static SysStartupCmd construct(str startupCommand)   2 {   3 str s = strLRTrim(startupCommand);   4 int p = strscan(s,'_',1,strlen(s)); 5 str parm; 6 SysStartupCmd sysStartupCmd;   7 ;   8 if (p) 9 { 10 parm = substr(s,p+1,strlen(s)); 11 parm = strrem(parm,'"'); 12 s = substr(s,1,p-1); 13 }  14 setprefix(s); 15 switch (s)  16 {  17 case 'setbuildno':  18 sysStartupCmd = new SysStartupCmdBuildNoSet(s,parm); 19 break;  20 ...  21 case 'callincomming': 22 // INT - process call 23 sysStartupCmd = new SysStartupCmdCallIncomming(s,parm);  24 break;  25 } 26 if (sysStartupCmd)  27 {  28 if (sysStartupCmd.canRun()) 29 return sysStartupCmd;  30 error(strfmt("%1, %2", s, parm));  31 error("@SYS81158"); 32 }  33 return null; 34 }
Regels bij 20 even voor de overzichtelijkheid wat code weggehaald. In deze methode wordt op basis van de aangeleverde waarde een commando uitgevoerd. Hier zit je al in de actieve thread van Ax en kan je alles doen. Open een form, geef een alert, maak een record aan, wat je wil. Maar je kan het ook doen zoals de reeds bekende commando’s, maak een SysStartupCmd subclass aan en beperkt de aanpassing tot een extra case in de switch. Nu het commando: deze bestaat uit minimaal één en maximaal twee delen: <command>_<parameter>, dus met underscore. In dit voorbeeld: als je het commando “callincomming_0221234567” dan splitst standaard Ax in regels 10 t/m 12 het commando en wordt de parameter als zodanig doorgegeven. Je kan zelf dit weer met een sub-scheidingsteken splitsen en in je eigen class verwerken.

Of als je veel commando’s denkt nodig te hebben, dan kan je de code beperken met de volgende switch:
1 ClassName className;  2 ClassId classId;   3 SysDictClass sysDictClass;   4 ;  5 ...   6 default:   7 // INT - use action name for classname 8 className = classid2name(classnum(SysStartupCmd));   9 className += s;  10 classId = classname2id(className);  11 sysDictClass = new SysDictClass(classId); 12 if (sysDictClass 13 && sysDictClass.allowMakeObject()) 14 {  15 sysStartupCmd = sysDictClass.makeObject(s,parm);  16 }  17 break;

donderdag 2 juli 2015

Workflow Werknemer-ID is niet ingesteld of de record voor %1, %2 is niet gevonden.

Bij het aanbieden van bijvoorbeeld een inkooporder aan de workflow worden verschillende automatische controles uitgevoerd. De foutmelding Werknemer-ID is niet ingesteld of de record voor %1, %2 is niet gevonden. geeft heel feitelijk aan wat er fout is.
Voor elke medewerker moet een bijbehorende gebruiker zijn aangemaakt. Of anders gezegd aan elke gebruiker moet een medewerker zijn gekoppeld.
Controleer bij systeembeheer, gebruiker



Optie Relaties:
Gebruiker zonder relatie
Voor deze gebruiker bestaat geen relatie, zoals de foutmelding "Werknemer-ID is niet ingesteld of de record voor %1, %2 is niet gevonden." ook duidelijk aangeeft.

De volgende situatie geeft echter ook dezelfde foutmelding:  

Gebruiker met een relatie
In de situatie hierboven wordt dezelfde foutmelding gegeven. Dit lijkt incorrect immers er is toch relatie voor de gebruiker? Inderdaad is aan de gebruiker een relatie gekoppeld. Echter de relatie is niet van het type werknemer. Dit is alleen te zien door het ontbreken van een afbeelding onder het kopje medewerker.

Gebruiker met een correcte relatie
Alleen indien een medewerker gekoppeld is aan de gebruiker zal de foutmelding Werknemer-ID is niet ingesteld of de record voor %1, %2 is niet gevonden. niet voorkomen. Het verschil met de voorgaande koppeling is het icoontje onder de kolom medewerker.

vrijdag 26 juni 2015

Verwijderen van gelijke elementen gedurende Import XPO

 

Bij het importeren van grote XPO bestanden kan het handig zijn om op voorhand al automatisch de elementen te laten controleren waar geen verschil in zit t.o.v. de AOT. De elementen die vervolgens overblijven kunnen dan alsnog handmatig gecontroleerd worden.

De code hiervoor is vrij eenvoudig.

1.) Open de AOT

2.) Ga naar Forms \ SysImportDialog

3.) Voeg onderstaande methode toe aan het form:

1 public void removeDuplicates()
2 {
3 TreeNodePath treeNodePath;
4 TreeNode treenodeInAot;
5 TreeNode treenodeInXpo;
6
7 while select tmpImportAot
8 {
9 try
10 {
11 treeNodePath = tmpImportAot.TreeNodePath;
12 treenodeInAot = TreeNode::findNode(treeNodePath);
13 //AX2012
14 treenodeInXpo = SysImportElementsForm.extractTreenodeFromFile(treeNodePath);
15 //AX2009 - Turn this on for AX2009 and remove line above
16 //treenodeInXpo = SysImportElements.extractTreenodeFromFile(treeNodePath);
17
18 if (treenodeInAot &&
19 treenodeInXpo)
20 {
21 if( SysCompare::silentCompare(SysTreeNode::newTreeNode(treenodeInAot), SysTreeNode::newTreeNode(treenodeInXpo)) )
22 {
23 tmpImportAot.delete();
24 }
25 }
26 }
27 catch
28 {
29 tmpImportAot.delete();
30 continue;
31 }
32 }
33
34 SysImportElementsForm.buildTree(aotTree);
35 SysFormTreeControl::expandTree(aotTree, aotTree.getRoot());
36 }
37

4.) Ga vervolgens naar het Design en open de ButtonGroup AOTButtonGroup.

5.) Voeg een nieuwe button toe: BtnRemoveDuplicateElements

6.) Voeg een Clicked methode toe met onderstaande code


1 void clicked()
2 {
3 super();
4
5 element.removeDuplicates();
6 }

7.) Geef een Label aan de button: Identiek verwijderen.

image

zondag 21 juni 2015

Log de infolog

Het komt geregeld voor dat gebruikers fouten krijgen. Soms duidelijk, soms niet. Soms meld de gebruiker de melding, maar soms ook niet. Voorbeeld:
image
Log de infolog:
image
Het bevat niet de formuliernamen in deze stack, maar wel dat het vanaf een formulier is. Als het via de formletter classes gaat, geeft het al meer inzicht wat de gebruiker aan het doen was. Dit werkt ook in de Ax 4.0 portal: fouten die op de portal gebeuren in de business connector komen ook terug als je de filter goed instelt.

Download

De oplossing voor Ax 2009 (werkt ook voor Ax 4.0), te downloaden hier: INT20150622_AX2009_MessageLog

Inrichting

Na import heb je onder administratie\instellen\Bericht filter/log. Dit opent het scherm:
image
Velden:
Message criteria: Filter met wildcards. ‘*conflict*’ logt alle berichten met het woord conflict erin. ‘*’ gewoon alles
Filter until date: Filter tot datum. Dit is een bewuste implementatie keuze. Het komt tevaak voor dat database logs worden aangezet en dan vergeten. Om te voorkomen dat het systeem zich volvreet met onnodige logging, vul deze datum realistisch.
Filter user: Als je een specifieke gebruiker wil loggen. Leeg = alle
Filter loglevel: De type melding die je wilt loggen. Info, warning error zijn gangbare. Let wel: omdat info een enumwaarde 0 heeft, moet je wel ‘filter loglevel active’ aan hebben staan.
Filter loglevel active: Indien aan, logt het systeem alleen meldingen die voldoen aan je filter loglevel
Created/modified: kan je zien wie de log heeft aangemaakt en gewijzigd.
Voorbeelden:
image
1 – Log alle fouten van iedereen
2 - Log alle warnings van de admin user
3 – Log alle meldingen (info, warning & error) waar de woorden ‘update’ en ‘conflict’ in die volgorde in de tekst staan.

Inhoudelijk

In standaard Ax lopen alle meldingen naar de infolog via dezelfde methode: infoLog.add(…). Daar zit dan ook de enige aanpassing in standaard Ax (naast het menu zelf):
1 Exception add(  2 Exception _exception,  3 str _txt,  4 str _helpUrl = '',  5 SysInfoAction _sysInfoAction = null,   6 boolean buildprefix = true)   7 {   8 // SysInfologLevel infologLevel = this.infologLevel(); 9 int numOfLines,i;  10 int actionClassId; 11 container packedAction; 12 xSession session;  13 ; 14 INTProgramLog::logInfoMessage(_txt, _exception); // INT Ghull - message log 15 switch (logLevel)  16 {  17 case SysInfologLevel::None:  18 return _exception;  19 case SysInfologLevel::Warning:  20 if (_exception == Exception::Info)  21 return _exception;  22 break; 23 case SysInfologLevel::Error:  24 if (_exception == Exception::Info || _exception == Exception::Warning)  25 return _exception;  26 } 27 ....
De security keys zijn gelijk getrokken aan de admin module. Het is net als de database log een tool voor beheerder en consultants om hun werk makkelijker te maken.

image

Let op bij de import. Een aanpassing zoals hier in de info class kan gevaarlijk zijn als deze in een niet-compileerbare staat wordt achtergelaten.

Volgende keer als een gebruiker dus komt: ik krijg een melding zonder de melding nog te weten of dat het sporadisch voorkomt, zet de log aan. Het geeft op zijn minst inzicht in de locatie binnen het programma. Vandaar ook de benaming ‘INTProgramLog’.

Gerrit Hulleman

Databaselog met callstack


Het komt vaak genoeg voor; een log om bepaalde records, velden of acties bij te houden. De database log is hierin een handige tool, maar deze geeft inzicht in onder welk account welke actie met welke waarden zijn gewijzigd, maar niet waar in de applicatie. Soms zijn bijwerkacties niet altijd vanzelfsprekend, en dan is het zoeken naar wat er voor een proces heeft plaatsgevonden. Vooral als de actie plaats vind als onderdeel van een proces in een maatwerk module is het zoeken.
Een kleine aanpassing om te helpen, werkt in AX2009 maar ook AX4.0; toevoegen van de callstack aan de databaselog.
image
Technisch zeer eenvoudig; een nieuw container veld op de sysdatabaselog krijg op moment van de insert de callstack:
1 public void insert()   2 {   3 container callStack; 4 ;   5 6 new SysDatabaseLogPermission().demand();   7 8 this.Username = UserInfoHelp::userName(curuserid()); 9 // INT Ghull - Begin - add calling callstack 10 callStack = xsession::xppCallStack(); 11 callStack = condel(callstack, 1, 6); // Skip the 3 top stack lines {...} 12 this.INTCallStack = callStack;  13 // INT Ghull - end - add calling callstack 14 super();  15 }
Omdat de insert van de databaselog ook op de stack staat, worden de bovenste 6 (3 regels van de callstack maar een callstack container bestaat uit paren van 2 waarden per callstack regel) verwijderd. Dit zijn de xrecord.update(), Application.LogUpdate() en SysDatabaseLog.Insert methoden (of het equivalent bij de insert/delete). Op het SysDatabaseLog form zelf een simpele knop met wel een securitykey om dit in een info te tonen:
1 //INT 20150621 Ghull - show callstack stored in database log 2 void clicked() 3 {   4 int stackLength = conlen(SysDatabaseLog.INTCallStack) / 2; // 2nd value line number 5 int idx; 6 ; 7 8 for (idx = 0; idx < stackLength; idx++)   9 {  10 info(strfmt('%1 (%2)', conpeek(SysDatabaseLog.INTCallStack, idx*2+1),   11 conpeek(SysDatabaseLog.INTCallStack, idx*2+2))); 12 } 13 }
Oplossing voor Ax2009 is te downloaden hier: INT20150622_AX2009_DatabaseLogCallStack.xpo De 4.0 variant is eigenlijk hetzelfde.

Gerrit