My last post started with a disclaimer that it was not a post about how-to cast .NET objects from one type to another in C/AL code. This post IS about how-to cast .NET objects from one type to another in C/AL code. But it also starts with a disclaimer; i.e. the code below is “sandpit” code, it has not been road-tested at all. It works with my contrived example but may not work with anything else. It’s a start…
After my last post I started investigating how we could get NAV to do Type Conversion (Casting). The MSDN page talks about 4 different types of conversion; Implicit, Explicit conversions (casts), User-defined conversions & Conversions with helper classes.
All of the examples for Implicit and Explicit rely on C# coding (e.g. result = (T)value; or result = value as T;) which isn’t going to help in C/AL, however System.Convert (one of the helper classes) looked very promising as it could handle all of the Implicit casting between Base Types as well. So I started with this simple example:
C/AL Cast(42,double); Cast(double,dotNetString); Message('Double = %1,String = %2',double,dotNetString); Cast(value : Variant;VAR result : DotNet "System.Object") type := GETDOTNETTYPE(result); result := SystemConvert.ChangeType(value, type);
And it worked! So I tried the XName example from Vjeko’s post and got the following:
A call to System.Convert.ChangeType failed with this message: Invalid cast from ‘System.String’ to ‘System.Xml.Linq.XName’.
Looking at the documentation on XName it clearly says “this class provides an implicit conversion from String that allows you to create an XName”. This is done using Conversion Operators (which is linked to from the User-defined conversions section from the MSDN page above).
Searching a bit more on how to find and use these Conversion Operators I found this very useful stackoverflow (as it seems to be often the case with NAV & .NET the answer is Reflection). If you look at the code that searches for the Methods it says they should be; 1) public & static 2) have one parameter that matches the Type you are converting from, and 3) will always be called op_Implicit or op_Explicit. Looking at the methods on the XName variable in NAV I found the matching method and can use it like this 🙂 :
C/AL XDoc := XDoc.XDocument(); XName := XName.op_Implicit('Root'); XElement := XElement.XElement(XName); XName := XName.op_Implicit('Child1'); XElement2 := XElement2.XElement(XName,'data1'); XElement.Add(XElement2); XDoc.Add(XElement); MESSAGE('%1',XDoc.ToString());
While this works, I didn’t think this was a very general solution. There could be lots of overloaded op_Implicit methods on a class, and NAV is often not great at picking which overloaded method to use. I also liked the idea of having one Cast function that I could call with any types and it would return the correct one if it could. So I implemented the same reflection code from the stackoverflow in NAV – the final working code is below, but first a little aside…
The code relies on being able to match the Type of the Parameter with the Type of the input. You may have noticed above I’m using Variant type as an input to my Cast function. This is nicely taking either a NAV type (42) or a .NET object (double) and doing the right thing with it. The interesting/annoying bit is when you try to get the .NET type of the Variant. Try this:
C/AL variant := 42; MESSAGE('%1',GETDOTNETTYPE(variant)); variant := double; MESSAGE('%1',GETDOTNETTYPE(variant));
You get the following messages:
GetDotNetType() does not support the type Microsoft.Dynamics.Nav.Runtime.NavVariant.
What this is saying is GETDOTNETTYPE only works on a Variant when the Variant is NOT a .NET Type. Makes perfect sense…
So, I had to split my Cast functions into whether I was passing in a NAV Type or a .NET Type. Luckily it looks like one function can just call the other (though see my disclaimer, I haven’t really tested this with many different types). So here is the function and the testing example:
C/AL CastNAV(value : Variant;VAR result : DotNet "System.Object") CastObj(value,result); CastObj(value : DotNet "System.Object";VAR result : DotNet "System.Object") type := GETDOTNETTYPE(result); MethodInfoArray := type.GetMethods(); FOR i := 0 TO MethodInfoArray.Length() - 1 DO BEGIN MethodInfo := MethodInfoArray.GetValue(i); IF (MethodInfo.IsPublic) AND (MethodInfo.IsStatic) AND (MethodInfo.Name IN ['op_Explicit','op_Implicit']) AND (MethodInfo.ReturnType.ToString() = type.ToString()) AND (MethodInfo.GetParameters().Length = 1) THEN BEGIN ParameterInfo := MethodInfo.GetParameters().GetValue(0); IF (ParameterInfo.ParameterType.Equals(GETDOTNETTYPE(value))) THEN BEGIN ObjectArray := ObjectArray.CreateInstance(GETDOTNETTYPE(NULL), 1); ObjectArray.SetValue(value, 0); result := MethodInfo.Invoke(NULL, ObjectArray); EXIT; END; END; END; //ELSE if no conversion operators found result := Convert.ChangeType(value, type); CastTest() CastNAV(42,double); CastObj(double,dotNetString); dotNetString := dotNetString.Concat('Child',dotNetString); //XNames complain if they start with a number XDoc := XDoc.XDocument(); CastNAV('Root',XName); XElement := XElement.XElement(XName); CastObj(dotNetString,XName); XElement2 := XElement2.XElement(XName,'data1'); XElement.Add(XElement2); XDoc.Add(XElement); MESSAGE('%1',XDoc.ToString());
What doesn’t work? The Implicit casting up an inheritance tree (up-casting) and Explicit casting down an inheritance tree (down-casting) doesn’t seem to work with this. However, as my last post showed you can get around a lot of that in NAV just by assigning the variables.
Also looking at some of the examples for System.Convert I’m probably not handling casting from NULL or to a NULLABLE type the correct way. I know there could be an issue, but I’m waiting for a real life example to work out whether I want to Error or just skip it.
Lastly, I can pass in NAV types or .NET types but I always get back a .NET type. I looked at seeing if I could use Variant again but you cannot have Variant as a return type for a function and if you set the Variant parameter as VAR then it becomes almost useless. So I haven’t got a solution for this yet…