diff --git a/Source/NotifyIconWpf/TaskbarIcon.cs b/Source/NotifyIconWpf/TaskbarIcon.cs index 1fde567..ec7eee2 100644 --- a/Source/NotifyIconWpf/TaskbarIcon.cs +++ b/Source/NotifyIconWpf/TaskbarIcon.cs @@ -32,1010 +32,1006 @@ using System.Windows.Controls.Primitives; using System.Windows.Interop; using System.Windows.Threading; using Hardcodet.Wpf.TaskbarNotification.Interop; -using Point=Hardcodet.Wpf.TaskbarNotification.Interop.Point; - +using Point = Hardcodet.Wpf.TaskbarNotification.Interop.Point; namespace Hardcodet.Wpf.TaskbarNotification { - /// - /// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's - /// taskbar notification area ("system tray"). - /// - public partial class TaskbarIcon : FrameworkElement, IDisposable - { - #region Members - /// - /// Represents the current icon data. + /// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's + /// taskbar notification area ("system tray"). /// - private NotifyIconData iconData; - - /// - /// Receives messages from the taskbar icon. - /// - private readonly WindowMessageSink messageSink; - - /// - /// An action that is being invoked if the - /// fires. - /// - private Action delayedTimerAction; - - /// - /// A timer that is used to differentiate between single - /// and double clicks. - /// - private readonly Timer singleClickTimer; - - /// - /// A timer that is used to close open balloon tooltips. - /// - private readonly Timer balloonCloseTimer; - - /// - /// Indicates whether the taskbar icon has been created or not. - /// - public bool IsTaskbarIconCreated { get; private set; } - - /// - /// Indicates whether custom tooltips are supported, which depends - /// on the OS. Windows Vista or higher is required in order to - /// support this feature. - /// - public bool SupportsCustomToolTips + public partial class TaskbarIcon : FrameworkElement, IDisposable { - get { return messageSink.Version == NotifyIconVersion.Vista; } - } + #region Members + /// + /// Represents the current icon data. + /// + private NotifyIconData iconData; + /// + /// Receives messages from the taskbar icon. + /// + private readonly WindowMessageSink messageSink; - /// - /// Checks whether a non-tooltip popup is currently opened. - /// - private bool IsPopupOpen - { - get - { - var popup = TrayPopupResolved; - var menu = ContextMenu; - var balloon = CustomBalloon; + /// + /// An action that is being invoked if the + /// fires. + /// + private Action delayedTimerAction; - return popup != null && popup.IsOpen || - menu != null && menu.IsOpen || - balloon != null && balloon.IsOpen; + /// + /// A timer that is used to differentiate between single + /// and double clicks. + /// + private readonly Timer singleClickTimer; - } - } + /// + /// A timer that is used to close open balloon tooltips. + /// + private readonly Timer balloonCloseTimer; - #endregion + /// + /// Indicates whether the taskbar icon has been created or not. + /// + public bool IsTaskbarIconCreated { get; private set; } - - #region Construction - - /// - /// Inits the taskbar icon and registers a message listener - /// in order to receive events from the taskbar area. - /// - public TaskbarIcon() - { - //using dummy sink in design mode - messageSink = Util.IsDesignMode - ? WindowMessageSink.CreateEmpty() - : new WindowMessageSink(NotifyIconVersion.Win95); - - //init icon data structure - iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle); - - //create the taskbar icon - CreateTaskbarIcon(); - - //register event listeners - messageSink.MouseEventReceived += OnMouseEvent; - messageSink.TaskbarCreated += OnTaskbarCreated; - messageSink.ChangeToolTipStateRequest += OnToolTipChange; - messageSink.BalloonToolTipChanged += OnBalloonToolTipChanged; - - //init single click / balloon timers - singleClickTimer = new Timer(DoSingleClickAction); - balloonCloseTimer = new Timer(CloseBalloonCallback); - - //register listener in order to get notified when the application closes - if (Application.Current != null) Application.Current.Exit += OnExit; - } - - #endregion - - - #region Custom Balloons - - /// - /// Shows a custom control as a tooltip in the tray location. - /// - /// - /// An optional animation for the popup. - /// The time after which the popup is being closed. - /// Submit null in order to keep the balloon open inde - /// - /// If - /// is a null reference. - public void ShowCustomBalloon(UIElement balloon, PopupAnimation animation, int? timeout) - { - Dispatcher dispatcher = this.GetDispatcher(); - if (!dispatcher.CheckAccess()) - { - var action = new Action(() => ShowCustomBalloon(balloon, animation, timeout)); - dispatcher.Invoke(DispatcherPriority.Normal, action); - return; - } - - if (balloon == null) throw new ArgumentNullException("balloon"); - if (timeout.HasValue && timeout < 500) - { - string msg = "Invalid timeout of {0} milliseconds. Timeout must be at least 500 ms"; - msg = String.Format(msg, timeout); - throw new ArgumentOutOfRangeException("timeout", msg); - } - - EnsureNotDisposed(); - - //make sure we don't have an open balloon - lock (this) - { - CloseBalloon(); - } - - //create an invisible popup that hosts the UIElement - Popup popup = new Popup(); - popup.AllowsTransparency = true; - - //provide the popup with the taskbar icon's data context - UpdateDataContext(popup, null, DataContext); - - //don't animate by default - devs can use attached - //events or override - popup.PopupAnimation = animation; - - //in case the balloon is cleaned up through routed events, the - //control didn't removed the balloon from its parent popup when - //if was closed the last time - just make sure it doesn't have - //a parent that is a popup - var parent = LogicalTreeHelper.GetParent(balloon) as Popup; - if (parent != null) parent.Child = null; - - if (parent != null) - { - string msg = - "Cannot display control [{0}] in a new balloon popup - that control already has a parent. You may consider creating new balloons every time you want to show one."; - msg = String.Format(msg, balloon); - throw new InvalidOperationException(msg); - } - - popup.Child = balloon; - - //don't set the PlacementTarget as it causes the popup to become hidden if the - //TaskbarIcon's parent is hidden, too... - //popup.PlacementTarget = this; - - popup.Placement = PlacementMode.AbsolutePoint; - popup.StaysOpen = true; - - Point position = TrayInfo.GetTrayLocation(); - popup.HorizontalOffset = position.X -1; - popup.VerticalOffset = position.Y -1; - - //store reference - lock (this) - { - SetCustomBalloon(popup); - } - - //assign this instance as an attached property - SetParentTaskbarIcon(balloon, this); - - //fire attached event - RaiseBalloonShowingEvent(balloon, this); - - //display item - popup.IsOpen = true; - - if (timeout.HasValue) - { - //register timer to close the popup - balloonCloseTimer.Change(timeout.Value, Timeout.Infinite); - } - } - - - /// - /// Resets the closing timeout, which effectively - /// keeps a displayed balloon message open until - /// it is either closed programmatically through - /// or due to a new - /// message being displayed. - /// - public void ResetBalloonCloseTimer() - { - if (IsDisposed) return; - - lock (this) - { - //reset timer in any case - balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - - /// - /// Closes the current , if the - /// property is set. - /// - public void CloseBalloon() - { - if (IsDisposed) return; - - Dispatcher dispatcher = this.GetDispatcher(); - if (!dispatcher.CheckAccess()) - { - Action action = CloseBalloon; - dispatcher.Invoke(DispatcherPriority.Normal, action); - return; - } - - lock (this) - { - //reset timer in any case - balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite); - - //reset old popup, if we still have one - Popup popup = CustomBalloon; - if (popup != null) + /// + /// Indicates whether custom tooltips are supported, which depends + /// on the OS. Windows Vista or higher is required in order to + /// support this feature. + /// + public bool SupportsCustomToolTips { - UIElement element = popup.Child; - - //announce closing - RoutedEventArgs eventArgs = RaiseBalloonClosingEvent(element, this); - if (!eventArgs.Handled) - { - //if the event was handled, clear the reference to the popup, - //but don't close it - the handling code has to manage this stuff now - - //close the popup - popup.IsOpen = false; - - //remove the reference of the popup to the balloon in case we want to reuse - //the balloon (then added to a new popup) - popup.Child = null; - - //reset attached property - if (element != null) SetParentTaskbarIcon(element, null); - } - - //remove custom balloon anyway - SetCustomBalloon(null); - } - } - } - - - /// - /// Timer-invoke event which closes the currently open balloon and - /// resets the dependency property. - /// - private void CloseBalloonCallback(object state) - { - if (IsDisposed) return; - - //switch to UI thread - Action action = CloseBalloon; - this.GetDispatcher().Invoke(action); - } - - #endregion - - #region Process Incoming Mouse Events - - /// - /// Processes mouse events, which are bubbled - /// through the class' routed events, trigger - /// certain actions (e.g. show a popup), or - /// both. - /// - /// Event flag. - private void OnMouseEvent(MouseEvent me) - { - if (IsDisposed) return; - - switch (me) - { - case MouseEvent.MouseMove: - RaiseTrayMouseMoveEvent(); - //immediately return - there's nothing left to evaluate - return; - case MouseEvent.IconRightMouseDown: - RaiseTrayRightMouseDownEvent(); - break; - case MouseEvent.IconLeftMouseDown: - RaiseTrayLeftMouseDownEvent(); - break; - case MouseEvent.IconRightMouseUp: - RaiseTrayRightMouseUpEvent(); - break; - case MouseEvent.IconLeftMouseUp: - RaiseTrayLeftMouseUpEvent(); - break; - case MouseEvent.IconMiddleMouseDown: - RaiseTrayMiddleMouseDownEvent(); - break; - case MouseEvent.IconMiddleMouseUp: - RaiseTrayMiddleMouseUpEvent(); - break; - case MouseEvent.IconDoubleClick: - //cancel single click timer - singleClickTimer.Change(Timeout.Infinite, Timeout.Infinite); - //bubble event - RaiseTrayMouseDoubleClickEvent(); - break; - case MouseEvent.BalloonToolTipClicked: - RaiseTrayBalloonTipClickedEvent(); - break; - default: - throw new ArgumentOutOfRangeException("me", "Missing handler for mouse event flag: " + me); - } - - - //get mouse coordinates - Point cursorPosition = new Point(); - WinApi.GetCursorPos(ref cursorPosition); - - bool isLeftClickCommandInvoked = false; - - //show popup, if requested - if (me.IsMatch(PopupActivation)) - { - if (me == MouseEvent.IconLeftMouseUp) - { - //show popup once we are sure it's not a double click - delayedTimerAction = () => - { - LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); - ShowTrayPopup(cursorPosition); - }; - singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); - isLeftClickCommandInvoked = true; - } - else - { - //show popup immediately - ShowTrayPopup(cursorPosition); - } - } - - - //show context menu, if requested - if (me.IsMatch(MenuActivation)) - { - if (me == MouseEvent.IconLeftMouseUp) - { - //show context menu once we are sure it's not a double click - delayedTimerAction = () => - { - LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); - ShowContextMenu(cursorPosition); - }; - singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); - isLeftClickCommandInvoked = true; - } - else - { - //show context menu immediately - ShowContextMenu(cursorPosition); - } - } - - //make sure the left click command is invoked on mouse clicks - if (me == MouseEvent.IconLeftMouseUp && !isLeftClickCommandInvoked) - { - //show context menu once we are sure it's not a double click - delayedTimerAction = () => LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); - singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); - } - - } - - #endregion - - #region ToolTips - - /// - /// Displays a custom tooltip, if available. This method is only - /// invoked for Windows Vista and above. - /// - /// Whether to show or hide the tooltip. - private void OnToolTipChange(bool visible) - { - //if we don't have a tooltip, there's nothing to do here... - if (TrayToolTipResolved == null) return; - - if (visible) - { - if (IsPopupOpen) - { - //ignore if we are already displaying something down there - return; + get { return messageSink.Version == NotifyIconVersion.Vista; } } - var args = RaisePreviewTrayToolTipOpenEvent(); - if (args.Handled) return; - TrayToolTipResolved.IsOpen = true; - - //raise attached event first - if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip); - - //bubble routed event - RaiseTrayToolTipOpenEvent(); - } - else - { - var args = RaisePreviewTrayToolTipCloseEvent(); - if (args.Handled) return; - - //raise attached event first - if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip); - - TrayToolTipResolved.IsOpen = false; - - //bubble event - RaiseTrayToolTipCloseEvent(); - } - } - - - /// - /// Creates a control that either - /// wraps the currently set - /// control or the string.
- /// If itself is already - /// a instance, it will be used directly. - ///
- /// We use a rather than - /// because there was no way to prevent a - /// popup from causing cyclic open/close commands if it was - /// placed under the mouse. ToolTip internally uses a Popup of - /// its own, but takes advance of Popup's internal - /// property which prevents this issue. - private void CreateCustomToolTip() - { - //check if the item itself is a tooltip - ToolTip tt = TrayToolTip as ToolTip; - - if (tt == null && TrayToolTip != null) - { - //create an invisible tooltip that hosts the UIElement - tt = new ToolTip(); - tt.Placement = PlacementMode.Mouse; - - //do *not* set the placement target, as it causes the popup to become hidden if the - //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through - //the ParentTaskbarIcon attached dependency property: - //tt.PlacementTarget = this; - - //make sure the tooltip is invisible - tt.HasDropShadow = false; - tt.BorderThickness = new Thickness(0); - tt.Background = System.Windows.Media.Brushes.Transparent; - - //setting the - tt.StaysOpen = true; - tt.Content = TrayToolTip; - } - else if (tt == null && !String.IsNullOrEmpty(ToolTipText)) - { - //create a simple tooltip for the string - tt = new ToolTip(); - tt.Content = ToolTipText; - } - - //the tooltip explicitly gets the DataContext of this instance. - //If there is no DataContext, the TaskbarIcon assigns itself - if (tt != null) - { - UpdateDataContext(tt, null, DataContext); - } - - //store a reference to the used tooltip - SetTrayToolTipResolved(tt); - } - - - /// - /// Sets tooltip settings for the class depending on defined - /// dependency properties and OS support. - /// - private void WriteToolTipSettings() - { - const IconDataMembers flags = IconDataMembers.Tip; - iconData.ToolTipText = ToolTipText; - - if (messageSink.Version == NotifyIconVersion.Vista) - { - //we need to set a tooltip text to get tooltip events from the - //taskbar icon - if (String.IsNullOrEmpty(iconData.ToolTipText) && TrayToolTipResolved != null) + /// + /// Checks whether a non-tooltip popup is currently opened. + /// + private bool IsPopupOpen { - //if we have not tooltip text but a custom tooltip, we - //need to set a dummy value (we're displaying the ToolTip control, not the string) - iconData.ToolTipText = "ToolTip"; - } - } + get + { + var popup = TrayPopupResolved; + var menu = ContextMenu; + var balloon = CustomBalloon; - //update the tooltip text - Util.WriteIconData(ref iconData, NotifyCommand.Modify, flags); - } - - #endregion - - #region Custom Popup - - /// - /// Creates a control that either - /// wraps the currently set - /// control or the string.
- /// If itself is already - /// a instance, it will be used directly. - ///
- /// We use a rather than - /// because there was no way to prevent a - /// popup from causing cyclic open/close commands if it was - /// placed under the mouse. ToolTip internally uses a Popup of - /// its own, but takes advance of Popup's internal - /// property which prevents this issue. - private void CreatePopup() - { - //check if the item itself is a popup - Popup popup = TrayPopup as Popup; - - if (popup == null && TrayPopup != null) - { - //create an invisible popup that hosts the UIElement - popup = new Popup(); - popup.AllowsTransparency = true; - - //don't animate by default - devs can use attached - //events or override - popup.PopupAnimation = PopupAnimation.None; - - //the CreateRootPopup method outputs binding errors in the debug window because - //it tries to bind to "Popup-specific" properties in case they are provided by the child. - //We don't need that so just assign the control as the child. - popup.Child = TrayPopup; - - //do *not* set the placement target, as it causes the popup to become hidden if the - //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through - //the ParentTaskbarIcon attached dependency property: - //popup.PlacementTarget = this; - - popup.Placement = PlacementMode.AbsolutePoint; - popup.StaysOpen = false; - } - - //the popup explicitly gets the DataContext of this instance. - //If there is no DataContext, the TaskbarIcon assigns itself - if (popup != null) - { - UpdateDataContext(popup, null, DataContext); - } - - //store a reference to the used tooltip - SetTrayPopupResolved(popup); - } - - - /// - /// Displays the control if - /// it was set. - /// - private void ShowTrayPopup(Point cursorPosition) - { - if (IsDisposed) return; - - //raise preview event no matter whether popup is currently set - //or not (enables client to set it on demand) - var args = RaisePreviewTrayPopupOpenEvent(); - if (args.Handled) return; - - if (TrayPopup != null) - { - //use absolute position, but place the popup centered above the icon - TrayPopupResolved.Placement = PlacementMode.AbsolutePoint; - TrayPopupResolved.HorizontalOffset = cursorPosition.X; - TrayPopupResolved.VerticalOffset = cursorPosition.Y; - - //open popup - TrayPopupResolved.IsOpen = true; - - - IntPtr handle = IntPtr.Zero; - if (TrayPopupResolved.Child != null) - { - //try to get a handle on the popup itself (via its child) - HwndSource source = (HwndSource)PresentationSource.FromVisual(TrayPopupResolved.Child); - if (source != null) handle = source.Handle; + return popup != null && popup.IsOpen || + menu != null && menu.IsOpen || + balloon != null && balloon.IsOpen; + } } - //if we don't have a handle for the popup, fall back to the message sink - if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle; + #endregion - //activate either popup or message sink to track deactivation. - //otherwise, the popup does not close if the user clicks somewhere else - WinApi.SetForegroundWindow(handle); + #region Construction - //raise attached event - item should never be null unless developers - //changed the CustomPopup directly... - if (TrayPopup != null) RaisePopupOpenedEvent(TrayPopup); - - //bubble routed event - RaiseTrayPopupOpenEvent(); - } - } - - #endregion - - #region Context Menu - - /// - /// Displays the if - /// it was set. - /// - private void ShowContextMenu(Point cursorPosition) - { - if (IsDisposed) return; - - //raise preview event no matter whether context menu is currently set - //or not (enables client to set it on demand) - var args = RaisePreviewTrayContextMenuOpenEvent(); - if (args.Handled) return; - - if (ContextMenu != null) - { - //use absolute positioning. We need to set the coordinates, or a delayed opening - //(e.g. when left-clicked) opens the context menu at the wrong place if the mouse - //is moved! - ContextMenu.Placement = PlacementMode.AbsolutePoint; - ContextMenu.HorizontalOffset = cursorPosition.X; - ContextMenu.VerticalOffset = cursorPosition.Y; - ContextMenu.IsOpen = true; - - //activate the message window to track deactivation - otherwise, the context menu - //does not close if the user clicks somewhere else - WinApi.SetForegroundWindow(messageSink.MessageWindowHandle); - - //bubble event - RaiseTrayContextMenuOpenEvent(); - } - } - - #endregion - - #region Balloon Tips - - /// - /// Bubbles events if a balloon ToolTip was displayed - /// or removed. - /// - /// Whether the ToolTip was just displayed - /// or removed. - private void OnBalloonToolTipChanged(bool visible) - { - if (visible) - { - RaiseTrayBalloonTipShownEvent(); - } - else - { - RaiseTrayBalloonTipClosedEvent(); - } - } - - /// - /// Displays a balloon tip with the specified title, - /// text, and icon in the taskbar for the specified time period. - /// - /// The title to display on the balloon tip. - /// The text to display on the balloon tip. - /// A symbol that indicates the severity. - public void ShowBalloonTip(string title, string message, BalloonIcon symbol) - { - lock (this) - { - ShowBalloonTip(title, message, symbol.GetBalloonFlag(), IntPtr.Zero); - } - } - - - /// - /// Displays a balloon tip with the specified title, - /// text, and a custom icon in the taskbar for the specified time period. - /// - /// The title to display on the balloon tip. - /// The text to display on the balloon tip. - /// A custom icon. - /// If - /// is a null reference. - public void ShowBalloonTip(string title, string message, Icon customIcon) - { - if (customIcon == null) throw new ArgumentNullException("customIcon"); - - lock (this) - { - ShowBalloonTip(title, message, BalloonFlags.User, customIcon.Handle); - } - } - - - /// - /// Invokes in order to display - /// a given balloon ToolTip. - /// - /// The title to display on the balloon tip. - /// The text to display on the balloon tip. - /// Indicates what icon to use. - /// A handle to a custom icon, if any, or - /// . - private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle) - { - EnsureNotDisposed(); - - iconData.BalloonText = message ?? String.Empty; - iconData.BalloonTitle = title ?? String.Empty; - - iconData.BalloonFlags = flags; - iconData.CustomBalloonIconHandle = balloonIconHandle; - Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon); - } - - - /// - /// Hides a balloon ToolTip, if any is displayed. - /// - public void HideBalloonTip() - { - EnsureNotDisposed(); - - //reset balloon by just setting the info to an empty string - iconData.BalloonText = iconData.BalloonTitle = String.Empty; - Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info); - } - - #endregion - - #region Single Click Timer event - - /// - /// Performs a delayed action if the user requested an action - /// based on a single click of the left mouse.
- /// This method is invoked by the . - ///
- private void DoSingleClickAction(object state) - { - if (IsDisposed) return; - - //run action - Action action = delayedTimerAction; - if (action != null) - { - //cleanup action - delayedTimerAction = null; - - //switch to UI thread - this.GetDispatcher().Invoke(action); - } - } - - #endregion - - #region Set Version (API) - - /// - /// Sets the version flag for the . - /// - private void SetVersion() - { - iconData.VersionOrTimeout = (uint) NotifyIconVersion.Vista; - bool status = WinApi.Shell_NotifyIcon(NotifyCommand.SetVersion, ref iconData); - - if (!status) - { - iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win2000; - status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion); - } - - if (!status) - { - iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win95; - status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion); - } - - if (!status) - { - Debug.Fail("Could not set version"); - } - } - - #endregion - - #region Create / Remove Taskbar Icon - - /// - /// Recreates the taskbar icon if the whole taskbar was - /// recreated (e.g. because Explorer was shut down). - /// - private void OnTaskbarCreated() - { - IsTaskbarIconCreated = false; - CreateTaskbarIcon(); - } - - - /// - /// Creates the taskbar icon. This message is invoked during initialization, - /// if the taskbar is restarted, and whenever the icon is displayed. - /// - private void CreateTaskbarIcon() - { - lock (this) - { - if (!IsTaskbarIconCreated) + /// + /// Inits the taskbar icon and registers a message listener + /// in order to receive events from the taskbar area. + /// + public TaskbarIcon() { - const IconDataMembers members = IconDataMembers.Message - | IconDataMembers.Icon - | IconDataMembers.Tip; + //using dummy sink in design mode + messageSink = Util.IsDesignMode + ? WindowMessageSink.CreateEmpty() + : new WindowMessageSink(NotifyIconVersion.Win95); - //write initial configuration - var status = Util.WriteIconData(ref iconData, NotifyCommand.Add, members); - if (!status) - { - //couldn't create the icon - we can assume this is because explorer is not running (yet!) - //-> try a bit later again rather than throwing an exception. Typically, if the windows - // shell is being loaded later, this method is being reinvoked from OnTaskbarCreated - // (we could also retry after a delay, but that's currently YAGNI) - return; - } + //init icon data structure + iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle); - //set to most recent version - SetVersion(); - messageSink.Version = (NotifyIconVersion) iconData.VersionOrTimeout; + //create the taskbar icon + CreateTaskbarIcon(); - IsTaskbarIconCreated = true; - } - } - } + //register event listeners + messageSink.MouseEventReceived += OnMouseEvent; + messageSink.TaskbarCreated += OnTaskbarCreated; + messageSink.ChangeToolTipStateRequest += OnToolTipChange; + messageSink.BalloonToolTipChanged += OnBalloonToolTipChanged; - /// - /// Closes the taskbar icon if required. - /// - private void RemoveTaskbarIcon() - { - lock (this) - { - //make sure we didn't schedule a creation + //init single click / balloon timers + singleClickTimer = new Timer(DoSingleClickAction); + balloonCloseTimer = new Timer(CloseBalloonCallback); - if (IsTaskbarIconCreated) - { - Util.WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message); - IsTaskbarIconCreated = false; - } - } - } - - #endregion - - #region Dispose / Exit - - /// - /// Set to true as soon as - /// has been invoked. - /// - public bool IsDisposed { get; private set; } - - - /// - /// Checks if the object has been disposed and - /// raises a in case - /// the flag is true. - /// - private void EnsureNotDisposed() - { - if (IsDisposed) throw new ObjectDisposedException(Name ?? GetType().FullName); - } - - - /// - /// Disposes the class if the application exits. - /// - private void OnExit(object sender, EventArgs e) - { - Dispose(); - } - - - /// - /// This destructor will run only if the - /// method does not get called. This gives this base class the - /// opportunity to finalize. - /// - /// Important: Do not provide destructors in types derived from - /// this class. - /// - /// - ~TaskbarIcon() - { - Dispose(false); - } - - - /// - /// Disposes the object. - /// - /// This method is not virtual by design. Derived classes - /// should override . - /// - public void Dispose() - { - Dispose(true); - - // This object will be cleaned up by the Dispose method. - // Therefore, you should call GC.SupressFinalize to - // take this object off the finalization queue - // and prevent finalization code for this object - // from executing a second time. - GC.SuppressFinalize(this); - } - - - /// - /// Closes the tray and releases all resources. - /// - /// - /// Dispose(bool disposing) executes in two distinct scenarios. - /// If disposing equals true, the method has been called directly - /// or indirectly by a user's code. Managed and unmanaged resources - /// can be disposed. - /// - /// If disposing equals false, the method - /// has been called by the runtime from inside the finalizer and you - /// should not reference other objects. Only unmanaged resources can - /// be disposed. - /// Check the property to determine whether - /// the method has already been called. - private void Dispose(bool disposing) - { - //don't do anything if the component is already disposed - if (IsDisposed || !disposing) return; - - lock (this) - { - IsDisposed = true; - - //deregister application event listener - if (Application.Current != null) - { - Application.Current.Exit -= OnExit; + //register listener in order to get notified when the application closes + if (Application.Current != null) Application.Current.Exit += OnExit; } - //stop timers - singleClickTimer.Dispose(); - balloonCloseTimer.Dispose(); + #endregion - //dispose message sink - messageSink.Dispose(); + #region Custom Balloons - //remove icon - RemoveTaskbarIcon(); - } + /// + /// Shows a custom control as a tooltip in the tray location. + /// + /// + /// An optional animation for the popup. + /// The time after which the popup is being closed. + /// Submit null in order to keep the balloon open inde + /// + /// If + /// is a null reference. + public void ShowCustomBalloon(UIElement balloon, PopupAnimation animation, int? timeout) + { + Dispatcher dispatcher = this.GetDispatcher(); + if (!dispatcher.CheckAccess()) + { + var action = new Action(() => ShowCustomBalloon(balloon, animation, timeout)); + dispatcher.Invoke(DispatcherPriority.Normal, action); + return; + } + + if (balloon == null) throw new ArgumentNullException("balloon"); + if (timeout.HasValue && timeout < 500) + { + string msg = "Invalid timeout of {0} milliseconds. Timeout must be at least 500 ms"; + msg = String.Format(msg, timeout); + throw new ArgumentOutOfRangeException("timeout", msg); + } + + EnsureNotDisposed(); + + //make sure we don't have an open balloon + lock (this) + { + CloseBalloon(); + } + + //create an invisible popup that hosts the UIElement + Popup popup = new Popup(); + popup.AllowsTransparency = true; + + //provide the popup with the taskbar icon's data context + UpdateDataContext(popup, null, DataContext); + + //don't animate by default - devs can use attached + //events or override + popup.PopupAnimation = animation; + + //in case the balloon is cleaned up through routed events, the + //control didn't removed the balloon from its parent popup when + //if was closed the last time - just make sure it doesn't have + //a parent that is a popup + var parent = LogicalTreeHelper.GetParent(balloon) as Popup; + if (parent != null) parent.Child = null; + + if (parent != null) + { + string msg = + "Cannot display control [{0}] in a new balloon popup - that control already has a parent. You may consider creating new balloons every time you want to show one."; + msg = String.Format(msg, balloon); + throw new InvalidOperationException(msg); + } + + popup.Child = balloon; + + //don't set the PlacementTarget as it causes the popup to become hidden if the + //TaskbarIcon's parent is hidden, too... + //popup.PlacementTarget = this; + + popup.Placement = PlacementMode.AbsolutePoint; + popup.StaysOpen = true; + + Point position = TrayInfo.GetTrayLocation(); + popup.HorizontalOffset = position.X - 1; + popup.VerticalOffset = position.Y - 1; + + //store reference + lock (this) + { + SetCustomBalloon(popup); + } + + //assign this instance as an attached property + SetParentTaskbarIcon(balloon, this); + + //fire attached event + RaiseBalloonShowingEvent(balloon, this); + + //display item + popup.IsOpen = true; + + if (timeout.HasValue) + { + //register timer to close the popup + balloonCloseTimer.Change(timeout.Value, Timeout.Infinite); + } + } + + + /// + /// Resets the closing timeout, which effectively + /// keeps a displayed balloon message open until + /// it is either closed programmatically through + /// or due to a new + /// message being displayed. + /// + public void ResetBalloonCloseTimer() + { + if (IsDisposed) return; + + lock (this) + { + //reset timer in any case + balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + + /// + /// Closes the current , if the + /// property is set. + /// + public void CloseBalloon() + { + if (IsDisposed) return; + + Dispatcher dispatcher = this.GetDispatcher(); + if (!dispatcher.CheckAccess()) + { + Action action = CloseBalloon; + dispatcher.Invoke(DispatcherPriority.Normal, action); + return; + } + + lock (this) + { + //reset timer in any case + balloonCloseTimer.Change(Timeout.Infinite, Timeout.Infinite); + + //reset old popup, if we still have one + Popup popup = CustomBalloon; + if (popup != null) + { + UIElement element = popup.Child; + + //announce closing + RoutedEventArgs eventArgs = RaiseBalloonClosingEvent(element, this); + if (!eventArgs.Handled) + { + //if the event was handled, clear the reference to the popup, + //but don't close it - the handling code has to manage this stuff now + + //close the popup + popup.IsOpen = false; + + //remove the reference of the popup to the balloon in case we want to reuse + //the balloon (then added to a new popup) + popup.Child = null; + + //reset attached property + if (element != null) SetParentTaskbarIcon(element, null); + } + + //remove custom balloon anyway + SetCustomBalloon(null); + } + } + } + + + /// + /// Timer-invoke event which closes the currently open balloon and + /// resets the dependency property. + /// + private void CloseBalloonCallback(object state) + { + if (IsDisposed) return; + + //switch to UI thread + Action action = CloseBalloon; + this.GetDispatcher().Invoke(action); + } + + #endregion + + #region Process Incoming Mouse Events + + /// + /// Processes mouse events, which are bubbled + /// through the class' routed events, trigger + /// certain actions (e.g. show a popup), or + /// both. + /// + /// Event flag. + private void OnMouseEvent(MouseEvent me) + { + if (IsDisposed) return; + + switch (me) + { + case MouseEvent.MouseMove: + RaiseTrayMouseMoveEvent(); + //immediately return - there's nothing left to evaluate + return; + case MouseEvent.IconRightMouseDown: + RaiseTrayRightMouseDownEvent(); + break; + case MouseEvent.IconLeftMouseDown: + RaiseTrayLeftMouseDownEvent(); + break; + case MouseEvent.IconRightMouseUp: + RaiseTrayRightMouseUpEvent(); + break; + case MouseEvent.IconLeftMouseUp: + RaiseTrayLeftMouseUpEvent(); + break; + case MouseEvent.IconMiddleMouseDown: + RaiseTrayMiddleMouseDownEvent(); + break; + case MouseEvent.IconMiddleMouseUp: + RaiseTrayMiddleMouseUpEvent(); + break; + case MouseEvent.IconDoubleClick: + //cancel single click timer + singleClickTimer.Change(Timeout.Infinite, Timeout.Infinite); + //bubble event + RaiseTrayMouseDoubleClickEvent(); + break; + case MouseEvent.BalloonToolTipClicked: + RaiseTrayBalloonTipClickedEvent(); + break; + default: + throw new ArgumentOutOfRangeException("me", "Missing handler for mouse event flag: " + me); + } + + + //get mouse coordinates + Point cursorPosition = new Point(); + WinApi.GetCursorPos(ref cursorPosition); + + bool isLeftClickCommandInvoked = false; + + //show popup, if requested + if (me.IsMatch(PopupActivation)) + { + if (me == MouseEvent.IconLeftMouseUp) + { + //show popup once we are sure it's not a double click + delayedTimerAction = () => + { + LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); + ShowTrayPopup(cursorPosition); + }; + singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); + isLeftClickCommandInvoked = true; + } + else + { + //show popup immediately + ShowTrayPopup(cursorPosition); + } + } + + + //show context menu, if requested + if (me.IsMatch(MenuActivation)) + { + if (me == MouseEvent.IconLeftMouseUp) + { + //show context menu once we are sure it's not a double click + delayedTimerAction = () => + { + LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); + ShowContextMenu(cursorPosition); + }; + singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); + isLeftClickCommandInvoked = true; + } + else + { + //show context menu immediately + ShowContextMenu(cursorPosition); + } + } + + //make sure the left click command is invoked on mouse clicks + if (me == MouseEvent.IconLeftMouseUp && !isLeftClickCommandInvoked) + { + //show context menu once we are sure it's not a double click + delayedTimerAction = + () => LeftClickCommand.ExecuteIfEnabled(LeftClickCommandParameter, LeftClickCommandTarget ?? this); + singleClickTimer.Change(WinApi.GetDoubleClickTime(), Timeout.Infinite); + } + } + + #endregion + + #region ToolTips + + /// + /// Displays a custom tooltip, if available. This method is only + /// invoked for Windows Vista and above. + /// + /// Whether to show or hide the tooltip. + private void OnToolTipChange(bool visible) + { + //if we don't have a tooltip, there's nothing to do here... + if (TrayToolTipResolved == null) return; + + if (visible) + { + if (IsPopupOpen) + { + //ignore if we are already displaying something down there + return; + } + + var args = RaisePreviewTrayToolTipOpenEvent(); + if (args.Handled) return; + + TrayToolTipResolved.IsOpen = true; + + //raise attached event first + if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip); + + //bubble routed event + RaiseTrayToolTipOpenEvent(); + } + else + { + var args = RaisePreviewTrayToolTipCloseEvent(); + if (args.Handled) return; + + //raise attached event first + if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip); + + TrayToolTipResolved.IsOpen = false; + + //bubble event + RaiseTrayToolTipCloseEvent(); + } + } + + + /// + /// Creates a control that either + /// wraps the currently set + /// control or the string.
+ /// If itself is already + /// a instance, it will be used directly. + ///
+ /// We use a rather than + /// because there was no way to prevent a + /// popup from causing cyclic open/close commands if it was + /// placed under the mouse. ToolTip internally uses a Popup of + /// its own, but takes advance of Popup's internal + /// property which prevents this issue. + private void CreateCustomToolTip() + { + //check if the item itself is a tooltip + ToolTip tt = TrayToolTip as ToolTip; + + if (tt == null && TrayToolTip != null) + { + //create an invisible tooltip that hosts the UIElement + tt = new ToolTip(); + tt.Placement = PlacementMode.Mouse; + + //do *not* set the placement target, as it causes the popup to become hidden if the + //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through + //the ParentTaskbarIcon attached dependency property: + //tt.PlacementTarget = this; + + //make sure the tooltip is invisible + tt.HasDropShadow = false; + tt.BorderThickness = new Thickness(0); + tt.Background = System.Windows.Media.Brushes.Transparent; + + //setting the + tt.StaysOpen = true; + tt.Content = TrayToolTip; + } + else if (tt == null && !String.IsNullOrEmpty(ToolTipText)) + { + //create a simple tooltip for the string + tt = new ToolTip(); + tt.Content = ToolTipText; + } + + //the tooltip explicitly gets the DataContext of this instance. + //If there is no DataContext, the TaskbarIcon assigns itself + if (tt != null) + { + UpdateDataContext(tt, null, DataContext); + } + + //store a reference to the used tooltip + SetTrayToolTipResolved(tt); + } + + + /// + /// Sets tooltip settings for the class depending on defined + /// dependency properties and OS support. + /// + private void WriteToolTipSettings() + { + const IconDataMembers flags = IconDataMembers.Tip; + iconData.ToolTipText = ToolTipText; + + if (messageSink.Version == NotifyIconVersion.Vista) + { + //we need to set a tooltip text to get tooltip events from the + //taskbar icon + if (String.IsNullOrEmpty(iconData.ToolTipText) && TrayToolTipResolved != null) + { + //if we have not tooltip text but a custom tooltip, we + //need to set a dummy value (we're displaying the ToolTip control, not the string) + iconData.ToolTipText = "ToolTip"; + } + } + + //update the tooltip text + Util.WriteIconData(ref iconData, NotifyCommand.Modify, flags); + } + + #endregion + + #region Custom Popup + + /// + /// Creates a control that either + /// wraps the currently set + /// control or the string.
+ /// If itself is already + /// a instance, it will be used directly. + ///
+ /// We use a rather than + /// because there was no way to prevent a + /// popup from causing cyclic open/close commands if it was + /// placed under the mouse. ToolTip internally uses a Popup of + /// its own, but takes advance of Popup's internal + /// property which prevents this issue. + private void CreatePopup() + { + //check if the item itself is a popup + Popup popup = TrayPopup as Popup; + + if (popup == null && TrayPopup != null) + { + //create an invisible popup that hosts the UIElement + popup = new Popup(); + popup.AllowsTransparency = true; + + //don't animate by default - devs can use attached + //events or override + popup.PopupAnimation = PopupAnimation.None; + + //the CreateRootPopup method outputs binding errors in the debug window because + //it tries to bind to "Popup-specific" properties in case they are provided by the child. + //We don't need that so just assign the control as the child. + popup.Child = TrayPopup; + + //do *not* set the placement target, as it causes the popup to become hidden if the + //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through + //the ParentTaskbarIcon attached dependency property: + //popup.PlacementTarget = this; + + popup.Placement = PlacementMode.AbsolutePoint; + popup.StaysOpen = false; + } + + //the popup explicitly gets the DataContext of this instance. + //If there is no DataContext, the TaskbarIcon assigns itself + if (popup != null) + { + UpdateDataContext(popup, null, DataContext); + } + + //store a reference to the used tooltip + SetTrayPopupResolved(popup); + } + + + /// + /// Displays the control if + /// it was set. + /// + private void ShowTrayPopup(Point cursorPosition) + { + if (IsDisposed) return; + + //raise preview event no matter whether popup is currently set + //or not (enables client to set it on demand) + var args = RaisePreviewTrayPopupOpenEvent(); + if (args.Handled) return; + + if (TrayPopup != null) + { + //use absolute position, but place the popup centered above the icon + TrayPopupResolved.Placement = PlacementMode.AbsolutePoint; + TrayPopupResolved.HorizontalOffset = cursorPosition.X; + TrayPopupResolved.VerticalOffset = cursorPosition.Y; + + //open popup + TrayPopupResolved.IsOpen = true; + + + IntPtr handle = IntPtr.Zero; + if (TrayPopupResolved.Child != null) + { + //try to get a handle on the popup itself (via its child) + HwndSource source = (HwndSource) PresentationSource.FromVisual(TrayPopupResolved.Child); + if (source != null) handle = source.Handle; + } + + //if we don't have a handle for the popup, fall back to the message sink + if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle; + + //activate either popup or message sink to track deactivation. + //otherwise, the popup does not close if the user clicks somewhere else + WinApi.SetForegroundWindow(handle); + + //raise attached event - item should never be null unless developers + //changed the CustomPopup directly... + if (TrayPopup != null) RaisePopupOpenedEvent(TrayPopup); + + //bubble routed event + RaiseTrayPopupOpenEvent(); + } + } + + #endregion + + #region Context Menu + + /// + /// Displays the if + /// it was set. + /// + private void ShowContextMenu(Point cursorPosition) + { + if (IsDisposed) return; + + //raise preview event no matter whether context menu is currently set + //or not (enables client to set it on demand) + var args = RaisePreviewTrayContextMenuOpenEvent(); + if (args.Handled) return; + + if (ContextMenu != null) + { + //use absolute positioning. We need to set the coordinates, or a delayed opening + //(e.g. when left-clicked) opens the context menu at the wrong place if the mouse + //is moved! + ContextMenu.Placement = PlacementMode.AbsolutePoint; + ContextMenu.HorizontalOffset = cursorPosition.X; + ContextMenu.VerticalOffset = cursorPosition.Y; + + ContextMenu.IsOpen = true; + + //activate the message window to track deactivation - otherwise, the context menu + //does not close if the user clicks somewhere else + WinApi.SetForegroundWindow(messageSink.MessageWindowHandle); + + //bubble event + RaiseTrayContextMenuOpenEvent(); + } + } + + #endregion + + #region Balloon Tips + + /// + /// Bubbles events if a balloon ToolTip was displayed + /// or removed. + /// + /// Whether the ToolTip was just displayed + /// or removed. + private void OnBalloonToolTipChanged(bool visible) + { + if (visible) + { + RaiseTrayBalloonTipShownEvent(); + } + else + { + RaiseTrayBalloonTipClosedEvent(); + } + } + + /// + /// Displays a balloon tip with the specified title, + /// text, and icon in the taskbar for the specified time period. + /// + /// The title to display on the balloon tip. + /// The text to display on the balloon tip. + /// A symbol that indicates the severity. + public void ShowBalloonTip(string title, string message, BalloonIcon symbol) + { + lock (this) + { + ShowBalloonTip(title, message, symbol.GetBalloonFlag(), IntPtr.Zero); + } + } + + + /// + /// Displays a balloon tip with the specified title, + /// text, and a custom icon in the taskbar for the specified time period. + /// + /// The title to display on the balloon tip. + /// The text to display on the balloon tip. + /// A custom icon. + /// If + /// is a null reference. + public void ShowBalloonTip(string title, string message, Icon customIcon) + { + if (customIcon == null) throw new ArgumentNullException("customIcon"); + + lock (this) + { + ShowBalloonTip(title, message, BalloonFlags.User, customIcon.Handle); + } + } + + + /// + /// Invokes in order to display + /// a given balloon ToolTip. + /// + /// The title to display on the balloon tip. + /// The text to display on the balloon tip. + /// Indicates what icon to use. + /// A handle to a custom icon, if any, or + /// . + private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle) + { + EnsureNotDisposed(); + + iconData.BalloonText = message ?? String.Empty; + iconData.BalloonTitle = title ?? String.Empty; + + iconData.BalloonFlags = flags; + iconData.CustomBalloonIconHandle = balloonIconHandle; + Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon); + } + + + /// + /// Hides a balloon ToolTip, if any is displayed. + /// + public void HideBalloonTip() + { + EnsureNotDisposed(); + + //reset balloon by just setting the info to an empty string + iconData.BalloonText = iconData.BalloonTitle = String.Empty; + Util.WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info); + } + + #endregion + + #region Single Click Timer event + + /// + /// Performs a delayed action if the user requested an action + /// based on a single click of the left mouse.
+ /// This method is invoked by the . + ///
+ private void DoSingleClickAction(object state) + { + if (IsDisposed) return; + + //run action + Action action = delayedTimerAction; + if (action != null) + { + //cleanup action + delayedTimerAction = null; + + //switch to UI thread + this.GetDispatcher().Invoke(action); + } + } + + #endregion + + #region Set Version (API) + + /// + /// Sets the version flag for the . + /// + private void SetVersion() + { + iconData.VersionOrTimeout = (uint) NotifyIconVersion.Vista; + bool status = WinApi.Shell_NotifyIcon(NotifyCommand.SetVersion, ref iconData); + + if (!status) + { + iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win2000; + status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion); + } + + if (!status) + { + iconData.VersionOrTimeout = (uint) NotifyIconVersion.Win95; + status = Util.WriteIconData(ref iconData, NotifyCommand.SetVersion); + } + + if (!status) + { + Debug.Fail("Could not set version"); + } + } + + #endregion + + #region Create / Remove Taskbar Icon + + /// + /// Recreates the taskbar icon if the whole taskbar was + /// recreated (e.g. because Explorer was shut down). + /// + private void OnTaskbarCreated() + { + IsTaskbarIconCreated = false; + CreateTaskbarIcon(); + } + + + /// + /// Creates the taskbar icon. This message is invoked during initialization, + /// if the taskbar is restarted, and whenever the icon is displayed. + /// + private void CreateTaskbarIcon() + { + lock (this) + { + if (!IsTaskbarIconCreated) + { + const IconDataMembers members = IconDataMembers.Message + | IconDataMembers.Icon + | IconDataMembers.Tip; + + //write initial configuration + var status = Util.WriteIconData(ref iconData, NotifyCommand.Add, members); + if (!status) + { + //couldn't create the icon - we can assume this is because explorer is not running (yet!) + //-> try a bit later again rather than throwing an exception. Typically, if the windows + // shell is being loaded later, this method is being reinvoked from OnTaskbarCreated + // (we could also retry after a delay, but that's currently YAGNI) + return; + } + + //set to most recent version + SetVersion(); + messageSink.Version = (NotifyIconVersion) iconData.VersionOrTimeout; + + IsTaskbarIconCreated = true; + } + } + } + + /// + /// Closes the taskbar icon if required. + /// + private void RemoveTaskbarIcon() + { + lock (this) + { + //make sure we didn't schedule a creation + + if (IsTaskbarIconCreated) + { + Util.WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message); + IsTaskbarIconCreated = false; + } + } + } + + #endregion + + #region Dispose / Exit + + /// + /// Set to true as soon as + /// has been invoked. + /// + public bool IsDisposed { get; private set; } + + + /// + /// Checks if the object has been disposed and + /// raises a in case + /// the flag is true. + /// + private void EnsureNotDisposed() + { + if (IsDisposed) throw new ObjectDisposedException(Name ?? GetType().FullName); + } + + + /// + /// Disposes the class if the application exits. + /// + private void OnExit(object sender, EventArgs e) + { + Dispose(); + } + + + /// + /// This destructor will run only if the + /// method does not get called. This gives this base class the + /// opportunity to finalize. + /// + /// Important: Do not provide destructors in types derived from + /// this class. + /// + /// + ~TaskbarIcon() + { + Dispose(false); + } + + + /// + /// Disposes the object. + /// + /// This method is not virtual by design. Derived classes + /// should override . + /// + public void Dispose() + { + Dispose(true); + + // This object will be cleaned up by the Dispose method. + // Therefore, you should call GC.SupressFinalize to + // take this object off the finalization queue + // and prevent finalization code for this object + // from executing a second time. + GC.SuppressFinalize(this); + } + + + /// + /// Closes the tray and releases all resources. + /// + /// + /// Dispose(bool disposing) executes in two distinct scenarios. + /// If disposing equals true, the method has been called directly + /// or indirectly by a user's code. Managed and unmanaged resources + /// can be disposed. + /// + /// If disposing equals false, the method + /// has been called by the runtime from inside the finalizer and you + /// should not reference other objects. Only unmanaged resources can + /// be disposed. + /// Check the property to determine whether + /// the method has already been called. + private void Dispose(bool disposing) + { + //don't do anything if the component is already disposed + if (IsDisposed || !disposing) return; + + lock (this) + { + IsDisposed = true; + + //deregister application event listener + if (Application.Current != null) + { + Application.Current.Exit -= OnExit; + } + + //stop timers + singleClickTimer.Dispose(); + balloonCloseTimer.Dispose(); + + //dispose message sink + messageSink.Dispose(); + + //remove icon + RemoveTaskbarIcon(); + } + } + + #endregion } - - #endregion - } } \ No newline at end of file