Send alerts/messages to online users

In Axapta 3.x there is a functionality that lets you send messages to online users. This functionality is lost in Dynamics AX 4.x which is a problem when you want an easy way to communicate with the online users. Maybe you want the users to know that that the AOS is going to be restarted at a specific time and as a result they should save their work and log out before this happens.

I’ve looked around the web and have found some examples on how to create these notifications, but none that worked all the way without a visit to the debugger. Tired of not having access to this functionality I decided to build it, and this is the result.

As the base I am using the alerts functionality in Dynamics AX 4.0.

To create a alert you need to create a new record in the Table EventInbox. This part is not that tricky but when you look at the notification created you will see that information is missing, and as a result, gives you a free ride into the debugger window. What I have done is to use existing information from a rule that I create that is used to trigger alerts. But I skip the “trigger” part and move directly to creating the alert based on the message the user enters in the dialog.

Class declaration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// START 080520 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
class FO_SendNotifications2OnlineUsers extends RunBase
{
    EventInbox              inbox;
    EventRule               rule;
    EventInboxId            inboxId;
    EventInboxData          data;
    EventType               eventType;
    str                     subject, message, reason;
    SysClientSessions       sessions;
    NoYesId                 sendEmail;
}
// END 080520 Fourone/EP (FO_Notifications2OnlineUsers)

This is the main method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
static void main(Args args)
{
    FO_SendNotifications2OnlineUsers  send = 
    new FO_SendNotifications2OnlineUsers();
    ;
 
    if(send.promptMessage())
    {
        send.run();
    }
 
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

The main method prompts a message to the user that looks like this:

Prompted message

In the reason field you enter the reason for this message, and then the subject and message as you would any email you would want to send.

This dialog is created in the promptMessage method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
boolean promptMessage()
{
    dialog          dlg;
    dialoggroup     dlgg;
    dialogfield     subjectField, messageField, 
                    reasonField, emailField;
    boolean         ret;
    ;
 
    dlg = new dialog("Send message to online users.");
    dlgg  = dlg.addGroup("Message");
    reasonField     = dlg.addField(
                      TypeID(Name), "Reason");
    subjectField    = dlg.addField(
                      TypeID(Name), "Subject");
    messageField    = dlg.addField(
                      TypeID(ItemFreeTxt), "Message");
 
    messagefield.displayHeight(10);
    messagefield.displayLength(50);
 
    emailField      = dlg.addField(
                      TypeId(NoYesId), "Send email");
 
    dlgg.columns(1);
 
    if(dlg.run())
    {
        subject     = subjectField.value();
        message     = messageField.value();
        reason      = reasonField.value();
        sendEmail   = emailField.value();
 
        ret = true;
    }
 
 
    return ret;
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

As you can see it’s pretty basic stuff. I will explain the email checkbox later, but I think you can guess what is does.

If the user clicks ok in the prompted dialog, we enter the run method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void run()
{
    ;
 
    this.setEventRule();
 
    if(rule)
    {
        this.setEventType();
 
        try
        {
            ttsbegin;
 
            setPrefix("Users notified.");
 
            while select * from sessions
            where sessions.Status  == SessionState::Running
            {
                // if(sessions.userId == curUserId())
                // Remove comments when testing. No need to 
                // send to all online users.
                // {
                    setPrefix(strFmt("UserName: %1", 
                                     sessions.userId));
                    this.createInbox();
                    this.createData();
                    this.infoLogMessage(sessions);
                // }
            }
 
            this.cleanUp();
 
            ttscommit;
        }
        catch(Exception::Error)
        {
            ttsabort;
 
            info("Error occured, process aborted.");
        }
    }
    else
    {
        error("Event rule could not be found. " +
              "This class requires " +
              "that atleast one event rule " +
              "exists before it can be used.");
    }
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

The method setEventRule:

1
2
3
4
5
6
7
8
9
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void setEventRule()
{
    ;
 
    rule = this.createEventRule();
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

The method createEventRule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
EventRule createEventRule()
{
    EventRule   localRule;
    ;
 
    localRule.initValue();
    localRule.Subject         = "";
    localRule.Message         = "";
    localRule.TypeId          = 1527;
    localRule.AlertTableId    = 65579;
    localRule.PrimTableId     = 65531;
    localRule.Until           = EventUntil::Always;
    localRule.Enabled         = NoYes::Yes;
    localRule.AlertQbdsNo     = 1;
    localRule.FormName        = "SysUserInfo";
    localRule.TypeTrigger     = 
    EventTypeTrigger::FieldChanged;
    localRule.RuleCondition   = EventRuleCondition::Current;
    localRule.AlertFieldLabel = "Alias";
    localRule.ShowPopup       = NoYes::Yes;
 
    localRule.insert();
 
    return localRule;
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

This method creates an event rule that we can use when we send the alerts to the users. What kind of rule this is and what it is normally for does not matter as long as the data is there when we send the alerts. In this case I create a rule that says that if my user id in Dynamics AX is changed, then an alert should be sent. This rule will be removed later on in the run method, so what kind and what data is in there is not that important. The important data is entered in the form previously described.

Back to the run method, we have now found a rule and want to loop through the SysClientSessions table which contains all of the online users. For each user we will create the alert. But first we want to create an instance of the EventType class, this class is going to provide us with necessary data for the event data table:

The setEventType method:

1
2
3
4
5
6
7
8
9
10
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void setEventType()
{
    ;
 
    eventType = EventType::construct(2195, 
                EventTypeTrigger::FieldChanged);
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

I’m using the class ID 2195 which is the event type class EventTypeDueIn. It should not matter which one you use. But this one did the trick for me, so I don’t see the point in switching to one of the others.

The createInbox method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void createInbox()
{
    ;
 
    inboxId = EventInbox::nextEventId();
 
    inbox.initValue();
    inbox.initFromEventRule(rule);
    inbox.ShowPopup = NoYes::Yes;
    inbox.SendEmail = false;
    inbox.AlertTableId = 1;
    inbox.AlertFieldId = 1;
    inbox.RuleId    = rule.RuleId;
    inbox.UserId = sessions.userId;
    inbox.InboxId = inboxId;
    inbox.TypeTrigger = EventTypeTrigger::RecordDelete;
    inbox.EmailRecipient = "";
    inbox.Subject = subject;
    inbox.Message = message;
    inbox.AlertCreatedDate = systemdateget();
    inbox.AlertCreateTime = timeNow();
    inbox.AlertedFor = reason;
    inbox.insert();
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

This method created an inbox message for the user that is current in the loop. You might notice that I use the rule for some of the data, and also the use of the enum value EventTypeTrigger::RecordDelete. This value makes it impossible for the user to go to “Alert origin” in the “View alerts” form. Since there is no Alert origin, we want to prevent that.

Moving on with the createData method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void createData()
{
    ;
 
    data.InboxId     =   inboxId;
    data.DataType    =   EventInboxDataType::Context;
    data.Data        =   rule.contextInfo();
    data.insert();
    data.clear();
    data.InboxId     =   inboxId;
    data.DataType    =   EventInboxDataType::TypeData;
    data.Data        =   eventType.pack();
    data.insert();
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

Every alert requires two records in the EventInboxData table, one is for context data from the event rule and the other is type data from the event type class previously instantiated.

Every loop ends with the infoLogMessage method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void infoLogMessage(SysClientSessions _sessions)
{
    SysCompanyUserInfo      companyUserInfo = 
                            SysCompanyUserInfo::find(
                            _sessions.userId);
    EmplTable               emplTable = EmplTable::find(
                            companyUserInfo.EmplId);
    ;
 
    if(emplTable)
    {
        info(strFmt("Employee: %1", emplTable.EmplId));
        info(strFmt("Name: %1", emplTable.Name));
 
        if(emplTable.Email && sendEmail)
        {
            if(this.sendMail(emplTable.Email))
            {
                info("Email has been sent to user.");
            }
        }
        else if(!emplTable.Email)
        {
            info("No email has been sent.");
        }
    }
    else
    {
        info("User has no relation to the employee table.");
    }
 
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

This method doubles as an info method but it also sends an email to the user containing the message entered if and only if, the user is related to an employee and this employee has an email address entered in the email field. The email functionality is not triggered if the checkbox in the initial dialog is not checked.

The email is sent in the sendMail method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
boolean sendMail(Email _email)
{
    boolean                 ret;
    SysMailer               mail;
    SysEmailParameters      emailParams = 
                            SysEmailParameters::find();
    ;
 
    try
    {
        if(!emailParams)
            throw error("No valid email parameters.");
 
        mail = new SysMailer();
        mail.SMTPRelayServer(
             emailParams.SMTPRelayServerName);
        mail.fromAddress(
             "important_message@dynamicsax.com");
        mail.subject(reason + " - " + subject);
        mail.body(message);
        mail.tos().appendAddress(_email);
 
        mail.sendMail();
 
        ret = true;
    }
    catch(Exception::Error)
    {
        ret = false;
    }
 
    return ret;
 
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

The sendmail functionality requires a seperate setup, so if you don’t have the email parameters setup correctly, skip the email functionality entirely.

The last method that runs in the main method is the cleanUp method:

1
2
3
4
5
6
7
8
9
10
11
12
// START 080516 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
void cleanUp()
{
    EventRule   localRule = 
                EventRule::find(rule.RuleId, true);
    ;
 
    localRule.delete();
 
}
// END 080516 Fourone/EP (FO_Notifications2OnlineUsers)

This method cleans up after the class by deleting the created rule used. This means that the only trace left of this process is the alerts sent to the users. I have encountered when testing that the insert of the new rule triggers a template form where the user must choose a template to base the new rule on. If you get this, just click ok, it won’t matter because the rule will be deleted when the process is done anyway. The times I have gotten this template form, it only seem to appear the first time you run this class.

Normally when using change based alerts you must start a batch job that checks for changes, but this functionality doesn’t need that job at all. What controls the alerts to the users is the time entered in the Tools/Options/General tab/Alerts/Recieve alerts every(minutes) for each user.

As I gather the batch job for the change based alerts creates records in the EventInbox and EventInboxData tables, and the setting in Tools for your user is the parameter that decides how often the EventInbox is checked for new alerts. If I’m wrong about this, feel free to point that out.

To use this class, just create a menu item of the type Action and point it to this class. Then add the menu item to a menu or a menuitembutton in a form.

And again, remember that the requirement for the email functionality is that the email parameters is setup correctly.

This class has not been tested that intensively, so I would appreciate feedback on how it works for you. The tests that have been perfomed is in a Dynamics AX 4.0 Sp2 environment.

I will keep this post updated in case of new development in regards to functionality or fixed bugs. If you find something that doesn’t seem right, please keep me posted.

UPDATE!

As noted in the comments by Marc I have neglected to add the unpack and pack methods, here they are:

1
2
3
4
5
6
7
8
9
10
11
// START 080520 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
public boolean unpack(container packedClass)
{
    boolean ret;
 
    ret = super(packedClass);
 
    return ret;
}
// END 080520 Fourone/EP (FO_Notifications2OnlineUsers)
1
2
3
4
5
6
7
8
9
10
11
// START 080520 Fourone/EP (FO_Notifications2OnlineUsers)
// -- Description:
public container pack()
{
    container ret;
 
    ret = super();
 
    return ret;
}
// END 080520 Fourone/EP (FO_Notifications2OnlineUsers)

Basically they are the same as the override methods from the RunBase class, I don’t think I have changed them in any way except for adding my comments.

7 thoughts on “Send alerts/messages to online users

  1. Hello Erik,
    just playing with you class, but i am getting a error on and main method.

    Object could not be created because abstract method RunBase.pack() has not been implemented.

    i hae copy everything just as you have done it, unsure what i have done wrong :-s

  2. Sorry about that Marc, I have missed to add the pack and unpack method in my post. It’s been a while since i wrote it but my guess is that I missed it probably because I have just used the override method functionality to get the methods from the RunBase class. Since I haven’t changed them I left them out but forgot to mention this small but important fact in the post.

    Let me know how it works!

  3. I have modified method infoLogMessage because some users do not have relation to an employee account and I get the email account from table SysUserInfo:

    void infoLogMessage(SysClientSessions _sessions)
    {

    SysUserInfo sys;
    UserInfo userInfo;

    ;
    select * from sys
    where sys.id == _sessions.userId;

    select * from userInfo
    where userInfo.id == _sessions.userId;

    if(userInfo)
    {
    info(strFmt(“Employee: %1”,_sessions.userId));
    info(strFmt(“Name: %1”, userInfo.name));

    if(sys.Email && sendEmail)
    {
    if(this.sendMail(sys.Email))
    {
    info(“Email has been sent to user.”);
    }
    }
    else if(!sys.Email)
    {
    info(“No email has been sent.”);
    }
    }

    }

    Thanks for this useful resource.

Leave a Reply

Your email address will not be published. Required fields are marked *