When is myTextBox.Text no longer null?

I got sick of NT crashing every time I change a setting in my Indicators window, so I was forced to create a custom text box to change a common value without having to re-load all the Indicators.

I have succeeded in creating the text box, and I can populate it with something, so that it shows on the screen as having the correct default data, but it is completely useless in my code, because myTextBox.Text always throws the BS “Object reference not set to an instance of an object.”

I have a class variable set:

	private string			_customControlText = "44";

I have this for adding the text box to my control grid:

	myTextBox				= new System.Windows.Controls.TextBox
		     {
		          Name 				= "StringInputBox",
		          Width 			= 80,
		          Text 				= _customControlText // Default value
		      };

As I’ve said, the box on the chart shows 44.

Here is where it crashes in OnBarUpdate:

	if (myTextBox.Text == null)
	{
		userInput	= bE.ToString();
	}
	else
	{
		userInput	= myTextBox.Text ?? "";
	}

It gives the “Object reference” error on Bar 5 every time.

If I have already set the value of the text box to a non-null value, why is myTextBox.Text coming back as null?

More to the point, how can I possibly be getting an error when I am checking whether it’s null or not? Isn’t that how you’re supposed to catch an error that would be caused by something being null?

I can see the text box on the screen, populated with the appropriate data, so I know myTextBox is not null.

When will myTextBox.Text become usable?

Do you know where it’s crashing? Is it because of the if statement or the else?
Do you know if both are already set? bE and myTextBox.Text?
is myTextBox initialized in Historical state?
Have you tried putting print statements to see if they actually print the values that should be set already? If it crashes before the a specific one, that could be the one causing you trouble…

You’re checking if myTextBox.Text == null, but in order for this check to work. myTextBox has to already exist.

instead first check for just myTextBox == null, then only if it’s not null can you check for .Text

It’s crashing on

if (myTextBox.Text == null)

because, if I change that to

if (userInput == null)

it works. I mean, it enters into the loop. Then, it crashes on the else, because userInput is no longer null, but myTextBox.Text somehow still is.

How could myTextBox ever be null on the 5th bar, if it didn’t crash on the first 4 bars, and it never gets set back to null? AND the text box, which IS myTextBox is visible on the screen, displaying values that are only assigned INSIDE myTextBox?

You need to show how / when you’re loading the WPF control

It’s in State.Historical

ChartControl.Dispatcher.InvokeAsync((() =>
	{
		myGrid2 = new System.Windows.Controls.Grid
			{
				Name = "myCustomGrid", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Bottom
			};
				System.Windows.Controls.ColumnDefinition column1 = new System.Windows.Controls.ColumnDefinition();
				column1.Width									 = new GridLength(80);
				System.Windows.Controls.ColumnDefinition column2 = new System.Windows.Controls.ColumnDefinition();
				column2.Width									 = new GridLength(140);
				System.Windows.Controls.ColumnDefinition column3 = new System.Windows.Controls.ColumnDefinition();
				column3.Width									 = new GridLength(140);
				System.Windows.Controls.ColumnDefinition column4 = new System.Windows.Controls.ColumnDefinition();
				column4.Width									 = new GridLength(140);
				myGrid2.ColumnDefinitions.Add(column1);
				myGrid2.ColumnDefinitions.Add(column2);
				myGrid2.ColumnDefinitions.Add(column3);
				myGrid2.ColumnDefinitions.Add(column4);
					
				System.Windows.Controls.RowDefinition row1  = new System.Windows.Controls.RowDefinition();
				System.Windows.Controls.RowDefinition row2  = new System.Windows.Controls.RowDefinition();
				System.Windows.Controls.RowDefinition row3  = new System.Windows.Controls.RowDefinition();
				row1.Height									= new GridLength(35);
				row2.Height									= new GridLength(35);
				row3.Height									= new GridLength(7);
				myGrid2.RowDefinitions.Add(row1);
				myGrid2.RowDefinitions.Add(row2);
				myGrid2.RowDefinitions.Add(row3);
				
    			autoOrder		 		= new System.Windows.Controls.Button
    				{
      					Name 				= "AutoOrder",
      					Content 			= "Manual Order",
      					Foreground 			= Brushes.White,
      					Background 			= Brushes.DarkGreen,
						FontStyle			= FontStyles.Normal,
						FontWeight			= FontWeights.Normal
        			};

				exitNow			 		= new System.Windows.Controls.Button
    				{
      					Name 				= "ExitNow",
      					Content 			= "Exit Now",
      					Foreground 			= Brushes.White,
      					Background 			= Brushes.Firebrick,
						FontStyle			= FontStyles.Normal,
						FontWeight			= FontWeights.Bold
        			};

				visLine			 		= new System.Windows.Controls.Button
    				{
      					Name 				= "ShowLine",
      					Content 			= "Show Line",
      					Foreground 			= Brushes.Black,
      					Background 			= Brushes.Goldenrod,
						FontStyle			= FontStyles.Normal,
						FontWeight			= FontWeights.Normal
        			};

				myTextBox				= new System.Windows.Controls.TextBox
		            {
		                Name 				= "StringInputBox",
		                Width 				= 80,
		                Text 				= _customControlText // Default value
		            };
					
	    			
   				autoOrder.Click			+= OnMyButtonClick;
				exitNow.Click			+= ExitNowClick;
				visLine.Click			+= ShowLineClick;
    				
    			// Define where the buttons should appear in the grid
	   			System.Windows.Controls.Grid.SetColumn(myTextBox, 0);
				System.Windows.Controls.Grid.SetColumn(visLine, 1);
				System.Windows.Controls.Grid.SetColumn(autoOrder, 2);
				System.Windows.Controls.Grid.SetColumn(exitNow, 3);
					
   				System.Windows.Controls.Grid.SetRow(myTextBox, 1);
				System.Windows.Controls.Grid.SetRow(visLine, 1);
				System.Windows.Controls.Grid.SetRow(autoOrder, 1);
				System.Windows.Controls.Grid.SetRow(exitNow, 1);
				
   				// Add the buttons as child to the custom grid
    			myGrid2.Children.Add(myTextBox);
				myGrid2.Children.Add(visLine);
				myGrid2.Children.Add(autoOrder);
				myGrid2.Children.Add(exitNow);
					
   				// Finally, add the completed grid to the custom NinjaTrader UserControlCollection
				UserControlCollection.Add(myGrid2);
	}

The other three buttons that are there have been working just fine for months, and they still work, as long as I REM out the new code scanning for myTextBox.

The text box is there and populated with 44. It works fine, as long as I don’t try and use myTextBox.

do you have anything in OnBarUpdate that skips the first 4 bars? or anything that skips bars in general? I would assume that is the case…

well, you’re loading the WPF control in state historical with a dispatcher with InvokeAsync() so it’s very late. Unclear why the error isn’t on Bar 0, show your onBarUpdate code.

But I go back to what I said earlier, I simple null check on the WPF object is all you need. Not a null check on a child element of it.

if (myTextBox != null)

{

Print("FOUND! myTextBox and we're on Bar: " + CurrentBar);

}
else
{
Print("myTextBox is still null and we're on Bar: " + CurrentBar);
}

1 Like

Okay, so…

myTextBox is still null and we're on Bar: 2527
myTextBox is still null and we're on Bar: 2528
myTextBox is still null and we're on Bar: 2529
FOUND! myTextBox and we're on Bar: 2530
FOUND! myTextBox and we're on Bar: 2531
FOUND! myTextBox and we're on Bar: 2532

Now what?

protected override void OnBarUpdate()
	{
		if (CurrentBar < 2530)
		{ return; }

		.......
	}

I can’t wait 2,500 candles for my Indicator to plot.

so… try to load it earlier?

I’m not entirely sure what you’re trying but you can try loading the WPF stuff in state data loaded, but it’s still not guaranteed to be done by bar 0.

Through the use of the right if statements and ChartControl.Dispatcher.InvokeAsync, I have reached the point where the data which is pre-populated in the text box is now usable, but I have encountered a different problem.

Whenever I try to enter information into the text box, I get this strange window that appears to be expecting me only to choose an Instrument because, if I enter a number, it gives me an error saying “Invalid instrument.”

I have found that there is such a thing as a NumericTextBox, which sounds like what I would want, as I am only trying to provide a place to change Indicator parameter settings on the fly, but the NumericTextBox doesn’t work. When I click in the box and try to type, absolutely nothing happens. When I press Enter, or click outside the box, I get the BS “Object reference not set” again.

Does anyone know how to code a working NumericTextBox? Or a regular text box that lets me enter something other than an Instrument? I’ve already figured out how to cast a numeric text string as double, so that would work, too.

Thanks in advance!

In the past I’ve used the .Loaded event on a control to detect when it’s ready, since I found it’s possible for your code to be faster than loading of controls.

I’ve also had to deal with that input stealing issue. Solution was to intercept the .PreviewKeyDown event before NT’s .KeyDown could get at the input.

I do some checks on the focused control to check if it’s one of mine, and if it’s a number or other relevant key:

		IInputElement focusedControl = Keyboard.FocusedElement;
		if (focusedControl == null || !(focusedControl is TextBox)) return;
		TextBox FocusedTextBox = focusedControl as TextBox;

//My boxes prefix. If false then return & allow NT to take control of the input
		if (!FocusedTextBox.Name.StartsWith("QT_")) return;

//Relevant key check
		int value = QT_Globals.NumberKeyToInt(e);
		if (value < 0 && (e.Key != Key.Delete && e.Key != Key.Back && e.Key != Key.Subtract && e.Key != Key.OemMinus && e.Key != Key.OemPeriod)) return;

// All checks passed.
// Flagging the event as handled stops it reaching
// NT's KeyDown handler, since PreviewKeyDown runs first.
		e.Handled = true;

After this I have to manually append the input to the textbox.text and handle outcomes for all other input combinations, like what if I’m in the middle of the text and hit backspace, or delete.

Just realised I cheated on some of the logic. I have it set so backspace & delete just remove the final character no matter the caret position. Obviously good enough as never noticed after all these years.

2 Likes

One additional thing I’d consider here is separating the TextBox input from the indicator’s calculation logic.

Since you’re creating the control asynchronously through the chart’s Dispatcher, I’d avoid reading myTextBox.Text directly from OnBarUpdate. Instead, you could handle TextChanged on the UI thread, validate the input with double.TryParse, and pass the resulting value to a separate field using appropriate thread synchronization. Then OnBarUpdate can use that value without depending on the WPF control’s initialization or thread.

For the keyboard issue, the PreviewKeyDown approach mentioned above is worth testing, but keep in mind that marking keys as handled may also prevent normal TextBox editing unless you implement that behavior yourself.

Are you still getting the “Invalid instrument” popup after the focus-handling changes, or is the remaining issue accessing the updated value from OnBarUpdate?

Building on the suggestions above, I’d consider keeping the TextBox input separate from OnBarUpdate entirely.

Since the control is created asynchronously through the chart’s Dispatcher, you could handle TextChanged on the UI thread, validate the input with double.TryParse, and store the parsed value in a separate field. OnBarUpdate can then read that value using appropriate thread synchronization, without directly accessing the WPF control.

For the “Invalid instrument” popup, the PreviewKeyDown approach mentioned above is worth testing. Just be careful with e.Handled, since it may also suppress normal TextBox editing behavior.

Since you’ve already resolved the initial null issue, is the remaining problem the keyboard/focus behavior, or accessing the updated value from OnBarUpdate?

The null is timing, not the TextBox. Dispatcher.InvokeAsync only queues the build on the chart’s thread. OnBarUpdate runs ahead on its own thread, so myTextBox doesn’t exist yet for the first ~2500 bars. OnBarUpdate also shouldn’t touch a WPF control directly, since it’s on the wrong thread.

The cleaner way is to bind the TextBox to a property. OnBarUpdate only reads or sets the property and never touches the control, so it doesn’t matter when the box gets built. It works both ways: typing changes the property, and setting the property in code changes the box.

Here’s a small working indicator that shows how this could be accomplished.

#region Using declarations
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Xml.Serialization;
using NinjaTrader.Gui.Chart;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
	public class HTTextBoxBindDemo : Indicator, INotifyPropertyChanged
	{
		private volatile string boxText = "44";
		private TextBox myTextBox;

		public event PropertyChangedEventHandler PropertyChanged;

		// The TextBox is bound to this. Typing changes it, and setting it in code changes the box.
		[Browsable(false)]
		[XmlIgnore]
		public string BoxText
		{
			get { return boxText; }
			set
			{
				boxText = value;
				if (PropertyChanged != null)
					PropertyChanged(this, new PropertyChangedEventArgs("BoxText"));
			}
		}

		protected override void OnStateChange()
		{
			if (State == State.SetDefaults)
			{
				Name		= "HTTextBoxBindDemo";
				Description	= "One TextBox bound both ways to a property.";
				Calculate	= Calculate.OnBarClose;
				IsOverlay	= true;
			}
			else if (State == State.DataLoaded)
			{
				if (ChartControl != null)
					ChartControl.Dispatcher.InvokeAsync(BuildTextBox);
			}
			else if (State == State.Terminated)
			{
				if (ChartControl != null)
					ChartControl.Dispatcher.InvokeAsync(RemoveTextBox);
			}
		}

		protected override void OnBarUpdate()
		{
			if (State != State.Realtime)
				return;

			// Code to box: the bar's range in ticks. Calculate decides how often:
			// On bar close = once per bar, On price change = every price change.
			BoxText = ((int)Math.Round((High[0] - Low[0]) / TickSize)).ToString();
		}

		private void BuildTextBox()
		{
			myTextBox = new TextBox
			{
				Width				= 60,
				HorizontalAlignment	= HorizontalAlignment.Right,
				VerticalAlignment	= VerticalAlignment.Top,
				Margin				= new Thickness(0, 10, 100, 0),
				HorizontalContentAlignment	= HorizontalAlignment.Center
			};

			myTextBox.SetBinding(TextBox.TextProperty, new Binding("BoxText")
			{
				Source				= this,
				Mode				= BindingMode.TwoWay,
				UpdateSourceTrigger	= UpdateSourceTrigger.PropertyChanged
			});

			myTextBox.PreviewKeyDown += OnBoxPreviewKeyDown;
			UserControlCollection.Add(myTextBox);
		}

		private void RemoveTextBox()
		{
			if (myTextBox == null)
				return;

			myTextBox.PreviewKeyDown -= OnBoxPreviewKeyDown;
			BindingOperations.ClearBinding(myTextBox, TextBox.TextProperty);
			UserControlCollection.Remove(myTextBox);
			myTextBox = null;
		}

		// The chart grabs typed keys to open its instrument selector.
		// Insert the character ourselves and mark the key handled so the chart never sees it.
		private void OnBoxPreviewKeyDown(object sender, KeyEventArgs e)
		{
			if (Keyboard.Modifiers != ModifierKeys.None)
				return;

			string ch = null;
			if (e.Key >= Key.D0 && e.Key <= Key.D9)
				ch = ((int)(e.Key - Key.D0)).ToString();
			else if (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9)
				ch = ((int)(e.Key - Key.NumPad0)).ToString();
			else if (e.Key == Key.OemPeriod || e.Key == Key.Decimal)
				ch = ".";
			else if (e.Key == Key.OemMinus || e.Key == Key.Subtract)
				ch = "-";
			else if ((e.Key >= Key.A && e.Key <= Key.Z) || e.Key == Key.Space)
			{
				e.Handled = true;	// numbers only: swallow letters so they never reach the chart
				return;
			}

			if (ch == null)
				return;

			int pos = myTextBox.SelectionStart;
			myTextBox.SelectedText = ch;
			myTextBox.Select(pos + 1, 0);
			e.Handled = true;
		}
	}
}

What it does:

  • The box starts at 44.
  • OnBarUpdate writes the bar’s range in ticks into the box. With Calculate set to On bar close it changes once per bar. With On price change it changes every tick.
  • You can type in the box anytime. Whatever is in it, typed or set by code, is in BoxText for OnBarUpdate to read, for example double.TryParse(BoxText, out value).
  • The PreviewKeyDown handler is what stops the instrument selector from opening while you type. The chart grabs typed keys, so the handler inserts digits, period and minus itself and swallows letters. That’s why this particular textbox example is numbers only.

Hope this might help, and give you some additional inspiration for your project.

While I’m here, a quick plug for my newest tool. If you build chart tools like this, it lets an AI read your live charts and help you test them. The video has chapters, so you can jump straight to what interests you:

NinjaTrader 8’s Ultimate AI Connection | HTAIBridge

Please Be Safe in this Crazy World!
:smiling_face_with_sunglasses:

1 Like