Parameters inside Custom Class don't get saved like normal parameters

I have the following structure. Which is essentially a custom class that appears in the parameter grid as an expandable menu (like a font or StrokeClass). So in the example below, the parameter grid will contain an expandable toggle called ‘Labels’, inside which is a checkbox ‘Show Labels’;

namespace NinjaTrader.NinjaScript.Indicators
{

	[TypeConverter("NinjaTrader.NinjaScript.Indicators.ToggleConverter")]
	public class MyIndicator : Indicator
	{

		[TypeConverter(typeof(NestedConverter))]
		public class LabelClass
		{
			[Display(Name = "Show Label", Description = "Show Top Label", Order = 10)]
			public bool ShowTop
			{ get; set; }
		}


		protected override void OnStateChange()
		{
			#region SET DEFAULTS
			if (State == State.SetDefaults)
			{
				myLabelClass = new LabelClass
				{
					ShowLabel = true
				}
			}
		}
		protected override void OnBarUpdate()
		{

		}


		[Display(Name = "Labels", Order = 390, GroupName = "Parameters")]
		public LabelClass myLabelClass { get; set; }
	}
}

Everything works fine, the user’s choice gets remembered between restarts/shutdowns. BUT, every time I compile anything in the NinjaScript Editor. Any values contained within custom classes get reset to default values. So, in this case. if I set ShowLabel = false manually, then compile something in the Editor. Reload NinjaScript and ShowLabel is back to = true.

How do I get these options to persist through re-compiles like normal parameters?

I also found the same issue in the NT example file; ‘PropertyGridControlsExamples’

Yes, this is a well known (and a very bizarre) issue. I don’t think there were any solutions suggested in the old forum and I even asked scripting support people and no one had a solution. So I came up with my own solution/workaround for this as I use these Expandable Objects extensively. I will demonstrate with a simple example below. You’ll need to adapt this to your specific data structures. It adds a lot more code and complexity (especially with more complex classes with multiple variables), but it is well worth it because it produces a clean and crisp user interface.

Since the custom class saved values don’t survive after a compile, you have to take extra steps and add additional inputs (one per each class variable that you wish to save). These extra/dummy variables do survive a compile and are saved in the XML for later retrieval. You will use these to initialize the custom class parameters in your code.

I have the following class:

	[TypeConverter(typeof(ExpandableObjectConverter))]
	public class BrushAndOpacity
	{
		[XmlIgnore]
		[Display(Name="Color", Order=1)]
		public Brush ColorBrush
		{ get; set; }

		[Range(0, 100)]
		[Display(Name="Opacity (0-100)", Order=2)]
		public int ColorOpacity
		{ get; set; }
	}

Add the following to Properties section. Each of the custom class variables will need their own separate input, so add additional inputs as necessary for your case.

		[Display(GroupName="Settings", Order=30, Name="Your parameter name")]
		public BrushAndOpacity Input_CollectionSettings
		{ get; set; }

		// Below two extra/dummy variables that save the custom class values. Adapt this to your case.
		public string Input_MyBrush_Serializable
		{
			get { return( Serialize.BrushToString(Input_CollectionSettings == null ? Default_MyBrush : Input_CollectionSettings.ColorBrush)); }
			set { Input_CollectionSettings.ColorBrush = Serialize.StringToBrush(value); }
		}

		public int Input_MyOpacity
		{
			get { return(Input_CollectionSettings == null ? Default_MyOpacity : Input_CollectionSettings.ColorOpacity); }
			set { Input_CollectionSettings.ColorOpacity = value; }
		}

Default_MyBrush and Default_MyOpacity will need to get defined at indicator class level:

		private Brush Default_MyBrush = Brushes.DarkGoldenrod;
		private int Default_MyOpacity = 40;

In State.SetDefaults initialize the custom class instance using default values.

				Input_CollectionSettings = new BrushAndOpacity()
				{
					ColorBrush = Default_MyBrush,
					ColorOpacity = Default_MyOpacity
				};

I believe this is all you need to get this working. Try this and let me know if I missed something and I’ll update my response.

After doing this, you’ll notice that the two new dummy inputs (Input_MyBrush_Serializable and Input_MyOpacity) will show up on the properties grid which is undesirable. You’ll need to hide those on the grid. This can be done in one of two ways that I’m aware of. You can add “[Browsable(false)]” to each input, or you can do this using propertyDescriptorCollection.Remove() inside your IndicatorBaseConverter if you have one defined already.

Obviously the first solution is much simpler if it works for you. I seem to recall there was an issue in my case with using Browsable(false) but I don’t remember the details, so I went with the other solution since I already have an IndicatorBaseConverter defined for all my tools and it was just a matter of adding an extra line per dummy input to hide them on the grid.

Try the simple method and if doesn’t work right, try the second approach. If you need code snippet for IndicatorBaseConverter, let me know and I’ll post it here.

2 Likes

Thanks, I will give it a go. Such a ball ache though as I also have like 24 nested classes each with 6-10 parameters.

1 Like

You need to override the CopyTo method. I suppose there should be some example in the old forum. I can confirm it works regardless of how complex the parameter structure is, even with collections of custom types with multiple levels of nesting.

2 Likes

I looked at the old forum and this was really the only thread I found https://forum.ninjatrader.com/forum/ninjatrader-8/add-on-development/1086229-variable-to-retain-its-value

it refers to the DrawingTileTool using CopyTo(), I tried the methods in there and it didn’t work.

The solution provided by fc77 does however work, I only tested 1 variable but it works.

1 Like

At the bottom of this page using_a_typeconverter , there is a link to the ‘SampleIndicatorTypeConverter’ indicator, which contains an example of overriding CopyTo."

1 Like

If there’s an easier solution than my tedious hack, I’m all ears. I looked at the CopyTo example in SampleIndicatorTypeConverter, but at first glance it seems a bit over my pay grade. I will look into it at some point when I have some free time. Thank you for the pointer @cls71.

In this example, I have a property of a CustomType called PropDeltaGUI. In fact, it’s not necessary to override the CopyTo method for data persistence between compilations in the case of a CustomType instance. The issue might be related to the use of TypeConverters.

There’s also a second property, which is a collection of PropDeltaGUI. In this case, overriding the CopyTo method is required to preserve data between compilations. Otherwise, the collection is cleared between builds.

I investigated this issue some time ago, and the example I uploaded is basically a copy-paste from code in other indicators/strategies of mine. The comments may not be entirely precise, but the functionality is correct and can be useful for testing and further research. I hope it helps.


namespace NinjaTrader.NinjaScript.Indicators.Utilities
{
    #region Comentarios
    //
    // ░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░
    // ░▒▓█                                               █▓▒░
    // ░▒▓█          á é í ó ú ñ . Á É Í Ó Ú Ñ            █▓▒░
    // ░▒▓█                                               █▓▒░
    // ░▒▓█    Fecha de creación: 01 de octubre de 2025   █▓▒░
    // ░▒▓█                                               █▓▒░    
    // ░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░█▓▒░
    //
    #endregion

    public class Zi8AssemblyClone : Indicator
	{
        #region Properties

        [NinjaScriptProperty]
        [Display(Name = "Delta", Description = "Configuración de la Delta del profile", GroupName = "NinjaScriptParameters", Order = 10)] 
        public PropDeltaGUI Delta { get; set; }

        [NinjaScriptProperty]
        [Display(Name = "Deltas", Description = "Colección de deltas", Prompt = "1 value|{0} values|Add value...|Edit value...|Edit values...", GroupName = "NinjaScriptParameters", Order = 20)]
        [PropertyEditor("NinjaTrader.Gui.Tools.CollectionEditor")] // Allows a pop-up to be used to add values to the collection, similar to Price Levels in Drawing Tools
        public System.Collections.ObjectModel.Collection<PropDeltaGUI> Deltas { get; set; }

        #endregion

        #region OnStateChange
        protected override void OnStateChange()
		{
			if (State == State.SetDefaults)
			{
                #region SetDefaults

                Description = @"Enter the description for your new custom Indicator here.";
				Name										= "Zi8AssemblyClone";
				Calculate									= Calculate.OnBarClose;
				IsOverlay									= true;
				DisplayInDataBox							= true;
				DrawOnPricePanel							= true;
				DrawHorizontalGridLines						= true;
				DrawVerticalGridLines						= true;
				PaintPriceMarkers							= true;
				ScaleJustification							= NinjaTrader.Gui.Chart.ScaleJustification.Right;
				//Disable this property if your indicator requires custom values that cumulate with each new market data event. 
				//See Help Guide for additional information.
				IsSuspendedWhileInactive					= true;

                Delta = new PropDeltaGUI();
                Deltas = new System.Collections.ObjectModel.Collection<PropDeltaGUI>();

                #endregion
            }
		}
        #endregion

        #region CopyTo
        // Este método es necesario para conservar la información de los parámetros GUI, entre compilaciones. 
        // 'ninjascript' es la instancia de la estrategia en el compilado más reciente.
        public override void CopyTo(NinjaScript ninjaScript)
        {
            base.CopyTo(ninjaScript);

            Type newInstType = ninjaScript.GetType();

            // 'this' es la instancia del ninjascript en ejecución (en caso de recompilaciones, su tipo es el del compilado antiguo).
            if (this.GetType().Equals(newInstType))
            {
                // Los compilados coinciden... no hay que hacer nada; ya se copió todo en la llamada a base.CopyTo(ninjaScript);
                return;
            }
            else
            {
                // Las propiedades de tipos nativos y otros básicos ya se copiaron en la llamada a base.CopyTo(ninjaScript), pero los valores de las
                // propiedades de tipos custom anidados, y de colecciones, hay que copiarlos explícitamente desde el compilado antiguo al nuevo...
                // (Se pueden encontrar en el OnStateChange.SetDefaults buscando las propiedades que se crean con NEW de un tipo custom).


                // PropertyInfo de 'Deltas' en el compilado nuevo:
                PropertyInfo propinfolist_new = newInstType.GetProperty("Deltas");
                if (propinfolist_new != null)
                {
                    // Valor de la propiedad 'Deltas' en el compilado nuevo (es una colección que debe estar vacía, ya que en el constructor del indicador no se añaden valores):
                    IList valuelist_new = propinfolist_new.GetValue(ninjaScript) as IList;
                    if (valuelist_new != null)
                    {
                        // ... pero aún así, me aseguro de vaciar la nueva lista:
                        valuelist_new.Clear();

                        // Referencia al tipo PropDeltaGUI de los ítems de la lista, en el compilado más reciente:
                        Type itemstype_new = NinjaTrader.Core.Globals.AssemblyRegistry.GetType("NinjaTrader.NinjaScript.Indicators.Utilities.nsZi8AssemblyClone.PropDeltaGUI");

                        // Copio los valores de cada ítem del compilado antiguo a un nuevo ítem ('PropDeltaGUI' y 'Deltas' se refieren al compilado antiguo):
                        foreach (PropDeltaGUI valueitem_old in Deltas)
                        {
                            // Creo un nuevo ítem clonando el ítem antiguo:
                            object newInstance = valueitem_old.AssemblyClone(itemstype_new);
                            if (newInstance == null)
                                continue;

                            valuelist_new.Add(newInstance);
                        }
                    }
                }
            }
        }
        #endregion

        protected override void OnBarUpdate()
		{
		}
	}
}

namespace NinjaTrader.NinjaScript.Indicators.Utilities.nsZi8AssemblyClone
{
    public static class Constants
    {
        public const string SYMBOL_WARNING = "\u26A0";
    }

    #region PropDeltaGUI

    [CategoryDefaultExpanded(true)]
    [TypeConverter(typeof(PropDeltaGUITypeConverter))]
    public class PropDeltaGUI : NotifyPropertyChangedBase, ICloneable
    {
        #region Properties

        [RefreshProperties(RefreshProperties.All)] // Update UI when value is changed
        [Display(Name = "Activar", Description = "Activar las condiciones para filtrar el volumen Delta", Order = 110)]
        public bool IsEnabled { get; set; }

        [Display(Name = "Min", Description = "Valor mínimo de la Delta con SIGNO", Order = 820)]
        public int MinDelta { get; set; }

        [Display(Name = "Max", Description = "Valor máximo de la Delta con SIGNO", Order = 860)]
        public int MaxDelta { get; set; }

        [XmlIgnore]
        [Display(Name = "Color Delta", Description = "Color para la delta", Order = 910)]
        public Brush ColorDelta{ get; set; }
        [Browsable(false)]
        public string ColorDeltaSerializable
        {
            get { return Serialize.BrushToString(ColorDelta); }
            set { ColorDelta = Serialize.StringToBrush(value); }
        }

        #endregion

        #region Constructor
        public PropDeltaGUI()
        {
            IsEnabled = true;
            MinDelta = 50;
            MaxDelta = int.MaxValue;
            ColorDelta = Brushes.Green;
        }
        #endregion

        #region Clone

        public virtual object Clone()
        {
            //
            // Si estando cargada la estrategia, compilo, se genera una nueva dll con los tipos. 
            // Y si después abro el diálogo de carga de estrategias se produce un conflicto entre los tipos
            // antiguos y los del nuevo compilado ya que no son los mismos aunque compartan nombre.
            //
            // 'myLastType' es el tipo en el nuevo compilado
            // 'this' es una instancia del tipo antiguo (antes de la nueva compilación)
            //

            // Referencia al tipo en el compilado más reciente:
            Type myLastType = NinjaTrader.Core.Globals.AssemblyRegistry.GetType("NinjaTrader.NinjaScript.Indicators.Utilities.nsZi8AssemblyClone.PropDeltaGUI");

            // Si no se ha compilado entre ejecuciones/cargas del indicador, el tipo de
            // la instancia en ejecución coincidirá con el del compilado más reciente:
            if (this.GetType().Equals(myLastType))
            {
                PropDeltaGUI o = new PropDeltaGUI();
                CopyTo(o);
                return o; 
            }

            // Si se ha compilado entre ejecuciones/cargas del indicador los tipos no 
            // coincidirán y tengo que usar Reflection para informar el resultado:
            else
            {
                return AssemblyClone(myLastType);
            }
        }
        #endregion

        #region CopyTo
        public virtual void CopyTo(PropDeltaGUI other)
        {
            other.IsEnabled = this.IsEnabled;
            other.MinDelta = this.MinDelta;
            other.MaxDelta = this.MaxDelta;
            other.ColorDelta = this.ColorDelta.Clone() as SolidColorBrush;
        }
        #endregion

        #region AssemblyClone
        // 't' es el tipo en el compilado más reciente:
        public object AssemblyClone(Type t)
        {
            Assembly asm = t.Assembly;
            object newInstance = asm.CreateInstance(t.FullName);

            #region D e b u g           N O   B O R R A R  ✔ ✔ ✔

            //
            // Si estando cargado el indicador en un chart, compilo, se genera una nueva dll con los tipos.
            // Si después abro el diálogo de carga de indicadores del chart, se ejecuta este método y el 
            // resultado de los IFs es el siguiente:
            //
            // 't' es el tipo en el nuevo compilado
            // 'this' es una instancia del tipo antiguo
            // 'newinstance' es una instancia del tipo nuevo
            // 'PropDeltaGUI' es una referencia al tipo antiguo
            //

            //if (this.GetType().Equals(t))
            //{
            //    // ❌  NO se cumple
            //}
            //if (newInstance.GetType().Equals(t))
            //{
            //    // ✔  SÍ se cumple
            //}
            //if (this.GetType() == t)
            //{
            //    // ❌  NO se cumple
            //}
            //if (this is PropDeltaGUI)
            //{
            //    // ✔  SÍ se cumple
            //}
            //if (newInstance.GetType() == this.GetType())
            //{
            //    // ❌  NO se cumple
            //}

            #endregion

            // Bucle por las propiedades del tipo compilado más reciente:
            foreach (PropertyInfo p in t.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
            {
                if (p.CanWrite) 
                {
                    // Obtención del valor para la propiedad en el tipo en ejecución (más antiguo): 
                    object value = this.GetType().GetProperty(p.Name).GetValue(this);

                    // Asignación del valor de la propiedad en la instancia del tipo compilado más reciente:
                    p.SetValue(newInstance, value, null);
                }
            }

            // Se devuelve la instancia del tipo compilado más reciente: 
            return newInstance;
        }
        #endregion

        public override string ToString()
        {
            return IsEnabled ?
                string.Format("[{0},{1}]", MinDelta == int.MinValue ? "-\u221E" : MinDelta, MaxDelta == int.MaxValue ? "+\u221E" : MaxDelta) :
                $"{K.SYMBOL_WARNING}  Desactivado  {K.SYMBOL_WARNING}";
        }
    }
    #endregion

    #region PropDeltaGUITypeConverter

    public class PropDeltaGUITypeConverter : ExpandableObjectConverter
    {
        public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object component, Attribute[] attrs)
        {
            PropDeltaGUI props = component as PropDeltaGUI;
            PropertyDescriptorCollection pdc = base.GetPropertiesSupported(context) ? base.GetProperties(context, component, attrs) : TypeDescriptor.GetProperties(component, attrs);

            if (props == null || pdc == null)
                return null;

            if (!props.IsEnabled)
            {
                // Si está desactivado, devuelvo sólo el checkbox para Activar/Desactivar:
                PropertyDescriptor o = pdc.Find("IsEnabled", false);
                return new PropertyDescriptorCollection(null) { o };
            }
            else
                return pdc; // devuelvo todas las propiedades
        }

        public override bool GetPropertiesSupported(ITypeDescriptorContext context)
        {
            return true;
        }
    }
    #endregion
}

3 Likes

Excellent. Bookmarking your post to review later.
Thank you for posting this.

So I got this to work, with cls71 solution as a basis(thanks!). Here are paste-able methods broken into sections with comments :

First add:

using System.Reflection;

then add the base class

	#region REUSABLE BASE CLASS

	public abstract class SerializableCustomClass : ICloneable
	{
		// Each derived class must specify its full type name for reflection
		protected abstract string GetFullTypeName();

		public virtual object Clone()
		{
			// Get the type from the most recent compiled assembly
			Type myLastType = NinjaTrader.Core.Globals.AssemblyRegistry.GetType(GetFullTypeName());

			// If no recompilation occurred, types match - use simple copy
			if (this.GetType().Equals(myLastType))
			{
				object o = Activator.CreateInstance(this.GetType());
				CopyPropertiesTo(o);
				return o;
			}
			// If recompiled, types differ - use reflection to copy across assemblies
			else
			{
				return AssemblyClone(myLastType);
			}
		}

		// Copy properties within the same assembly
		protected void CopyPropertiesTo(object other)
		{
			foreach (PropertyInfo p in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
			{
				if (p.CanWrite && p.CanRead)
				{
					object value = p.GetValue(this);
					p.SetValue(other, value);
				}
			}
		}

		// Copy properties from old assembly to new assembly using reflection
		public object AssemblyClone(Type t)
		{
			Assembly asm = t.Assembly;
			object newInstance = asm.CreateInstance(t.FullName);

			// Loop through all properties in the new type
			foreach (PropertyInfo p in t.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
			{
				if (p.CanWrite)
				{
					// Get value from old type, set it in new type
					object value = this.GetType().GetProperty(p.Name)?.GetValue(this);
					if (value != null)
						p.SetValue(newInstance, value, null);
				}
			}

			return newInstance;
		}
	}
	#endregion

In each of your custom class custom classes add the full name:

		[TypeConverter(typeof(ExpandableObjectConverter))]
	public class NestedClass : SerializableCustomClass // <---
	{

		// Returns the fully qualified type name for reflection lookup
		// Format: "Namespace.IndicatorClassName+NestedClassName"
		// The '+' separates the outer class from the nested class

		protected override string GetFullTypeName()
		{
			return     "NinjaTrader.NinjaScript.Indicators.ExampleCustomClassPreserveParameterCopyTo+NestedClass";
    	}

		// Public properties 
		[Display(Name = "Int", Description = "First integer value", Order = 70)]
		[Range(0, 1000)]
		public int MyInt { get; set; }

		[Display(Name = "Int2", Description = "Second integer value", Order = 71)]
		[Range(0, 1000)]
		public int MyInt2 { get; set; }
...........

    // REST OF YOUR CUSTOM CLASS PROPERTIES

Then the CopyTo and CopyCustomProperty methods. See ***** where you add each custom class you need add:

	// Called when indicator is recompiled while loaded on a chart
	public override void CopyTo(NinjaScript ninjaScript)
	{
		base.CopyTo(ninjaScript);

		Type newInstType = ninjaScript.GetType();

		// If assemblies match, base.CopyTo already handled everything
		if (this.GetType().Equals(newInstType))
		{
			return;
		}
		// If assemblies differ (recompilation occurred), manually copy custom classes
		else
		{
			// ***** Copy each custom class property using reflection
			CopyCustomProperty(ninjaScript, newInstType, "MyNestedClass",
				"NinjaTrader.NinjaScript.Indicators.ExampleCustomClassPreserveParameterCopyTo+NestedClass");
		}
	}

	// Helper method to copy a single custom class property across assemblies
	private void CopyCustomProperty(NinjaScript ninjaScript, Type newInstType, string propertyName, string typeName)
	{
		// Get the property in the new assembly
		PropertyInfo propinfo_new = newInstType.GetProperty(propertyName);
		if (propinfo_new != null)
		{
			// Get the type from the new assembly
			Type classType_new = NinjaTrader.Core.Globals.AssemblyRegistry.GetType(typeName);
			
			// Get the property in the old assembly
			PropertyInfo propinfo_old = this.GetType().GetProperty(propertyName);
			
			if (propinfo_old != null)
			{
				// Get the old value
				object oldValue = propinfo_old.GetValue(this);
				if (oldValue != null)
				{
					// Cast to base class and clone to new assembly type
					SerializableCustomClass oldClass = oldValue as SerializableCustomClass;
					if (oldClass != null)
					{
						object newInstance = oldClass.AssemblyClone(classType_new);
						if (newInstance != null)
						{
							// Set the new value in the new assembly instance
							propinfo_new.SetValue(ninjaScript, newInstance);
						}
					}
				}
			}
		}
	}

My Nested Class Parameter:

	// PARAMETER NESTED CLASS
	[Display(Name = "Nested Class", Order = 80, GroupName = "Global Parameters")]
	public NestedClass MyNestedClass { get; set; }
3 Likes