Jaa


Timer events on a page

Have you ever wanted to have an event raised every 10th second on a page in the RoleTailored Client?

Wait no more – here is how you can do just that in Microsoft Dynamics NAV 2009SP1.

A Timer control is a Non-Visual Add-In

I have seen a number of development platforms treat a Timer as a Non-Visual Add-In (including .net) – so I thought I would try to create a non-visual Add-In for NAV – and what better than create the Timer. A Timer should not be visible to the user, but it should be able to raise events.

There are different ways to create a Non-Visual control, but the most obvious method will not work.

Adding a control and setting Visible to FALSE – will cause the control to be optimized away – it will never be created.

You can however create a Non-Visual control in other ways:

  • Set the visible property to a global variable, which is false.
  • Set the size (and MinSize, MaxSize) of the Control to 0, 0.

The first approach would require you to add a variable called something like falsevar on each page you use the Timer Control – and that isn’t really what we want – so I will use the second approach.

Well – then everything seems pretty simple – right?

Yes and No.

It is very simple to create a non-visual control which instantiates a timer and fires events – Yes, but what if the service tier opens up a modal dialog (like a CONFIRM command) – then I would suggest that we do NOT keep firing events.

For this purpose our control needs to subscribe to two application level events.

Application.EnterThreadModal

Application.LeaveThreadModal

What is my Application? Well, that is of course the RoleTailored Client. Your WinForms Control gets created as a first class citizen in the RoleTailored Client and of course you have access to the Application events as well. In fact there are all kinds of things you can do and all kinds of things you shouldn’t do.

Always bare in mind that if you start to go outside the control itself – think whether this is necessary, think future compatibility if the RoleTailored Client changes various things and remember to clean up.

For the two events above – they are pretty clear – EnterThreadModal is fired when the application enters Modal state and LeaveThreadModal is fired when the application leaves the modal state.

Remember to clean up – your mother isn’t here!

When coding in .net you often don’t need to consider cleaning up – the garbage collector will come and clean everything up. Now that isn’t always true.

In the case of the Application Level events – when you subscribe to an event, you actually give the Application object a pointer to your object – telling it to call you whenever something happens. This in fact means that the garbage collector is not allowed to cleanup anymore – it doesn’t matter that the page is closed, your control is gone – the Application object still maintains a reference to your object and therefore it will stay.

Of course this doesn’t apply when you subscribe to events in your own control, since the object holding the reference to your object goes out of scope at the same time as yourself.

Hmmm – admitted – I am probably getting too nerdy now – but it is rather important to understand this in order to avoid memory leaks and these memory leaks will affect the RoleTailored Client – not only your Add-In.

Instead of going further into detail – the curious read can read much more about garbage collection on msdn: Garbage Collector Basics and Performance Hints.

Let’s look at the code

The way I have implemented the Timer control is like this

[ControlAddInExport("FreddyK.TimerControl")]
public class TimerControl : StringControlAddInBase, IStringControlAddInDefinition
{
EventHandler EnterThreadModal;
EventHandler LeaveThreadModal;
Timer timer = null;
int interval = 0;
int count = 0;

    /// <summary>
/// Constructor - Setup timer and Application event subscriptions
/// </summary>
public TimerControl()
{
EnterThreadModal = new EventHandler(Application_EnterThreadModal);
LeaveThreadModal = new EventHandler(Application_LeaveThreadModal);
Application.EnterThreadModal += EnterThreadModal;
Application.LeaveThreadModal += LeaveThreadModal;
timer = new Timer();
timer.Tick += new EventHandler(timer_Tick);
}

    /// <summary>
/// Dispose method - cleanup timer and Application event subscriptions
/// </summary>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
Application.EnterThreadModal -= EnterThreadModal;
Application.LeaveThreadModal -= LeaveThreadModal;
if (timer != null)
{
timer.Stop();
timer.Dispose();
timer = null;
}
}
}

    /// <summary>
/// Event handler for Application.EnterThreadModal
/// </summary>
void Application_EnterThreadModal(object sender, EventArgs e)
{
timer.Stop();
}

    /// <summary>
/// Event handler for Application.LeaveThreadModal
/// </summary>
void Application_LeaveThreadModal(object sender, EventArgs e)
{
if (timer.Interval != 0)
timer.Start();
}

    /// <summary>
/// Create the native Add-In Control
/// </summary>
protected override Control CreateControl()
{
// Create a panel with the size 0,0
Panel panel = new Panel();
panel.BorderStyle = BorderStyle.None;
panel.MinimumSize = new Size(0, 0);
panel.MaximumSize = new Size(0, 0);
panel.Size = new Size(0, 0);
return panel;
}
/// <summary>
/// Timer tick handler - raise the Service Tier Add-In Event
/// </summary>
void timer_Tick(object sender, EventArgs e)
{
// Stop the timer while running the add-in Event
timer.Stop();
// Invoke event
this.RaiseControlAddInEvent(this.count++, "");
// Restart the timer
timer.Start();
}

    /// <summary>
/// Override to specify that Caption should be omitted
/// </summary>
public override bool AllowCaptionControl
{
get
{
return false;
}
}

    /// <summary>
/// Override to specify that value has not changed
/// </summary>
public override bool HasValueChanged
{
get
{
return false;
}
}

    /// <summary>
/// Value for the Timer Control - the value is the number of 1/10's of a second between Tick events
/// NOTE: every event is sent from the Client to the Service Tier - meaning that this is not intended
/// for events executing more frequently than 1/10's of a second
/// </summary>
public override string Value
{
get
{
return base.Value;
}
set
{
base.Value = value;
if (!int.TryParse(value, out interval))
{
interval = 0;
}
interval = interval * 100;
if (timer != null && timer.Interval != interval)
{
timer.Interval = interval;
count = 0;
if (interval == 0)
timer.Stop();
else
timer.Start();
            }
}
}
}

A couple of things to note

  • The Value is set on the Control even it doesn’t seem necessary – that is the reason for checking whether the interval has changed before doing anything.
  • We don’t really use the native control, the Panel(0,0), for anything – it is only there for the RoleTailored Client to have something to hold on to – returning null causes the RoleTailored Client to display an Add-In error.
  • I stop the timer while running the server side event. The primary reason for this is to ensure we don’t get multiple events triggered simultaneously and this causes the interval time to be applied after the event returns – not from the time the event started.
  • If you setup the Timer to trigger an event every 10 seconds – it will do so when there has been 10 seconds without any modal dialogs. If this isn’t what you want, you should setup the trigger to fire every second and look when the Add-In event Index parameter is 10.

How to use the Control

For a test, we create a sample page like this:

image

with the following global variables:

image

and the following triggers:

OnOpenPage()
timer := '10';

timer - OnControlAddIn(Index : Integer;Data : Text[1024])
count := Index;

As you can see, the timer is set to trigger once a second and the Index in the AddIn event actually counts the number of times the trigger has been fired, so the count will be counting.

Now you might wonder - why is the Timer caption Timer – DO NOT REMOVE?

The reason for this is, that the RoleTailored Client doesn’t really know about the concept Non-Visual controls and as you probably know, personalization can remove everything from a page – including your timer:

image

If you remove this control – the Timer will of course stop.

You can find the Visual Studio project and the TimerTest.fob here.

Enjoy

Freddy Kristiansen PM Architect
Microsoft Dynamics NAV

Comments

  • Anonymous
    November 04, 2009
    Hi Freddy, Thanks for this article, it certainly is interesting.   I have a quick question that relates to ControlAddIn's in general, but this one made me wonder. How would you pass data from the ControlAddIn back to the Application.. i.e.  your Timed Event pops up a window every hour, and the user fills something in, that you then need to write back to the database etc.. via Webservices or whatever.. I know you could embed the web service url in the control etc, but is there any other way, to retrieve which Instance I am currently connected to. Is there a way to do this ? i.e.  know what my current connection url is.. Thanks, Steve

  • Anonymous
    November 04, 2009
    You cannot get to your “current connection” – in my samples I have a table where I setup the base URL for web services. The company name then needs to be added to the base URL and the page/codeunit + name. Sometimes web services can come in handy from an add-in – but you also have the ability to send back a string (limited to 1024 unfortunately) in your add-in event In the sample you specify – note that the timed event is written in AL code and you have full access to NAV there – no need to do anything special. /Freddy

  • Anonymous
    April 12, 2010
    The comment has been removed

  • Anonymous
    April 12, 2010
    You need to add the add-in described in this post to the client add-in table (and place the compiled DLL in the Add-Ins folder) There are posts on both my and Christians blog on how to add add-ins to that table (find the public key etc.)

  • Anonymous
    May 25, 2010
    Hi Freddy, and thanks for making my life easier with ur posts :-) I have used your timer add-in, and i noticed something interesting. On the rolecenter i added a chart add-in with some code on the trigger OnAfterGetRecord, to update the xml for the addin. If i run a page with a timer on it, then the chart on the Rolecenter starts refreshing, and if the data has been modified , i can see it in the chart. On the OnControlAddIn of the timer i just wrote CurrPage.Update. It acts as if it was doing an Update on all the open pages including the role center. Can you give an explanation for this ? Also, if on a page part i put a chart add-in and the timer, and afterwards i try to put this page part in a page, then my client crashes without any error. Any idea why ? :) Thanks, Anca

  • Anonymous
    September 01, 2010
    hi freedy, Thanks for this Post.  I tried the Objects and u r test page it is working. this DLL file i used for another page. when i run the Object from OBJECT DESIGNER it is running perfeclty., from the navigation pane of the RTC, if i am trying to run the same PAGE. the RTC is closes.

  • Anonymous
    September 13, 2010
    It doesn't work if you link the page to a role center page....it crashes the RTC why?

  • Anonymous
    October 23, 2010
    I'm trying to use this on a List page that I open from the Departments menusuite and it crashes the RTC for me as well.  Has anyone figured out why that is happening?  If I run the page from the object designer it works okay...although my list format is messed up since I can't put the timer within the Repeater group, and as soon as I put it outside the repeater things get ugly.  :)

  • Anonymous
    November 08, 2010
    I found that it blew up for me because the Int.TryParse was the right idea, but then there is this bit of code: if (!int.TryParse(value, out interval))            {                interval = 0;            } Zero is not a valid interval for the Timer.  So when the page was loading up and Value isn't getting sent to the control corrected the first 3 or 4 times (it sends DBNull.value about 4 times before it sends the real value), it just crashes.   You can escape this simply by changing the "interval = 0;" to "interval = 100000;", which will set a very large interval until your real one gets sent to the control. Hopefully that makes sense to folks also. PS:  the "Count" control on the page?   1) You don't have to have that, Freddy was just doing an illustration so you can see the timer ticking away. 2) You also don't have to capture the Index value on the OnControlAddIn trigger if you have no need of knowing how many ticks have occurred. 3) Don't name it a RESERVED KEYWORD ("Count") on a Page that has a SourceTable.   PS2:   Whenever I'm doing a Set on Value for addins, I find that it's helpful to do this: if (value != DBNull.Value)                {                    if (value is int)                    {                          myintvalue = (int)value) [etc...]

  • Anonymous
    November 10, 2010
    Hi, first of all, many thanks to freddy for that addin. and thanks to Jeremy for fixing the rtc crash problem. But, i had insert the timer addin in a listpart of a role center to set the filter of the repeater of the listpart. The timer only starts if i refresh the list part manualy. how can i fix that problem? A refresh of the rolecenter page doesn't fix my problem. Thanks Regards Andreas Just

  • Anonymous
    April 06, 2011
    Hi Freddy You wrote: "if the service tier opens up a modal dialog (like a CONFIRM command) – then I would suggest that we do NOT keep firing events" Let's say we want to fire the event when a modal dialog is open. Do you see any problems doing that? In my case I have to check every second for a file and process it... and it's important that that event is always fired. However, I experienced the following problem:

  • Page A has a timer and calls RaiseControlAddInEvent every second
  • Page B calls a Report which is doing something else and opens first a dialog (d.OPEN)... and then shows a MESSAGE ... without (!) closing the dialog with D.CLOSE first. Once the MESSAGE code is executed, the RTC crashes. If  I close the dialog before showing the message, everything works fine. The trouble is, that I have no idea, which objects might cause the crash because the dialog is not closed before showing the MESSAGE. Any ideas how to solve this problem?