File: System\Windows\Forms\Timer.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Runtime.InteropServices;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Implements a Windows-based timer that raises an event at user-defined intervals.
///  This timer is optimized for use in Win Forms applications and must be used in a window.
/// </summary>
[DefaultProperty(nameof(Interval))]
[DefaultEvent(nameof(Tick))]
[ToolboxItemFilter("System.Windows.Forms")]
[SRDescription(nameof(SR.DescriptionTimer))]
public class Timer : Component
{
    private int _interval = 100;
 
    private bool _enabled;
 
    private protected EventHandler? _onTimer;
 
    private GCHandle _timerRoot;
 
    // Holder for the HWND that handles our Timer messages.
    private TimerNativeWindow? _timerWindow;
 
    private readonly Lock _lock = new();
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="Timer"/> class.
    /// </summary>
    public Timer() : base()
    {
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="Timer"/> class with the specified container.
    /// </summary>
    public Timer(IContainer container) : this()
    {
        ArgumentNullException.ThrowIfNull(container);
 
        container.Add(this);
    }
 
    [SRCategory(nameof(SR.CatData))]
    [Localizable(false)]
    [Bindable(true)]
    [SRDescription(nameof(SR.ControlTagDescr))]
    [DefaultValue(null)]
    [TypeConverter(typeof(StringConverter))]
    public object? Tag { get; set; }
 
    /// <summary>
    ///  Occurs when the specified timer interval has elapsed and the timer is enabled.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.TimerTimerDescr))]
    public event EventHandler Tick
    {
        add => _onTimer += value;
        remove => _onTimer -= value;
    }
 
    /// <summary>
    ///  Disposes of the resources (other than memory) used by the timer.
    /// </summary>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _timerWindow?.StopTimer();
            Enabled = false;
        }
 
        _timerWindow = null;
        base.Dispose(disposing);
    }
 
    /// <summary>
    ///  Indicates whether the timer is running.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.TimerEnabledDescr))]
    public virtual bool Enabled
    {
        get => _timerWindow is null ? _enabled : _timerWindow.IsTimerRunning;
        set
        {
            lock (_lock)
            {
                if (_enabled == value)
                {
                    return;
                }
 
                _enabled = value;
 
                // At runtime, enable or disable the corresponding Windows timer
                if (DesignMode)
                {
                    return;
                }
 
                if (value)
                {
                    // Create the timer window if needed.
                    _timerWindow ??= new TimerNativeWindow(this);
 
                    _timerRoot = GCHandle.Alloc(this);
                    _timerWindow.StartTimer(_interval);
                }
                else
                {
                    _timerWindow?.StopTimer();
                    if (_timerRoot.IsAllocated)
                    {
                        _timerRoot.Free();
                    }
                }
            }
        }
    }
 
    /// <summary>
    ///  Indicates the time, in milliseconds, between timer ticks.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(100)]
    [SRDescription(nameof(SR.TimerIntervalDescr))]
    public int Interval
    {
        get => _interval;
        set
        {
            lock (_lock)
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException(nameof(value), value, string.Format(SR.TimerInvalidInterval, value, 0));
                }
 
                if (_interval == value)
                {
                    return;
                }
 
                _interval = value;
 
                if (Enabled && !DesignMode)
                {
                    // Change the timer value, don't tear down the timer itself.
                    _timerWindow?.RestartTimer(value);
                }
            }
        }
    }
 
    /// <summary>
    ///  Raises the <see cref="Tick"/> event.
    /// </summary>
    protected virtual void OnTick(EventArgs e) => _onTimer?.Invoke(this, e);
 
    /// <summary>
    ///  Starts the timer.
    /// </summary>
    public void Start() => Enabled = true;
 
    /// <summary>
    ///  Stops the timer.
    /// </summary>
    public void Stop() => Enabled = false;
 
    public override string ToString() => $"{base.ToString()}, Interval: {Interval}";
 
    private class TimerNativeWindow : NativeWindow
    {
        // The timer that owns the window
        private readonly Timer _owner;
 
        // The current id -- this is usually the same as TimerID but we also
        // use it as a flag of when our timer is running.
        private nuint _timerID;
 
        // An arbitrary timer ID.
        private static nuint s_timerID = 1;
 
        // Setting this when we are stopping the timer so someone can't restart it in the process.
        private bool _stoppingTimer;
 
        private readonly Lock _lock = new();
 
        internal TimerNativeWindow(Timer owner)
        {
            _owner = owner;
        }
 
        ~TimerNativeWindow()
        {
            // This call will work form the finalizer thread.
            StopTimer();
        }
 
        public bool IsTimerRunning => _timerID != 0 && !HWND.IsNull;
 
        private bool EnsureHandle()
        {
            if (HWND.IsNull)
            {
                // Create a totally vanilla invisible window just for WM_TIMER messages
                CreateParams cp = new()
                {
                    Style = 0,
                    ExStyle = 0,
                    ClassStyle = 0,
                    Caption = GetType().Name,
 
                    // Message only windows are cheaper and have fewer issues than
                    // full blown invisible windows.
                    Parent = HWND.HWND_MESSAGE
                };
 
                CreateHandle(cp);
            }
 
            Debug.Assert(!HWND.IsNull, "Could not create timer HWND.");
            return !HWND.IsNull;
        }
 
        /// <summary>
        ///  Returns true if we need to marshal across threads to access this timer's HWND.
        /// </summary>
        private static bool GetInvokeRequired(HWND hwnd)
        {
            if (!hwnd.IsNull)
            {
                return PInvoke.GetWindowThreadProcessId(hwnd, out _) != PInvokeCore.GetCurrentThreadId();
            }
 
            return false;
        }
 
        /// <summary>
        ///  Changes the interval of the timer without destroying the HWND.
        /// </summary>
        public void RestartTimer(int newInterval)
        {
            StopTimer(default, destroyHwnd: false);
            StartTimer(newInterval);
        }
 
        public void StartTimer(int interval)
        {
            if (_timerID == 0 && !_stoppingTimer)
            {
                if (EnsureHandle())
                {
                    _timerID = PInvoke.SetTimer(this, s_timerID, (uint)interval);
                    s_timerID++;
                }
            }
        }
 
        public void StopTimer() => StopTimer(default, destroyHwnd: true);
 
        /// <summary>
        ///  Stop the timer and optionally destroy the HWND.
        /// </summary>
        public void StopTimer(HWND hwnd, bool destroyHwnd)
        {
            if (hwnd.IsNull)
            {
                // This is the normal use case. The hwnd only has a value if it comes back from the WndProc.
                hwnd = HWND;
            }
 
            // Fire a message across threads to destroy the timer and HWND on the thread that created it.
            if (GetInvokeRequired(hwnd))
            {
                PInvokeCore.PostMessage(hwnd, PInvokeCore.WM_CLOSE);
                return;
            }
 
            lock (_lock)
            {
                if (_stoppingTimer || hwnd.IsNull || !PInvoke.IsWindow(hwnd))
                {
                    return;
                }
 
                if (_timerID != 0)
                {
                    try
                    {
                        _stoppingTimer = true;
                        PInvoke.KillTimer(hwnd, _timerID);
                    }
                    finally
                    {
                        _timerID = 0;
                        _stoppingTimer = false;
                    }
                }
 
                if (destroyHwnd)
                {
                    base.DestroyHandle();
                }
            }
 
            GC.KeepAlive(this);
        }
 
        /// <summary>
        ///  Destroy the handle, stopping the timer first.
        /// </summary>
        public override void DestroyHandle()
        {
            // Avoid recursing.
            StopTimer(default, destroyHwnd: false);
            Debug.Assert(_timerID == 0, "Destroying handle with timerID still set.");
            base.DestroyHandle();
        }
 
        protected override void OnThreadException(Exception e)
        {
            Application.OnThreadException(e);
        }
 
        public override void ReleaseHandle()
        {
            // Avoid recursing.
            StopTimer(default, destroyHwnd: false);
            Debug.Assert(_timerID == 0, "Destroying handle with timerID still set.");
            base.ReleaseHandle();
        }
 
        protected override void WndProc(ref Message m)
        {
            Debug.Assert(m.HWND == HWND && !HWND.IsNull, "Timer getting messages for other windows?");
 
            // For timer messages call the timer event.
            if (m.MsgInternal == PInvokeCore.WM_TIMER)
            {
                if (m.WParamInternal == _timerID)
                {
                    _owner.OnTick(EventArgs.Empty);
                    return;
                }
            }
            else if (m.MsgInternal == PInvokeCore.WM_CLOSE)
            {
                // This is a posted method from another thread that tells us we need
                // to kill the timer. The handle may already be gone, so we specify it here.
                StopTimer(m.HWND, destroyHwnd: true);
                return;
            }
 
            base.WndProc(ref m);
        }
    }
}