Dynamics Ax Alerting multiple users (By email)

First things first… the original post that I found about this topic can be found here : http://dynamic-ax.co.uk/DynamicsAXAlertsToMultipleUsers.aspx

But since the link to the xpo was broken , I created this post to put this online and make some minor changes. (Don’t mind some best practices stuff like labels, security keys, … because you’ll probably create your own to use )

First we create a table and form to contain the recipient list.

Event Rule Recipient List

 

Then an adjustment is needed in the EventCreateRule class, method Run. Here the event rule will be updated so we need to maintain the link between the EventRuleId and the recipients table.

public void run()
{
    #OCCRetryCount
    EventRule           eventRuleDB;

    // KeSae : 21/04/2010 : Alerting multiple mail recipients
    EventRuleId        oldRuleId;
    ;

    try
    {
        ttsbegin;

        select firstonly eventRuleTmp;

        eventRuleDB.data(eventRuleTmp);
        eventRuleDB.setDeleteRefRecId(callerRecord);

        // KeSae : 21/04/2010 : Alerting multiple mail recipients
        oldRuleId = eventRuleDB.RuleId;

        eventRuleDB.RuleId = this.parmCreatedRuleId();
        eventRuleDB.insert();
        eventRuleDB.filterQuery(this.queryrun().query());
        eventRuleDB.alertField([tmpEventAlertField]);
        eventRuleDB.typeValue(packedEventType);
        eventRuleDB.contextInfo(this.packContextInfo(eventRuleDB));

        this.parmCreatedRuleId(eventRuleDB.RuleId);

        // KeSae : 21/04/2010 : Alerting multiple mail recipients
        // Since the RuleId changes during this process, update the link in the recipient table
        NVMPEventRuleRecipients::updateRuleIdLink(oldRuleId, eventRuleDB.RuleId);

        ttscommit;

        this.checkForeignKey();
    }
    catch (Exception::Deadlock)
    {
        retry;
    }
    catch (Exception::UpdateConflict)
    {
        if (appl.ttsLevel() == 0)
        {
            if (xSession::currentRetryCount() >= #RetryNum)
            {
                throw Exception::UpdateConflictNotRecovered;
            }
            else
            {
                retry;
            }
        }
        else
        {
            throw Exception::UpdateConflict;
        }
    }

}

 

The following change is adding the recipients when mailing. Class EventActionEmail, method Execute :

mappings = this.createEmailParameterMap(alertInbox,eventType,eventRule);

        xmlParameters = EventActionEmail::createEmailParameterXml(mappings);

        if (eventParameters.AlertTemplateId)
        {
            SysEmailTable::sendMail(eventParameters.AlertTemplateId
                                ,   userInfo.Language
                                ,   sysUserInfo.Email
                                ,   mappings
                                ,   ''
                                ,   xmlParameters
                                ,   true
                                ,   eventRule.UserId
                                ,   true);

            // KeSae : 21/04/2010 : Alerting multiple mail recipients
            NVMPEventRuleRecipients::sendMail(eventRule.RuleId
                                            , eventParameters.AlertTemplateId
                                            , mappings
                                            , xmlParameters);
        }
        else
            throw error("@SYS94918");

 

When the email for this alert is sent, every user in the list will receive a link to the alert by email.

Download the xpo here

Debug Alerts in Dynamics Ax 2009

Today I was creating some alert rules and needed to make some modifications to the alerting functionality. Something didn’t go quite as expected, so I wanted to debug the execution of an alert action.

Here is how you can do it :

  1. Make sure the debug mode is set to ‘when breakpoint’ for the user linked to the alert
  2. Put a breakpoint in the EventJobCud class, method Run
  3. Change the EventJobCus class, method Run so that it does not use the runAs command :

( Revert these changes when the debugging is done, but for now you need to make this change otherwise you will not be able to step into the EventAction classes for debugging)

void run()
{
    EventCUD             event;
    container             params;
    RunAsPermission    runAsPermission;
    ;

    this.expandCudRecords();

    while select event
          group by UserId
          where (event.Status == BatchStatus::Waiting) &&
                (event.CompanyId == curext())
    {
              params = connull();
              runAsPermission = new RunAsPermission(event.UserId);
              runAsPermission.assert();

              //BP Deviation Documented
              //runas(event.UserId, classnum(EventJobCUD), staticmethodstr(EventJobCUD, runCudEventsForUser),params,
              //      curext(), EventJobCUD::getLanguageId(event.UserId));

              EventJobCUD::runCudEventsForUser(params);

              CodeAccessPermission::revertAssert();
    }

    this.cleanupExecuting();
}

X++ run a process with user credentials

When you want to run for example a command prompt from within the X++ language, you can use the following code :
Let’s assume that the following parameters were passed to the method :

  • FileName : “C:\windows\system32\cmd.exe”
  • Arguments : “”
  • RunAsAdmin : true
  • UseShellExecute : true
  • WaitForExit : false

 

static void runWindowsProcess(  FileName    _fileName
                            ,   str         _arguments
                            ,   boolean     _runAsAdmin         = false
                            ,   boolean     _useShellExecute    = false
                            ,   boolean     _waitForExit        = false)
{
    System.Diagnostics.ProcessStartInfo processStartInfo;
    System.Diagnostics.Process          process;
    System.OperatingSystem              operatingSystem;
    System.Version                      operatingSystemVersion;
    System.Exception                    ex;
    int                                 major;
    Boolean                             start;
    ;

    try
    {
        // Assert CLRInterop permission
        new InteropPermission(InteropKind::ClrInterop).assert();

        // BP deviation documented
        processStartInfo    = new System.Diagnostics.ProcessStartInfo(_fileName, _arguments);

        // BP deviation documented
        process             = new System.Diagnostics.Process();

        // Get an instance of the System.OperatingSystem class
        operatingSystem         = System.Environment::get_OSVersion();

        // Get the OS version number
        operatingSystemVersion  = operatingSystem.get_Version();

        // If admin rights asked
        if(_runAsAdmin)
        {
            major = operatingSystemVersion.get_Major();

            // Get the major part of the version number (Starting from Windows Vista)
            if(major >= 6)
            {
                // Start using the runAs property which will prompt for administrator credentials
                processStartInfo.set_Verb('runas');
            }
        }

        // Use the system shell to execute the process
        processStartInfo.set_UseShellExecute(_useShellExecute);

        // Attach the starting information to the process
        process.set_StartInfo(processStartInfo);

        // Start the process and wait for it to complete
        start = process.Start();

        // Wait for the process to finish
        if(_waitForExit)
        {
            process.WaitForExit();
        }

        // Revert permission
        CodeAccessPermission::revertAssert();
    }
    catch(Exception::CLRError)
    {
        // BP Deviation documented
        ex = ClrInterop::getLastException();

        while (ex != null)
        {
            error(ex.ToString());

            ex = ex.get_InnerException();
        }

        throw error(strFmt("@HEL270", _fileName));
    }
}

Dynamics Ax AIF Web Service reference credential problem

 

At a customer’s site, I was creating some extra AIF wcf services so that some extra tables would be externally available. For creating the WCF service, I was using the service wizard in Ax 2009 which is described here : http://msdn.microsoft.com/en-us/library/aa609947.aspx

The newly created service was deployed to the Internet Information Server but when the .Net application tried to reference the webservice, the following happened : The service was prompting for credentials and did not work properly.

WCF Service Reference Credential Prompt

After a certain amount of searching time and nearly giving up,  I was browsing the configuration file of the web services directory and discovered that the BindingMethod of the deployed services was configured to use basicHttpBinding.

Endpoint binding method basicHttpBinding

This was the actual problem because the wsdl looked like this : (negotiate authentication tags were added, …)

 

Wrong WSDL with negotiateAuthentication tag

Changing the binding method to wsHttpBinding creates the service without the negotiateAuthentication and takes care of our annoying credential problem.

Endpoint binding method wsHttpBinding

Dynamics AX 6.0 – SQL AOD

Owkay, have a seat and hold on to whatever you can hold on to…

As of the beginning of AX (Axapta) we have always been used to working with AOD files. But it seems that the days of the AOD fileformat are numbered !

There are a couple of reasons why the use of AOD files has become slightly obsolete :

  • The MorphX environment has been decreasing in performance because of the increasing number of objects in the AOT
  • Searching for text in the AOT nodes takes a huge amount of time

So if AOD files don’t cut it anymore, what is there to be used then? Well Dynamics Ax 6.0 will be the first version ever to have the AOT metadata stored in the SQL Server backend !!
For the users using MorphX there is no difference except for the fact that it will be much more responsive, faster, … (fe searching for a text in all the methods of all forms completes in 2 seconds ! )

The next question will probably be : How are we going to transport between different Ax environments…? Well the answer is the new fileformat : *.axModel
This format is also a binary fileformat as the AOD file but they are much smaller then AOD files (Half the size) and there will be a new tool to export/import them into a SQL Database.

The client will be configurable to point to a certain model file to use in the client session. So it will be possible to fire up an Ax client and choose a certain model file to work in. Then you can create your solution and when you’re done all the changes will be available in that .axModel file and will be easily transported to other environments.

This will be a benefit in several situations, but if you want to read the full explanation, you can find a lot of info here : http://blogs.msdn.com/mfp/

Dynamics Ax : Posting packing slip for a shipment X++

To post a packing slip in Dynamics Ax for a Shipment you can do the following :

void postPackingSlipForShipment()
{
    SalesFormLetter salesFormLetter;
    Set             shipmentsToPostSet = new Set(Types::String);
    ;

    shipmentsToPostSet.add(wmsShipmentCreated.shipmentId);

    salesFormLetter = SalesFormLetter::construct(DocumentStatus::PackingSlip);
    salesFormLetter.parmLibMkdShipmentId(this.parmLIBMKDShipmentSignOffLine().ShipmentId);
    salesFormLetter.parmWmsShipmentIdSet(shipmentsToPostSet);
    salesFormLetter.update(wmsShipmentCreated, systemDateGet(), SalesUpdate::PickingList, AccountOrder::Account);
}

Notice the parmWmsShipmentIdSet parameter.
This is the one actually making sure only the data for that shipments is selected. The parmShipmentId() method does not cut it here.

Using C# / XML / XSLT to create Excel Spreadsheet

This article will not go into the details of the SpreadSheetML format but is inteded to show a way of creating Excel spreadsheets programatically.
There are several ways to do this and this is only one of them so I am also not going to make comparisons between the different methods.

In this example we are going to do the following :

  • Create an XML file containing customers
  • Create a template from Excel
  • Adjust the template file and turn it into an XSL file
  • Transform the customer XML file to get the result we want

Creating the XML file

We are going to create a simple XML file using C# containing a list of Items. The following figure shows the fields available for an item.

tblinventTable

For this post, we are going to keep it simple and write three sample items in the XML file.
Normally this data would be fetched from a database but here we just export three sample items from the code.

CreateXml

The result is the following XML file :

RawXml

Creating the template

Now that we have the XML file (which is relatively simple to create from your application), we are going to create an excel file template that will be used to merge with the XML file to get the result we want.

So we fire up Excel and create a simple spreadsheet to contain the list of Items.

ExcelTemplate1

When we have our desired layout, we save the file as an XML spreadsheet.

FileType

Now it is getting interesting… We can open the XML file an there we see that it is saved in the OOXML format (SpreadsheetML in this case)

ExcelTemplate2

From here on we can change the template by adding XSLT code that will receive data from the Items.xml file and that way we can add a row for each item to our spreadsheet. (You many possibilities here. It is also possible for example to create a worksheet for each item, …)

First  we adjust the template to create an XSL file. So rename the ItemsTemplate.xml filename to ItemsTemplate.xsl.

Then adjust the xsl file by adding following code at the beginning of the document :

BeginDoc

Also add the closing tags at the end of the document.

To create a record for each item in the source XML file , we just need to do an xsl:for-each loop for each item and process the XML code generated by excel.

 ValueOfSelect

Before we continue, there is a little thing we still need to do because otherwise this will not be working. In the xml node “Table” we need to delete the attribute “ExpandedRowCount” otherwise excel will not be able to open the result document.

Now our template is ready to be used in a transformation with the source XML file.

Transform the XML

Following code shows how you can do XSLT transformation through C# code resulting in the final XML file.

TransformCode

The actual result looks like this in the XML file.

ResultXml

And this can be neatly opened in Excel and gives the following result.

ResultExcel

Disable Driver Signing in Windows Vista / Windows 7

Today I wanted to install USB drivers from garmin on my box running Windows 7. Windows started complaining about the driver not being signed and thus I could not proceed with the installation of the garmin software.

The solution here is to disable the driver signing. You can do this in several ways.

  • First is to run some dos commands to disable it but I am not going to describe this here for the simple fact that this method does not work anymore after some of the updates are already present.
  • Second is to disable Driver Signing Enforcement at bootup. You can call up the advanced boot menu by Pressing F8 and then there is an option to disable driver signing enforcement. This does work and solved the problem for me because the driver neatly installed.
  • Third manner of doing this is the automated version of the second way : Ready Driver. Ready driver is a tool that will run at bootup and will automatically do the steps for you without you needing to do them. The thing here is that by doing it this way, you always have it disabled.

advancedbootoptions

I used the second method because I just wanted to disable it for this bootup.

Dynamics Ax Get and Remove text from the Infolog

Sometimes you want a process to log infolog data into a text field instead of sending it to the infolog. For example you are posting 6 custom made journalTables and if something goes wrong you want the infologdata to be stored in a Log field on the table and you want to continue with the processing of the other journalTables.

Then you will have a catch statement where you will need to get the infolog data and store it into the field but there are some difficulties :

  • First you want only the data generated by your process and not the data that might have already been in the infolog.
  • Second you want to clear the errors from you code from the infolog without clearing all the rest of the infolog data.

The key is to work with the infolog.line() method to remember when to start keeping track of the lines in the infolog.

We can demonstrate this with the following job :

static void LIB_KeSae_TestInfologRemoval(Args _args)
{
    int line;
    str omittedText;
    ;
    
    error("error1");
    error("error2");
    
    // Now we have 2 records in the infolog
    // From now on we want the following entries in the infolog to be stored
    // somewhere and omitted from the infolog window
    
    // Remember the number of entries before the custom code (in this case line = 2)
    line = infolog.line();

            //Customer code
            error("error3");
            error("error4");
            error("error5");
    
    // Here we want to retrieve statement 3, 4 and 5 en clear those from the infolog
    omittedText = global::LIBGetTextFromInfoLog(line + 1, true);

    // Some infolog entries after our code
    error("error6");
    error("error7");
}

The following code is the code from the GetTextFromInfolog method :

public static str LIBGetTextFromInfoLog(int _startLine = 1, boolean _clearInfo = true)
{
    int currentLine;
    str retValue;
    ;

    /*
        Do not use infolog.infologData to get the infolog data because that creates a copy and clears the actual infolog.
        Then the infolog.line() method will return 0 afterwards and you cannot clear the selected lines anymore
    */

    for(currentLine = _startLine; currentLine < = infolog.line(); currentLine++)
    {
        retValue += 'n' + infolog.text(currentLine);
    }

    // _startLine -1 because the infolog method will add 1
    if(_clearInfo)
        infolog.clear(_startLine -1);

    return retValue;
}

The infolog.clear() method will now take the startline into account and only clear the infolog entries from our code and leave the other entries intact as shown in the result of this job :

Result of the infolog after removing entries

Result of the infolog after removing entries

Dynamics Ax Disable / Enable all datasource fields

Sometimes you want to disable all fields on a form’s datasource and enable some of them afterwards. Put the following piece of code in the Global class and you can call it from wherever you like.

public static void LIBSetAllTableFields(FormDataSource _formDataSource,Boolean _value)
{
    DictTable       dictTable = new DictTable(_formDataSource.table());
    int             i;
    int             fieldNumber;
    ;
    for(i = 1;i < = dictTable.fieldCnt();i++)
    {
        fieldNumber = dictTable.fieldCnt2Id(i);
        if(_formDataSource.object(fieldNumber))
        {
            _formDataSource.object(fieldNumber).allowEdit(_value);
        }
    }
}