Ghost instances of indicators (WPF/Tabs)

I’m adding buttons to the menu bar, using TabChangeHanlder. All works well but after a while, and somewhat unpredictably…I get duplicate WPF buttons that are triggered by tab switches.

I dispose of everything correctly, unsubbed from all event handlers. But it seems NT is creating multiple instances of my indicator that hang around in the background and these are calling AddItems() when switching tabs. And I don’t know why. It seems to happen after a period of inactivity and is related to the TabChangeHandler being called and adding buttons. Once these instances are around they hang around in the background messing around with the menu on tab switches.

Does anyone have any experience of these issues? I’m following an almost identical structure to what is common with adding buttons to the menu bar and handling tab switching. Similar to that in the Sample WPF modifications file. (in this sample sometimes I’ve seen that duplicate buttons)

I’ve spent hours and hours trying to fix this and just when I think it’s fixed, it comes back. Sometimes can be hours, sometimes days. I’ve written a function that loops through the items in the menu and finds things that were created by my indicator that include a ‘tag’ I set but this seems like a hack. Sometimes there’s like 100+ icons that have accumulated and I’d like to get to the bottom of the problem.

Hi
Make sure you aggressively detach from ALL static or globally scoped event handlers in your DisposeOverride() method. If TabChangeHandler is a static/global event, the garbage collector can’t remove the indicator instance because the event handler still references it. This is the most common reason for duplicate AddItems() calls

GC.SuppressFinalize(): While generally discouraged for minor issues, for deep memory leaks like this where you are manually cleaning up unmanaged resources (like UI elements) you might need to ensure the runtime environment does not call the finalizer again, thus preventing lingering instances

Use Name Property for Identification Instead of relying on a tag (which you correctly called a hack) use the Name or AutomationId property of your buttons to check if the element already exists in the menu before calling AddItems() if it exists skip the creation this prevents duplicates while you fix the root memory leak

Resolving these type of deep memory/WPF integration conflicts requires a comprehensive review of the entire Indicator lifecycle, especially the Initialize() and DisposeOverride() structure I specialize in optimizing custom UI and Add-On integrations to eliminate memory leaks and ensure clean lifecycle management in NinjaTrader. I can review your full code logic to resolve this root issue permanently

1 Like

Thanks for the reply. Here is an example structure, I removed almost everything except the main elements.

I tried all various kinds of dispatcher calls, I tried wrapping the tab switcher in a dispatcher to delay it, all kinds of things.

The bug appears when you have more than 1 tab open and switch between, not always but at some points one tab will get multiple duplicate icons. Once it does, reloading the chart removes them(because destroyghostitems runs)…but when you switch tabs again, they come back.

I can fix it between having destroyghostitems run everytimre you tab switch but this isn’t getting to the bottom of the issue as to why there is a leak in the first place.

//
// Copyright (C) 2020, NinjaTrader LLC <www.ninjatrader.com>.
// NinjaTrader reserves the right to modify or overwrite this NinjaScript component with each release.
//
#region Using declarations
using System;
using System.ComponentModel.DataAnnotations;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript;
using Enums911501;
using System.ComponentModel;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    public class ExampleMenuIconDuplicateBug : Indicator
    {
        // Chart + menu objects
        private Chart myChartWindow;
        private NTMenuItem MyMenu;
        private Menu MyMenuContainer; // Used to attach style only

        private bool menuAdded;
        private TabItem tabItem;
        private ChartTab chartTab;
        // Styles pulled from NT skin

		public bool myBool1;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Name = "ExampleMenuIconDuplicateBug";
                IsOverlay = true;


                UserToolbarPosition = 10;

            }
            else if (State == State.DataLoaded)
            {
                if (ChartControl != null)
                {
                    ChartControl.Dispatcher.BeginInvoke(new Action(() =>
                    {
                        myChartWindow = Window.GetWindow(ChartControl.Parent) as Chart;
                        if (myChartWindow == null)
                            return;

                        RemoveGhostItems();
                        
                        if (!menuAdded)
                        {
                            QM_Initialize();

                        }
                    }), DispatcherPriority.Loaded);
                }
            }
            else if (State == State.Terminated)
            {
                if (myChartWindow != null)

					ChartControl.Dispatcher.InvokeAsync((Action)(() =>
					{
						try
						{
							QM_Dispose();
						}
						catch  (Exception ex)
						{
							Print(ex.Message);
						}								
					}));

            }
        }


        // Create menu structure
        private void QM_Initialize()
        {

            QM_MenuContainer();

            // Top-level NTMenuItem
            if (MyMenu == null)
            { 
                QM_MenuIcon(); // Our Menu Icon

                QM_PopulateMenu();

                MyMenuContainer.Items.Add(MyMenu);

				// Tab Change Handler
				if (myChartWindow != null)
				{
					myChartWindow.MainTabControl.SelectionChanged -= TabChangedHandler;
					myChartWindow.MainTabControl.SelectionChanged += TabChangedHandler;
				}

                if (TabSelected())
                    QM_AddMenuIcon();                
            }
        }

		// Menu Container hack
        private void QM_MenuContainer()
        {
            // Outer menu container (provides rendering context)
            if (MyMenuContainer == null)
            {
                MyMenuContainer = new Menu
                {
                    Name = "MyMenuContainer",
                    VerticalAlignment = VerticalAlignment.Center,
                    HorizontalAlignment = HorizontalAlignment.Left,
                    Margin = new Thickness(0),
                    Padding = new Thickness(0)
                };
            }
        }

        // Create our Menu Item(Button)
        private void QM_MenuIcon()
        {   
              // Our Main Menu Button
              Geometry menuIcon = Geometry.Parse(

				// Main symbol shape
				"M14,0c-1.1,0-2,.9-2,2,0,.66.32,1.24.82,1.61l-2.57,6.42c-.08-.01-.17-.03-.25-.03-.37,0-.71.11-1.01.28l-1.27-1.27c.18-.3.28-.64.28-1.01,0-1.1-.9-2-2-2s-2,.9-2,2c0,.51.2.97.51,1.33l-1.86,2.79c-.21-.07-.42-.12-.65-.12-1.1,0-2,.9-2,2s.9,2,2,2,2-.9,2-2c0-.51-.2-.97-.51-1.33l1.86-2.79c.21.07.42.12.65.12.37,0,.71-.11,1.01-.28l1.27,1.27c-.18.3-.28.64-.28,1.01,0,1.1.9,2,2,2s2-.9,2-2c0-.66-.32-1.24-.82-1.61l2.57-6.42c.08.01.17.03.25.03,1.1,0,2-.9,2-2s-.9-2-2-2ZM2,15.09c-.6,0-1.09-.49-1.09-1.09s.49-1.09,1.09-1.09,1.09.49,1.09,1.09-.49,1.09-1.09,1.09ZM4.91,8c0-.6.49-1.09,1.09-1.09s1.09.49,1.09,1.09-.49,1.09-1.09,1.09-1.09-.49-1.09-1.09ZM10,13c-.55,0-1-.45-1-1s.45-1,1-1,1,.45,1,1-.45,1-1,1ZM14,3.09c-.6,0-1.09-.49-1.09-1.09s.49-1.09,1.09-1.09,1.09.49,1.09,1.09-.49,1.09-1.09,1.09Z " +
				
				// The “U” character shape
				"M2.55,4.4c1.63,0,2.55-.83,2.55-2.15V0h-1.49v2.14c0,.63-.33,1.09-1.06,1.09s-1.07-.46-1.07-1.09V0H0v2.25c0,1.32.92,2.15,2.55,2.15Z " +

				// The “E” polygon converted into a valid closed path
				"M10.58,3.19L7.35,3.19L7.35,2.6L9.93,2.6L9.93,1.61L7.35,1.61L7.35,1.07L10.52,1.07L10.52,0L5.9,0L5.9,4.26L10.58,4.26Z"		
                );
                
			// Illustrator SVG settings 16x16 canvas
			// Object > make pixel perfect
			// expost SVG > decimals = 1 or 0
                
			MyMenu = new NTMenuItem
			{
				Name = "MyMenu",
				Icon = menuIcon,
				VerticalAlignment = VerticalAlignment.Center,
				HorizontalAlignment = HorizontalAlignment.Left,
				Margin = new Thickness(0),
				Padding = new Thickness(0),
				RenderTransform = new ScaleTransform(1.067, 1.067), // For some reason icon is drawn at 15x15 so we scal up
				RenderTransformOrigin = new Point(0.5, 0.5),
				SnapsToDevicePixels = true,
				UseLayoutRounding = true
			};
        }

        // Add menu to toolbar
        private void QM_AddMenuIcon()
        {
            if (myChartWindow == null || myChartWindow.MainMenu == null || menuAdded)
                return;

            // Add only the button to toolbar (not the whole menu - avoids spacing)
            if (UserToolbarPosition >= 0 && UserToolbarPosition < myChartWindow.MainMenu.Count)
                myChartWindow.MainMenu.Insert(UserToolbarPosition, MyMenuContainer);
            else
                myChartWindow.MainMenu.Add(MyMenuContainer);

            menuAdded = true;
        }

        // Populate our menu with items
        private void QM_PopulateMenu()
        {
            
		    MyMenu.Items.Add(QM_CreateMenuItem("My Bool 1"));

        }

        // Create Checkable Menu Item
        private NTMenuItem QM_CreateMenuItem(string _header)
        {
			
			var menuItem = new NTMenuItem
            {
                Header = _header,
                IsCheckable = true,
                IsChecked = true,
				IsEnabled = true,
				Tag = "",
                BorderThickness = new Thickness(0),
                FontSize = 10,
                FontWeight = FontWeights.Normal
            };
			
			return menuItem;
        }


        // Check if current tab is selected
        private bool TabSelected()
        {
            if (myChartWindow == null || myChartWindow.MainTabControl == null)
                return false;

            foreach (TabItem tab in myChartWindow.MainTabControl.Items)
            {
                if (tab.Content is ChartTab chartTabContent &&
                    chartTabContent.ChartControl == ChartControl &&
                    tab == myChartWindow.MainTabControl.SelectedItem)
                {
                    return true;
                }
            }

            return false;
        }

        // Tab change event handler
        private void TabChangedHandler(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count <= 0)
                return;

            tabItem = e.AddedItems[0] as TabItem;
            if (tabItem == null)
                return;

            chartTab = tabItem.Content as ChartTab;
            if (chartTab == null)
                return;

            if (TabSelected())
                QM_AddMenuIcon();
            else
                QM_RemoveMenuIcon();
        }

        // Remove menu from toolbar only (for tab switching)
        private void QM_RemoveMenuIcon()
        {
            if (myChartWindow?.MainMenu != null && MyMenu != null && 
                myChartWindow.MainMenu.Contains(MyMenuContainer))
            {
                myChartWindow.MainMenu.Remove(MyMenuContainer);
                
                menuAdded = false;
            }
        }

        // Remove ghost items from previous instances
        private void RemoveGhostItems()
        {
            if (myChartWindow?.MainMenu == null)
                return;

            var itemsToRemove = new System.Collections.Generic.List<object>();

            foreach (object item in myChartWindow.MainMenu)
            {
                if (item is NTMenuItem ntItem && 
                    ntItem.Name != null && 
                    ntItem.Name.StartsWith("MyMenu"))
                {
                    itemsToRemove.Add(item);
                }
            }

            foreach (object item in itemsToRemove)
            {
                myChartWindow.MainMenu.Remove(item);
            }
        }

        // Remove menu from chart (full cleanup)
        private void QM_Dispose()
        {
            try
            {

                // Unsub from TabChangedHandler
				if (myChartWindow != null)
				{
					myChartWindow.MainTabControl.SelectionChanged -= TabChangedHandler;
				}
			
                // Remove from toolbar
                if (myChartWindow?.MainMenu != null && MyMenuContainer != null && 
                    myChartWindow.MainMenu.Contains(MyMenuContainer))
                {
                    myChartWindow.MainMenu.Remove(MyMenuContainer);
                }
            }
            catch { }
            finally
            {
                menuAdded = false;
            }
        }

        protected override void OnBarUpdate() { }

        #region Persisted user settings

        [Display(Name = "Toolbar Position", Description = "Position in toolbar (0=first)", Order = 6, GroupName = "Menu States")]
        [Range(0, 100)]
        public int UserToolbarPosition { get; set; }

        #endregion
    }
}

namespace Enums911501
{


}

Add container tag when creating.
MyMenuContainer.Tag = “MyMenu”;

In QM_AddMenuIcon() before adding

for (int i = myChartWindow.MainMenu.Count - 1; i >= 0; i–)
{
if (myChartWindow.MainMenu[i] is Menu m && (string)m.Tag == “MyMenu”)
myChartWindow.MainMenu.RemoveAt(i);
}

1 Like

Would this not suffice? private void RemoveUIElement(UIElement element)
{
if (element != null && chartWindow.MainMenu.Contains(element))
chartWindow.MainMenu.Remove(element);
}

I was already using those methods. I can tell you one trick that helped. I had an old copy of DrawingToolTile on my machine from a couple of NT revisions ago and by accident I noticed a small change between it and the latest version;

They made this change:

				if (ChartControl != null)
				{
					ChartControl.Dispatcher.InvokeAsync(() => { UserControlCollection.Add(CreateControl()); });
				}

to this:

				if (ChartControl != null)
				{

					ChartControl.Dispatcher.InvokeAsync(() => { if (State < State.Terminated) UserControlCollection.Add(CreateControl()); });
				}

This helped, sometimes I would have some race conditions where state terminated and disposal of the WPF control would start to run before it had been loaded.

I do see NT still create ghost instances of indicators however, I’m not sure how to fix that.

1 Like