Announcement

Collapse

Looking for a User App or Add-On built by the NinjaTrader community?

Visit NinjaTrader EcoSystem and our free User App Share!

Have a question for the NinjaScript developer community? Open a new thread in our NinjaScript File Sharing Discussion Forum!
See more
See less

Partner 728x90

Collapse

Custom Type Converter is displaying an error in the NT Log only after compiling

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    #16
    Hi bltdavid,

    Sorry, I had forgotten to subscribe to this post so I missed your request.

    Below is my solution to the issue. Hopefully it will work for you as well. It is based of the sample that koganam provided and an addition sample can be found in @PriceLevels.cs. I believe @Pricelevels has a hacky workaround for the collection though - you'll probably see what I mean below.


    The Problem

    Before coming up with a solution, it's always good to know exactly what the problem is. Basically, the issue comes down to copying instances of objects in one assembly to instances of objects in another assembly. When you "Reload Ninjascript" on a chart, what NT does is load another instance of the custom DLL and clones all the object instances in the currently loaded custom DLL to the instances in the newly loaded DLL.

    This gives rise to two distinct scenarios (let's call the current assembly A and the new assembly B):
    1. Reload Ninjascript without recompiling
      • A and B refer to exactly the same assembly
    2. Reload Ninjascript after recompiling
      • A and B refer to different assemblies (even if the code did not change)

    Most problems stem from the second scenario. Code executing in an assembly will only use types in that assembly when the types are statically referenced. In order to use types from another version of the assembly, reflection is needed.

    e.g.

    In assembly A:

    Code:
    [LEFT][COLOR=#4D4D4D][FONT=Helvetica]var a = new Person();[/FONT][/COLOR][/LEFT]
    [LEFT][COLOR=#4D4D4D][FONT=Helvetica][/FONT][/COLOR][/LEFT]

    In assembly B:

    Code:
    [LEFT][COLOR=#4D4D4D][FONT=Helvetica]var a = new Person();[/FONT][/COLOR][/LEFT]
    [LEFT][COLOR=#4D4D4D][FONT=Helvetica][/FONT][/COLOR][/LEFT]

    The code in assembly A creates an A.Person and the code in assembly B creates a B.Person. If there was a recompile, these two types are distinct and are not assignable to each other!

    The Solution

    There are two main entry points to the NT cloning logic: Implementing ICloneable and Indicator.CopyTo(NinjaScript script). You'll want to use ICloneable for individual objects and CopyTo for collections of objects.

    So, the solution comes down to:
    1. Cloning
      • Create a new instance of the corresponding type in B
      • Copy the properties of the instance in A to the new B instance (May need to clone property values as well)
    2. CopyTo
      • Clone the collection items in A to the corresponding type in B as described in step 1
      • Get the collection in B and add the cloned items

    This post is a bit long so I'll post the implementation of the solution in a different post below.

    Thanks,
    Wil

    Comment


      #17
      Solution Implementation

      In the CopyTo scenario, you are given an instance of the new script in B. It's therefore fairly straightforward to then create instances of types in assembly B. But how do you get a reference to the new assembly when you are cloning? Enter Globals.AssemblyRegistry.

      Globals.AssemblyRegistry isn't documented so here is my description of how it works based on experimentation and looking at sample code. The custom assembly is the one that is recompiled and has a new instance loaded when you "Reload NinjaScript". Globals.AssemblyRegistry contains all types defined in the custom assembly, always referencing the newest loaded version of the custom assembly. So when you call the GetType(string typename) method, it will return the type in assembly B or null if the given type was not defined in the custom assembly.

      There are two parts to the solution implementation:
      1. The code you write each time
      2. A utility class and attribute that you can use
      I'll start with the code you would write and then provide the supporting utilities.

      Your Code (In the custom assembly)

      Code:
      [Converter(typeof(ExpandableObjectConverter))] [COLOR=#008000][B]// SPECIAL NOTE[/B][/COLOR]
      public class Person : ICloneable
      {
          [Display(…)]
          public string Name { get; set; }
      
          [XmlIgnore]
          [DoNotClone] [COLOR=#0000FF][B]// NOTE 1[/B][/COLOR]
          public string CalculatedValue { get; set; }
      
      public object Clone() => ReflectionUtils.AssemblyClone(this); [COLOR=#0000FF][B]// NOTE 2[/B][/COLOR]
      }
      As a collection:

      Code:
      public class MyIndicator : Indicator
      {
          public override void CopyTo(NinjaScript otherScript)
          {
              Type otherType = otherScript.GetType(); [COLOR=#0000FF][B]// NOTE 3[/B][/COLOR]
              PropertyInfo otherPeopleProp = otherType.GetProperty(nameof(People));
      
              if (otherPeopleProp?.GetValue(otherScript) is IList otherPeople)  [COLOR=#0000FF][B]// NOTE 4[/B][/COLOR]
              {
                  otherPeople.Clear(); // NT does adds when cloning so clear existing values. [COLOR=#0000FF][B]// NOTE 5[/B][/COLOR]
      
                  for (int i = 0; i < People.Count; i++)
                  {
                      otherPeople.Add(ReflectionUtils.AssemblyClone(People[i]);
                  }
              }
          }
      
          private List<Person> people_ = new List<Person>();
          [Display(…)]
          [SkipOnCopyTo(true)]
      [PropertyEditor("NinjaTrader.Gui.Tools.CollectionEditor")]
          public List<Person> People
          {
              get => people_;
              set => people_ = new List<Person>(value);  [COLOR=#0000FF][B]// NOTE 6[/B][/COLOR]
          }
      }
      SPECIAL NOTE:
      1. If you do NOT attach a converter, you need to override ToString to provide the description used when showing in the collection editor. If you DO attach a converter, your class needs to have a Name property that will serve as the description in the collection editor. This is what I've observed - there may be different approaches.

      NOTES:
      1. If there are properties you do not wish cloned, tag with DoNotCloneAttribute (Implementation below)
      2. For most scenarios, call AssemblyClone to clone your instance (Implementation below)
      3. This is the type in the new assembly
      4. IList is an interface and not defined in the custom assembly so we can use it statically
      5. Works in tandem with NOTE 6. If a copy was not made in NOTE 6 and we refreshed without a recompile, both lists would be the same reference so clearing would affect both the current and the new indicator instance
      6. Always make a new collection on assignment. See NOTE 5 above
      That's it! All that is left is to show the supporting utility code.

      DoNotClone Attribute:

      Code:
      [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
      public sealed class DoNotCloneAttribute : Attribute { }
      ReflectionUtils:

      Code:
      public static class ReflectionUtils
      {
       public static T GetAttribute<T>(this PropertyInfo property) where T : Attribute
           => property?.GetCustomAttribute<T>();
      
       public static T GetAttribute<T>(this Type type) where T : Attribute
           => type?.GetCustomAttribute<T>();
      
       public static T GetAttribute<T>(this PropertyDescriptor descriptor) where T : Attribute
        => descriptor?.Attributes.OfType<T>().FirstOrDefault();
      
       public static Type GetRegisteredType(this Type type) => AssemblyRegistry.GetType(type.FullName);
       public static Type GetRegisteredType<T>() => AssemblyRegistry.GetType(typeof(T).FullName);
       public static Type GetRegisteredType(string fullName) => AssemblyRegistry.GetType(fullName);
      
       public static object AssemblyClone(object instance)
       {
        if (instance == null) return instance;
      
        Type instanceType = instance.GetType();
        string typeName = instanceType.FullName;
        Type newType = GetRegisteredType(typeName);
      
        if (newType == null) return instance;
      
        Assembly newAssembly = newType.Assembly;
      
        if (newType.IsEnum)
         return Enum.Parse(newType, instance.ToString());
      
        object newInstance = newAssembly.CreateInstance(typeName);
      
        foreach (PropertyInfo newProp in newType.GetProperties(assemblyCloneFlags))
        {
         PropertyInfo oldProp = instanceType.GetProperty(newProp.Name);
      
         if (oldProp == null) continue;
         if (!newProp.CanWrite) continue;
         if (oldProp.GetAttribute<DoNotCloneAttribute>() != null) continue;
      
         object instanceValue = oldProp.GetValue(instance);
         Type valueType = oldProp.PropertyType;
         MethodInfo valueClone = valueType.GetMethod(nameof(AssemblyClone), assemblyCloneFlags); [B][COLOR=#0000FF]// NOTE 1...[/COLOR][/B]
      
         object newInstanceValue = instanceValue != null && valueClone != null
          ? valueClone.Invoke(instanceValue, Array.Empty<object>()) [COLOR=#0000FF][B]// ...NOTE 1[/B][/COLOR]
          : AssemblyClone(instanceValue);
      
         newProp.SetValue(newInstance, newInstanceValue, null);
        }
      
        return newInstance;
       }
      
       private const BindingFlags assemblyCloneFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
       private static AssemblyRegistry AssemblyRegistry => Globals.AssemblyRegistry;
      }
      NOTES:
      1. If you want to customize the cloning when your type is a property of another object, just create a method named AssemblyClone in your type and it will be used here. To customize when it is top level just have your ICloneable.Clone method call your AssemblyClone method instead of the ReflectionUtils one.

      I make no claims that this code is bug free. Consider any bugs found extra protein for your day provided free of charge

      HTH,
      Wil

      Comment


        #18
        bmont,

        It seems your type converter issue stems from the same assembly reloading issue. zweistein's sample is incorrect. If you are referencing a type converter defined in the custom assembly, you cannot use a static reference as he shows. This will always refer to the current executing instance of the assembly. You instead need to refer by name.

        e.g.

        Incorrect:

        Code:
        [TypeConverter(typeof(MyNamespace.MyConverter))]

        Correct:

        Code:
        [LEFT][COLOR=#4D4D4D][FONT=Helvetica][TypeConverter("MyNamespace.MyConverter")][/FONT][/COLOR][/LEFT]
        [LEFT][COLOR=#4D4D4D][FONT=Helvetica][/FONT][/COLOR][/LEFT]

        Comment


          #19
          wbennetjr,

          I see you're using C# 6.0 features in your code sample, but NinjaScript uses C# 5. Is there a way to use C# 6 features in NinjaScript, or are you using another method to build this code?

          Comment


            #20
            I've had similar issues, but under different circumstances. It seems the problem lies in the fact that your custom type persists between reloads of ninjascript, and is not disposed of with the rest of the indicator. The reason it happens only after a re-compile is that NT creates a new NinjaTrader.Custom.dll (it also generates a temporary version) and NT loses track of the undisposed type when ninjascript is reloaded again.

            My solution was to put a check in to see if there was a "recompile" done after the indicator had been added to a chart. If it has been recompiled, then your type will have to be disposed of separately, and you can't count on the normal disposal of the indicator.

            Like I said, my circumstances were different, but the effect was the same. I would get an error, only after a recompile of any indicator since my indicator was applied to a chart.

            Hope this helps.

            showboxSpotify premium apkpandora one premium

            Comment

            Latest Posts

            Collapse

            Topics Statistics Last Post
            Started by FrazMann, Today, 11:21 AM
            2 responses
            6 views
            0 likes
            Last Post NinjaTrader_ChristopherJ  
            Started by rjbtrade1, 11-30-2023, 04:38 PM
            2 responses
            80 views
            0 likes
            Last Post DavidHP
            by DavidHP
             
            Started by Spiderbird, Today, 12:15 PM
            1 response
            7 views
            0 likes
            Last Post NinjaTrader_ChristopherJ  
            Started by lorem, Yesterday, 09:18 AM
            5 responses
            18 views
            0 likes
            Last Post NinjaTrader_ChelseaB  
            Started by cmtjoancolmenero, Yesterday, 03:58 PM
            12 responses
            42 views
            0 likes
            Last Post NinjaTrader_ChelseaB  
            Working...
            X