I am working on updating a native module to v.61 that is currently on v.59. This module has a method with an ICallback argument, and this ICallback is then invoked with two arguments later on. In v.61, ICallback is gone and the replacements I see for it do not allow me to pass multiple arguments to them (instead I would have to pass an object to the ICallback replacement). I can rewrite this native module's JavaScript to use ReactCallback instead and then give it an object as an argument when being fulfilled, but I don't need to make any additional changes for Android or iOS to get this feature working after upgrading to v.61.
Here is an example of what I am seeing in a native module I am working with. Notice the callback is invoked with two arguments.
NativeModule.cs
public async Task PrintLabel(int x, int y, ICallback callback)
{
if (!someCondition)
{
callback.Invoke(false, "this callback has two arguments");
}
...
}
ReactCallback<T>
is typed and typed to just one argument. I don't see an equivalent replacement for what was used in v.61. Is there something I am missing that allows this functionality that was used in RNW v.59?
I faced this same issue with a previous module I was working on, and I 'corrected' it by fragmenting Window's JavaScript from the Android/iOS JavaScript. I am not proud of it, but below is what I did to get around this issue. Notice that the prepare
method for Windows is treated as a promise and for Android and iOS it has a callback that takes multiple arguments.
nativeModule.js
if (IsWindows){
// I am not seeing how to fire off a callback with multiple arguments in RNW right now.
// Updated the native module to use a promise that returns an object.
NativeModuleXYZ.prepare(this._filename, this._key, options || {})
.then(resp => {
...
})
.catch(err => {
...
})
}
else
{
NativeModuleXYZ.prepare(this._filename, this._key, options || {}, (error, props) => {
if (props) {
...
}
if (error) {
onError && onError(error, props);
}
});
}
I _definitely_ prefer the approach I took using promises, but the important part for me right now is upgrading to v.61 as quickly,and as safely, as possible. I would rather not change what I don't have to (yet)... As I asked above, is there another way to go about this when updating to v.61?
Thanks in advanced.
@vmoroz Can you help field this question?
Hi @woodrufs, I am sorry about the pain this issue causes. It is a clear oversight from my site. For some reason I thought it must be always one argument. I recently did a similar fix for event arguments (PR #4426), but the callbacks still have the same limitation. I will try to address it as soon as possible.
Now to the second part of your question: what to do before it all get fixed?
We can pack these two fields into one single struct or class and provide a custom serializer which pushes two values instead of one. From the JS side it will look as two arguments. The workaround is only in the native code. For example, if we have to pass two parameters: bool
and string
as it was in your first example, then we can define a struct TwoParams
:
``` C#
struct TwoParams
{
public bool BoolValue;
public string StringValue;
}
Then, the method can be written as
``` C#
[ReactMethod]
public async void PrintLabel(int x, int y, Action<TwoParams> callback)
{
if (!someCondition)
{
callback(new TwoParams { BoolValue = false, StringValue = "this callback has two arguments" });
}
...
}
_Note_ that we are deprecating the ReactCallback
in favor of the Action
. It is Obsolete in 0.62 and may be deleted in 0.63.
To serialize the value you should create an extension method in your new or existing static class:
C#
static class MySerialization
{
// Writing TwoParams value as two arguments to the IJSValueWriter.
public static void WriteValue(this IJSValueWriter writer, TwoParams value)
{
// We write two values directly without start/end object or array.
writer.WriteValue(value.BoolValue);
writer.WriteValue(value.StringValue);
}
}
The static class can be in any namespace. We just need it to be a public extension method for IJSValueWriter
with name WriteValue
and the target type as second parameter.
I have implemented this solution as a unit test to see that it works: https://github.com/vmoroz/react-native-windows/blob/TwoCallbackArgs/vnext/Microsoft.ReactNative.Managed.UnitTests/TwoParamCallback.cs
It runs successfully against latest code. It should also work for 0.61 except that we do not need to register the assembly there in the test. (It is required by new code where we replaced shared C# code in favor of an assembly.)
Hi @vmoroz,
I really appreciate your detailed reply and sharing a fix for me to use - thank you. I have been out the last couple days, so I have not been able to try this yet. I will get back to you Monday to confirm that this works. Once again, thank you for your help and for all you're doing for the RN community!
Hi @vmoroz,
Thanks again for the your time and help. I was able to get your implementation working using a model similar to yours; however, I ran into an issue when I have arguments that are more complex and are POCOs. I tried to debug this myself earlier today, but I can't get the symbols loaded correctly to properly step through any of this. By the time the exception is hit in the program and the debugger is loaded up with the symbols it has, everything I see in the stack trace is just pointer addresses :(.
I have a need to have my callback arguments look something like this:
class CallbackArguments
{
public Error Error;
public Properties Props;
}
class Error
{
public string Message;
public int? Code;
}
class Properties
{
public double Duration;
public int? TotalChannels;
}
I try to invoke it like below, but an exception I cannot figure out is thrown. Please take note that the Error
property on the argument's wrapper class isn't set. The same goes for the TotalChannels
property on the Properties
class.
[ReactModule]
class RNSoundExample
{
[ReactMethod("prepare")]
public async Task Prepare(string file, int key, JSValue options, Action<CallbackArguments> callback)
{
...
callback(
new CallbackArguments
{
Props = new Properties
{
Duration = player.PlaybackSession.NaturalDuration.TotalMilliseconds * .001
}
});
I have gone through a couple iterations of the serializer I am using, but here is my current version:
static class TwoParamsSerialization
{
// Writing CallbackArguments value as two arguments to the IJSValueWriter.
public static void WriteValue(this IJSValueWriter writer, CallbackArguments value)
{
if (value.Error == null)
{
writer.WriteValue(JSValue.Null);
}
else
{
writer.WriteValue(value.Error);
}
if (value.Props == null)
{
writer.WriteValue(JSValue.Null);
}
else
{
writer.WriteValue(value.Props);
}
}
}
I had an earlier version working using structs instead of classes when everything was being assigned a value, but I have scenarios where the JavaScript's callback is not always expecting a value for one of the arguments or one of the properties on an argument. For instance, null is a valid response for Error
when we have no error, and there is a possibility that there might not be a value for TotalChannels
on Properties
.
Can you please help me out with this? I cannot figure out what I am doing wrong... I _really_ appreciate your time and effort. Thank you!
After stepping away for a little bit, I came back with a different approach but still getting an exception I can't understand in Microsoft.ReactNative.h
Your answer seems clear to me that I just needed to intercept the serialization and write the properties as two separate arguments. I tried to make it as easy, yet realistic, as I can.
struct CallbackArguments
{
public JSValue Error;
public JSValue Props;
}
static class TwoParamsSerialization
{
// Writing CallbackArguments value as two arguments to the IJSValueWriter.
public static void WriteValue(this IJSValueWriter writer, CallbackArguments value)
{
if (value.Error.IsNull)
{
writer.WriteNull();
}
else
{
writer.WriteValue(value.Error);
}
if (value.Props.IsNull)
{
writer.WriteNull();
}
else
{
writer.WriteValue(value.Props);
}
}
}
[ReactModule]
class RNSound
{
[ReactMethod("prepare")]
public async Task Prepare(String fileName, int key, JSValue options, Action<CallbackArguments> callback)
{
...
callback(
new CallbackArguments
{
Error = JSValue.Null,
Props = new JSValueObject() { { "duration", new JSValue(totalMilliseconds * .001) } }
});
}
@woodrufs, thank you for the minimal repro! Let me have a look at it and I will get back to you as soon as possible.
@woodrufs, I have augmented earlier branch with changes to sample project similar to your latest code: https://github.com/vmoroz/react-native-windows/tree/TwoCallbackArgs . It works.
The issue is that the method must return void
instead of Task
.
In our code we never observe the result of the async
method and the situation when method returns Task
is not handled correctly. We allow methods to return a value, but we treat it as method having a callback with such parameter. Handling Task or any other async
function result type in async
functions would require more complex logic. We will see how we can add a compilation error for such cases in future.
@vmoroz,
All I can say is THANK YOU! I cannot tell you how long that would have taken me to figure out (especially since I have had the mindset to never return void when working with async operations)! I have tested it this morning, and everything is working as expected now that I return void. Thanks again, and I hope you have a wonderful day!
I am glad to hear that it works! :)