W standardowej funkcjonalności Axapty nie ma prostej możliwości uruchomienia zewnętrzną aplikacją formularza na konkretnej pozycji z bazy danych. Modyfikując nieznacznie kod AX i wykorzystując wbudowaną obsługę alertów możemy zaimplementować taką funkcjonalność. Daje nam to możliwość uruchamiania dowolnych formularzy z wybranymi danymi.

Projekt do wykonania, wybrane rozwiązanie

Dostałem zadanie podłączenia Axapty do centralki telefonicznej. Podstawową wymaganą funkcjonalnością było wyświetlanie konta kontrahenta w AX, który dzwoni do firmy. Oprogramowanie telekomunikacyjne na stanowiskach operatorów może wywoływać z odpowiednimi parametrami plik wykonywalny. Ponieważ „business connector” nie obsługuje interfejsu użytkownika (nie można uruchomić formularza) musiałem znaleźć inne rozwiązanie. Po krótkich testach zdecydowałem się na „EventDrillDown”. Metoda ta nie wymaga rozległych zmian w kodzie AX i działa stabilnie.

Wybrany sposób wykorzystuje funkcjonalność alertów. Gdy odbieramy alert za pomocą wiadomości e-mail możemy kliknąć wygenerowany link i otworzyć formularz z powiązanym zdarzeniem. Wykorzystamy to przy implementacji.

Krótki opis działania

Podczas uruchamiania Ax32.exe klasa EventDrillDownPoller tworzy potok („named pipe”) o przykładowej nazwie "Dynamics\Event\0S-1-5-5-0-284256". Nazwa składa się z 3 składników:

  • Dynamics\Event\ - stały prefix
  • 0 - Wartość z pola Podstawowe>Ustawienia>Alerty>Parametry Alertów>Cel przechodzenia do szczegółów
  • S-1-5-5-0-284256 - identyfikator sesji

Następnie wywołuje obiekt dziedziczący po SysStartupCmd gdy alert jest generowany. Po kliknięciu w link alertu w wiadomości e-mail wywoływany jest axhlink.exe, który łączy się z potokiem i przekazuje mu komendę. Uruchomiony klient AX wykrywa nową wiadomość i uruchamia SysStartupCmd, następnie SysAutoRun gdzie jest obsługa pliku xml z parametrami startowymi.

Żeby wykorzystać wbudowaną funkcjonalność obsługi alertów do uruchamiania formularzy z rekordem bazy musimy wykonać następujące zadania:

  • Dodać do klasy SysAutoRun obsługę wywoływania formularza ustawionego na wskazanym rekordzie w bazie
  • Utworzyć plik xml, który klient AX wywoła podczas uruchamiania i otworzy wybrany formularz
  • Połączyć się z potokiem („named pipe”) za pomocą identyfikatora sesji i parametru „przechodzenie do szczegółów”
  • Przekazać do potoku unikodowy string z komendę startową.

Kod x++ z modyfikacją Ax 2009

Kod x++, który implementujemy w klasie SysAutoRun. Zmieniamy jedną metodę i jedną dopisujemy. W metodzie execRun dodajemy obsługę pliku startowego z atrubutem table. Nasz kod od RunForRecord start.

Metoda SysAutoRun

// Common for all menu item types
  name = this.getAttributeValue(_command, #XmlAttrName);
  this.logInfo(strfmt("@SYS101205", name, enum2str(menuItemType)));
  mf = new MenuFunction(name, menuItemType);
  if (mf)
  {
      this.logInfo(strfmt("@SYS101206", mf.object(), enum2str(mf.objectType())));
      if(!mf.securityKey())
      {
          this.logError(strfmt("@SYS73076"));
      }
      else
      {
          // RunForRecord start
          // Jeżeli w naszym pliku xml jest atrybut table uruchamiany formularz z rekordem ze wskazanej tabeli
          if (this.getAttributeValue(_command, 'table'))
              mf.run(this.getArgs(_command));
          else// RunForRecord end
              mf.run();
          result = true;
      }
  }

Dodajemy metodę getArgs()

Metoda getArgs

// RunForRecord start
  // Wybranie rekordu na podstawie recId ze startowego pliku xml
  Args getArgs(XmlNode _command)
  {
      Args ret = new Args();
      Common record = SysDictTable::newTableId(tableName2ID(this.getAttributeValue(_command, 'table')))
                          .makeRecord();
      RecID   recID = str2int64(this.getAttributeValue(_command, 'recID'));
      select record where record.recID == recID;
      ret.record(record);
      return ret;
  }

Kod C# z testem działania

Kompletny przykład w C#. Wystarczy utworzyć w Visual Studio projekt typu „Console Application” i skopiować cały kod do program.cs. Uruchomiony program wywoła formularz odbiorcy na wybranym rekordzie.

Listing kodu C#

using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.IO.Pipes;
  using System.Runtime.InteropServices;
  using System.Security.Principal;
  namespace RunStartupCmd
  {
    class Program
    {
        static void Main(string[] args)
        {
            string myDocFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);//moje dokumenty
            string axEventDrillDown = "0";//wartość parametru z podstawowe/ustawienia/alerty/ustawienia alertów
            string axStartFileName = "axStart.xml";//nazwa pliku startowego
            string axStartLogFile = "axStart.log";//nazwa pliku z logiem
            string axMenuItem = "CustTable";//menuitem do uruchomienia
            string axTable = "CustTable";//tabela z której ma być pobrany rekord z danymi
            Int64 recId = 5637158410;//rekord w wybranej tabeli AX, należy zmienić na własny
            //string reprezentujący zawartość startowego pliku xml, który zostanie przetworzony podczas wywoływania potoku.
            string xmlData = "" +
                 "" + "";
            string fullXmlFilePath = myDocFolder + @"\" + axStartFileName;//ścieżka do naszego pliku
            string cmd = "AutoRun_" + fullXmlFilePath;//komenda do przekazania dla klienta AX żeby przetworzył plik startowy
            //Zapisujemy nasz plik w moich dokumentach
            using (System.IO.StreamWriter outFile = new System.IO.StreamWriter(fullXmlFilePath))
            {
                outFile.Write(xmlData);
            }
            string pipeName = "Dynamics\\Event\\" + axEventDrillDown + ClsLookupAccountName.GetLogonId();
            //łączymy się z potokiem klienta AX i przekazujemy komendę do wykonania (przetworzenie pliku startowego)
            using (var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous))
            {
                try
                {
                    //jeśli nie ustawimy timeout-u to aplikacja może się zawiesić np. jak nie jest odpalony klient AX
                    pipe.Connect(3000);
                }
                catch (Exception e)
                {
                    return;
                }
                //Wywołujemy AX
                var bytes = System.Text.UnicodeEncoding.Unicode.GetBytes(cmd);
                var bytesWithZero = bytes.Concat(new byte[] { 0, 0 }).ToArray();
                pipe.Write(bytesWithZero, 0, bytesWithZero.Length);
            }
        }
    }
    // Poniższy kod służy do pobrania logon sid-a dla użytkownika
    // i pochodzi ze strony http://stackoverflow.com/questions/2146153/how-to-get-the-logon-sid-in-c
    public class ClsLookupAccountName
    {
        public const uint SE_GROUP_LOGON_ID = 0xC0000000; // from winnt.h 
        public const int TokenGroups = 2; // from TOKEN_INFORMATION_CLASS 
        enum TOKEN_INFORMATION_CLASS
        {
            TokenUser = 1,
            TokenGroups,
            TokenPrivileges,
            TokenOwner,
            TokenPrimaryGroup,
            TokenDefaultDacl,
            TokenSource,
            TokenType,
            TokenImpersonationLevel,
            TokenStatistics,
            TokenRestrictedSids,
            TokenSessionId,
            TokenGroupsAndPrivileges,
            TokenSessionReference,
            TokenSandBoxInert,
            TokenAuditPolicy,
            TokenOrigin
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct SID_AND_ATTRIBUTES
        {
            public IntPtr Sid;
            public uint Attributes;
        }
        [StructLayout(LayoutKind.Sequential)]
        public struct TOKEN_GROUPS
        {
            public int GroupCount;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
            public SID_AND_ATTRIBUTES[] Groups;
        };
        // Using IntPtr for pSID instead of Byte[] 
        [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
        static extern bool ConvertSidToStringSid(IntPtr pSID, out IntPtr ptrSid);
        [DllImport("kernel32.dll")]
        static extern IntPtr LocalFree(IntPtr hMem);
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool GetTokenInformation(
            IntPtr TokenHandle,
            TOKEN_INFORMATION_CLASS TokenInformationClass,
            IntPtr TokenInformation,
            int TokenInformationLength,
            out int ReturnLength);
        public static string GetLogonId()
        {
            int TokenInfLength = 0;
            // first call gets lenght of TokenInformation 
            bool Result = GetTokenInformation(WindowsIdentity.GetCurrent().Token, TOKEN_INFORMATION_CLASS.TokenGroups, IntPtr.Zero, TokenInfLength, out TokenInfLength);
            IntPtr TokenInformation = Marshal.AllocHGlobal(TokenInfLength);
            Result = GetTokenInformation(WindowsIdentity.GetCurrent().Token, TOKEN_INFORMATION_CLASS.TokenGroups, TokenInformation, TokenInfLength, out TokenInfLength);
            if (!Result)
            {
                Marshal.FreeHGlobal(TokenInformation);
                return string.Empty;
            }
            string retVal = string.Empty;
            TOKEN_GROUPS groups = (TOKEN_GROUPS)Marshal.PtrToStructure(TokenInformation, typeof(TOKEN_GROUPS));
            int sidAndAttrSize = Marshal.SizeOf(new SID_AND_ATTRIBUTES());
            for (int i = 0; i < groups.GroupCount; i++)
            {
                SID_AND_ATTRIBUTES sidAndAttributes = (SID_AND_ATTRIBUTES)Marshal.PtrToStructure(
                    new IntPtr(TokenInformation.ToInt64() + i * sidAndAttrSize + IntPtr.Size), typeof(SID_AND_ATTRIBUTES));
                if ((sidAndAttributes.Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID)
                {
                    IntPtr pstr = IntPtr.Zero;
                    ConvertSidToStringSid(sidAndAttributes.Sid, out pstr);
                    retVal = Marshal.PtrToStringAuto(pstr);
                    LocalFree(pstr);
                    break;
                }
            }
            Marshal.FreeHGlobal(TokenInformation);
            return retVal;
        }
    }
  }

Podsumowanie

Opisana metoda działa na komputerze z działającym klientem Dynamics AX 2009. Jeżeli Axapta nie będzie uruchomiona aplikacja nie odnajdzie potoku i zakończy działanie. Poniżej plik do pobrania z kodem x++, który należy wgrać do AX oraz źródła wiedzy będące inspiracją powyższego artykułu:


Formularz kontaktowy

Dane kontaktowe

+48 663 990 838 kontakt@dynamik.pl Lokalizacja: Warszawa

Bądź z nami w kontakcie!