react-native-windows-samples
react-native-windows-samples copied to clipboard
UIDispatcher how to return value/resolve/reject promise without crash? (Documentation unlcear)
Problem Description
I have two small native c++ modules which use the Launcher::LaunchFileAsync
and FileOpenPicker::pickSingleFileAsync
.
After upgrading from 0.63 to 0.65 we have to use the UIDispatcher in order for it to work. So I had a look at the following Documentation: Using UIDispatcher with C++/WinRT
Unfortunately, the example lacks the bit about how to return a value from there (in that example, that would probably be the file.Path()
).
So I digged a bit further and had a look at the following 4 modules inside react-native-windows: Clipboard Module AccessibilityInfoModule ImageViewManagerModule AlertModule
I tried multiple different variations now and had a look at the crash dump but I always crash with an Access Violation
on the promise.resolve
line.
What am I doing wrong there? I don't see a lot of differences between my code and the above modules despite the co_await
usage.
I'm also having the same problem with FileOpenPicker where I'd want to resolve the tempFile.Path()
after picking+moving it to the apps temp folder (StorageFile tempFile{ co_await file.CopyAsync(winrt::Windows::Storage::ApplicationData::Current().TemporaryFolder(), file.Name(), NameCollisionOption::ReplaceExisting) };
)
Steps To Reproduce
- Init a new project
- Add the FileOpener posted below to the project
- Add an openable file to the application folder (for example a pdf)
- Call
NativeModules.FileOpener.openFileAsync(filePath, shouldShowOpenWithDialog)
Expected Results
Not crash, resolve the promise to JS.
Documentation to reflect an actual usage of FileOpenPicker/UIDispatcher with result returns/promise.resolve.
CLI version
6.1.0
Environment
info Fetching system and libraries information...
System:
OS: Windows 10 10.0.19043
CPU: (12) x64 AMD Ryzen 5 5600X 6-Core Processor
Memory: 24.72 GB / 31.94 GB
Binaries:
Node: 14.16.0 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.11 - ~\AppData\Roaming\npm\yarn.CMD
npm: 6.14.11 - C:\Program Files\nodejs\npm.CMD
Watchman: Not Found
SDKs:
Android SDK:
API Levels: 28, 29, 30
Build Tools: 29.0.0, 29.0.1, 29.0.2, 29.0.3, 30.0.0, 30.0.1, 30.0.2, 30.0.3
System Images: android-28 | Google APIs Intel x86 Atom, android-29 | Google APIs Intel x86 Atom, android-30 | Google APIs Intel x86 Atom
Android NDK: Not Found
Windows SDK:
AllowDevelopmentWithoutDevLicense: Enabled
AllowAllTrustedApps: Enabled
Versions: 10.0.16299.0, 10.0.17134.0, 10.0.17763.0, 10.0.18362.0, 10.0.19041.0
IDEs:
Android Studio: Not Found
Visual Studio: 16.9.31313.79 (Visual Studio Community 2019)
Languages:
Java: 1.8.0_282 - /c/Program Files/OpenJDK/openjdk-8u282-b08/bin/javac
npmPackages:
@react-native-community/cli: Not Found
react: 17.0.2 => 17.0.2
react-native: 0.65.2 => 0.65.2
react-native-windows: ^0.65.7 => 0.65.7
npmGlobalPackages:
*react-native*: Not Found
Target Platform Version
10.0.19041
Target Device(s)
Desktop
Visual Studio Version
Visual Studio 2019
Build Configuration
Release
Snack, code example, screenshot, or link to a repository
FileOpener.h
#pragma once
#include "pch.h"
#include "NativeModules.h"
#include <string>
namespace RN = winrt::Microsoft::ReactNative;
namespace inspection::FileOpener {
REACT_MODULE(FileOpener, L"FileOpener");
struct FileOpener final {
RN::ReactContext m_reactContext;
REACT_INIT(Initialize)
void Initialize(RN::ReactContext const& reactContext) noexcept;
REACT_METHOD(openFileAsync)
winrt::fire_and_forget openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept;
};
}
FileOpener.cpp
#include "pch.h"
#include "FileOpener.h"
#include <filesystem>
#include <windows.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.System.h>
using namespace winrt;
using namespace winrt::Windows::Storage;
using namespace winrt::Windows::Foundation;
using namespace Windows::System;
namespace inspection::FileOpener {
void FileOpener::Initialize(RN::ReactContext const& reactContext) noexcept {
m_reactContext = reactContext;
}
winrt::fire_and_forget FileOpener::openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept
try {
std::filesystem::path fPath{ filepath };
fPath.make_preferred();
auto jsDispatcher = m_reactContext.JSDispatcher();
try {
StorageFile file{ co_await StorageFile::GetFileFromPathAsync(winrt::to_hstring(fPath.c_str())) };
try {
if (file) {
m_reactContext.UIDispatcher().Post([showOpenWithDialog, file, promise, fPath, jsDispatcher]()->winrt::fire_and_forget {
LauncherOptions launchOptions;
launchOptions.DisplayApplicationPicker(showOpenWithDialog);
bool success{ co_await Launcher::LaunchFileAsync(file, launchOptions) };
if (success) {
jsDispatcher.Post([promise] { promise.Resolve(); });
} else {
jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
}
});
} else {
promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) });
}
} catch (const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to LaunchFileAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
}
} catch(const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to GetFileFromPathAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
}
} catch(const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to make path or make_preferred for File " + filepath, winrt::to_string(ex.message()).c_str() });
}
}
Thanks for the detailed issue report, @creambyemute. Sorry that the documentation hasn't been clear here. Let me see if we can bring in someone to help unblock you and also get the documentation better expanded for the next person.
Since I think this is a documentation issue I'll move to the repo where that article lives.
I have got it working now. Turns out, the comment // unfortunately, lambda captures doesn't work well with winrt::fire_and_forget and co_await here // call asyncOp.Completed explicitly
in ClipboardModule
is correct.
When using The IAsyncOperation<T>
with asyncOp.Completed
I can successfully resolve to the JS realm.
Is there any way to use co_await
in this scenario? The code would be a lot shorter without the asyncOp.Completed
, especially for the case where you'd have more than one of the asyncOperations.
Edit: According to https://github.com/microsoft/react-native-windows-samples/issues/427#issuecomment-826667012 it may be possible with a std::move
on the promise. I'll try that out an report back (I think this may be worth mentioning in the docs)
winrt::fire_and_forget FileOpener::openFileAsync(std::string filepath, bool showOpenWithDialog, RN::ReactPromise<void> promise) noexcept
try {
std::filesystem::path fPath{ filepath };
fPath.make_preferred();
auto jsDispatcher = m_reactContext.JSDispatcher();
try {
StorageFile file{ co_await StorageFile::GetFileFromPathAsync(winrt::to_hstring(fPath.c_str())) };
try {
if (file) {
m_reactContext.UIDispatcher().Post([showOpenWithDialog, file, promise, fPath, jsDispatcher] {
LauncherOptions launchOptions;
launchOptions.DisplayApplicationPicker(showOpenWithDialog);
IAsyncOperation<bool> asyncLaunchFileOp = Launcher::LaunchFileAsync(file, launchOptions);
asyncLaunchFileOp.Completed([jsDispatcher, promise, fPath](const IAsyncOperation<bool>& asyncLaunchFileOp, AsyncStatus status) {
switch (status) {
case AsyncStatus::Completed: {
bool success = asyncLaunchFileOp.GetResults();
if (success) {
jsDispatcher.Post([promise] { promise.Resolve(); });
}
else {
jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
}
break;
}
case AsyncStatus::Canceled: {
jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
break;
}
case AsyncStatus::Error: {
auto message = std::wstring(winrt::hresult_error(asyncLaunchFileOp.ErrorCode()).message());
jsDispatcher.Post([promise, message, fPath] { promise.Reject(RN::ReactError{ "Unable to open File" + winrt::to_string(fPath.c_str()), winrt::to_string(message.c_str()) }); });
break;
}
case AsyncStatus::Started: {
jsDispatcher.Post([promise, fPath] { promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) }); });
break;
}
}
});
});
} else {
promise.Reject(RN::ReactError{ "Unable to open File", winrt::to_string(fPath.c_str()) });
}
} catch (const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to LaunchFileAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
}
} catch(const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to GetFileFromPathAsync for File " + filepath, winrt::to_string(ex.message()).c_str() });
}
} catch(const hresult_error& ex) {
promise.Reject(RN::ReactError{ "Unable to make path or make_preferred for File " + filepath, winrt::to_string(ex.message()).c_str() });
}