0

I am experiencing an error in Excel while running a COM (Home made) Timer. Basically Excel instantiate the Timer, initializes it and starts it. The Timer then ticks every X milliseconds raising an event that Excel catches (pretty standard stuff). I am not using Excel itself as a timer because it does not tick faster than every second (which is too long for my purpose)

My issue is that if I click and hold on the spreadsheet while the event is raised by the timer, Excel crashes pretty bad. Unfortunately, the user needs (sometimes) to click in the spreadsheet and modify it while the timer is running.

I have seen somewhere that I could be using the IMessageFilter interface in my timer. This should make sure that if Excel is busy when the event is fired up the timer can just see this and act accordingly. However I was unable to implement it properly.

If somebody could help me out that would be great.

Here is the source code I am using:

In Excel I have a singleton that carries a WithEvents ExcelTimer.ExcelTimer object, here is the code of my singleton:

Option Explicit
Private Const m_sMODULE_NAME = "cTimerManager"

Public WithEvents oCsharpTimer As ExcelTimer.ExcelTimer

Private Sub Class_Initialize()
    Set oCsharpTimer = New ExcelTimer.ExcelTimer

    'The following two lines are called dynamically from somewhere else 
    'normally but for simplicity of my post I have put them here
    oCsharpTimer.Initialize 500  
    oCsharpTimer.StartTimer
End Sub

Private Sub oCsharpTimer_TimeTickEvt(ByVal o As Variant, ByVal Time As String)
    Const sPROCEDURE_NAME = "oCsharpTimer_TimeTickEvt"
    On Error GoTo ErrorHandler

    '"Send" confirmation with time to the COM object.
    oCsharpTimer.TimeReceived Time

    'Do whatever I wanna do when the event is trigger

CleanUp:
    Exit Sub

ErrorHandler:
    'My Error handling structure
    If ProcessError(m_sMODULE_NAME, sPROCEDURE_NAME, Err) Then
        Stop
        Resume
    Else
        Resume Next
    End If
End Sub

Here is the code for my COM Object:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Runtime.InteropServices;
using System.Windows.Forms;


namespace ExcelTimer
{
    public delegate void EventTimeRaiser(object o, string Time);

    //COM Interface
    public interface ICOMExcelTimer
    {
        [DispId(1)]        
        void StartTimer();
        [DispId(2)]
        void StopTimer();
        [DispId(3)]
        void Initialize(int TimeInMilliseconds);
        [DispId(4)]
        void TimeReceived(string ReceivedTime);
    }

    //Event interface 
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface ICOMExcelTimerEvent
    {
        [DispId(1000)]
        void TimeTickEvt(object o, string Time);
    }


    [ClassInterface(ClassInterfaceType.None),
    ComSourceInterfaces(typeof(ICOMExcelTimerEvent)),
    ComVisible(true)]
    public class ExcelTimer : ICOMExcelTimer, IMessageFilter
    {
        private event EventTimeRaiser TimeTickEvt;
        private bool _started;
        private bool _initialised;
        private int _timeInMilliseconds;
        private string _lastTimeReceived;
        private Control _control;
        private Thread _timerThread;
        private IAsyncResult _AsynchronousResult;

        [ComVisible(true)]
        public void Initialize(int TimeInMilliSeconds)
        {   
            //To be called by Excel to set which timer parameters it wants
            _timeInMilliseconds = TimeInMilliSeconds;
            _initialised = true;

            //Make sure we clear the last confirmation received 
            //since we are re-initialising the object
            _lastTimeReceived = ""; 
        }


        [ComVisible(true)]
        public void TimeReceived(string ReceivedTime)
        {
            //Store the last time received. Excel calls this function
            _lastTimeReceived = ReceivedTime;
        }

        public ExcelTimer()
        {
            _lastTimeReceived = "";
        }

        [ComVisible(true)]
        //Start the Timer
        public void StartTimer()
        {
            //If the timer has not been initialised just yet
            if (!_initialised)
            {
                //Sends back an error message to Excel
                TimeTickEvt(this, "Error: Timer Not Initialised");
                return;
            }

            try
            {
                //Start the timer
                _timerThread = new Thread(new ThreadStart(TimeTicking));

                //Start the Thread
                _started = true;
                _timerThread.Start();
            }
            catch (Exception ex)
            {
                System.IO.File.AppendAllText(@"C:\ErrorLog.txt", ex.Message + " - StartTimer - " + DateTime.Now.ToString("hh:mm:ss.f") + "\n");
            }
        }


        [ComVisible(true)]
        //Stop the timer
        public void StopTimer()
        {            
            //Stop the Thread
            _timerThread.Abort();
            //Change the status
            _started = false;
        }

        private void TimeTicking()
        {
            string SentTime;

            //As long as the timer is running
            while (_started)
            {
                try
                {
                    //Pause the timer for the right number of milliseconds

                    Thread.Sleep(_timeInMilliseconds);

                    SentTime = DateTime.Now.ToString("hh:mm:ss.ffff");

                    //########### The CODE Errors Here when Excel is busy!  ###########
                    //Raise an event for Excel to grab with the time that the thread finished the sleep at.
                    OnTimeTick(SentTime);

                    //_lastTimeReceived is used so that if the link between Excel and the Thread is broken the thread stops after sometimes
                    //if no confirmation was received from Excel.

                    //If no last time was received just yet, we setup the last time received to the sent time
                    if (_lastTimeReceived.Equals(""))
                    {
                        _lastTimeReceived = SentTime;
                    }
                    //If the last time received is older than 10 x TimeInMilliseconds (in Seconds) we stop the timer.
                    else if (Convert.ToDateTime(_lastTimeReceived).AddSeconds(_timeInMilliseconds * 10 / 1000) < Convert.ToDateTime(SentTime))
                    {
                        OnTimeTick("Timer timed out. No Confirmation for more than " + _timeInMilliseconds * 10 / 1000 + " second(s).");

                        //Stop the timer because the thread has not received a last time recently
                        _started = false;

                    }
                }
                catch (Exception ex)
                {
                    System.IO.File.AppendAllText(@"C:\ErrorLog.txt", ex.Message + " - TimeTicking - " + DateTime.Now.ToString("hh:mm:ss.f") + "\n");
                }

            }
        }

        protected virtual void OnTimeTick(string Time)
        {
            try
            {
                if (Time != null)
                {
                    //Raise the event
                    TimeTickEvt(this, Time);                    
                }
            }
            catch (Exception ex)
            {
                System.IO.File.AppendAllText(@"C:\ErrorLog.txt", ex.Message + " - OnTimeTick - " + DateTime.Now.ToString("hh:mm:ss.f") + "\n");
            }
        }        
    }
}
Chetter Hummin
  • 6,687
  • 8
  • 32
  • 44

1 Answers1

0

I can't help you with the C# coding, but if you would prefer to use Excel and a timer with more precision than 1 second, this can be done in VBA:

Public Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long

Public Declare Function timeGetTime Lib "winmm.dll" () As Long

You can then use the timeGetTime function from your subroutine to return the time in millisecond accuracy, if desired.

Credit where it's due:

http://www.excelforum.com/excel-programming-vba-macros/738087-using-milliseconds-in-vba.html?p=2748931&viewfull=1#post2748931

David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • Thanks for your answer. As a first thought, I think the problem with the approach described in the link is that the user does not get the hand on the sheet (clicking, modifying, etc.) while the timer's loop is on Sleep (even though there is a DoEvents). This is a big issue for my purpose. – Jeremie Coffin Apr 22 '13 at 03:28
  • You could add a userform, set it's `.Visible = False` and and then `.Show vbModeless` it during the execution. This would allow the user full ability to interact with the worksheet during the loop. – David Zemens Apr 22 '13 at 13:41
  • The problem is that I was doing this as a first step to get familiar on how to use C# multithreaded objects within Excel/VBA. My next step is a bit more complex (on the C# side) and I wanted to make sure that I could make the interaction with Excel/VBA smooth and efficient. – Jeremie Coffin Apr 29 '13 at 01:04