When you build a Windows application using React Native for Windows, typically you don't have to worry about the underline generated application. One of the powerful features of React Native is that it generates a truly native application, which means that the JSX controls you place in the UI are translated with the equivalent native XAML control. This feature helps to deliver great performance and a familiar Fluent look & feel to your applications.
However, there are a few scenarios where you would need to access the underlying XAML infrastructure. Recently, I worked on an engagement with a customer who needed to show a flyout when specific actions occur. However, the flyout didn't have to be displayed in a fixed position, but near the control that generated the action (like the click of a button). Achieving this goal only using JSX isn't feasible for two reasons:
- React Native is a technology to build cross-platform applications, while
Flyout
is a specific Windows control. As such, by default, React Native doesn't expose a JSX equivalent. - When you create a
Flyout
, you must link it to another XAML control (like aButton
) by setting itsFlyout
property or by passing it as parameter to theShowAt()
method. We can't access this information in JSX.
In this blog post we'll learn how to support this scenario, by combining a native module and the findNodeHandle()
React Native function, which is the one that gives you most flexibility but it's also more overhead to maintain.
In the second part, instead, we'll see how to achieve the same goal in a simpler way with the react-native-xaml library.
Create the native module
The first step is to create a native module, which we'll need to interact with the native XAML control. You can follow the guidance on the official website to create a new one and add support for Windows. Once you have the basic infrastructure, we can add in the module class a method to display the flyout, as in the following example:
namespace ReactNativeInAppNotifications
{
[ReactModule("inappnotifications")]
internal sealed class ReactNativeModule
{
private ReactContext _reactContext;
[ReactInitializer]
public void Initialize(ReactContext reactContext)
{
_reactContext = reactContext;
}
[ReactMethod("showNotification")]
public void ShowNotification(int tag, string title)
{
_reactContext.Handle.UIDispatcher.Post(() =>
{
Flyout flyout = new Flyout
{
Content = new TextBlock { Text = title }
};
var control = XamlUIService.FromContext(_reactContext.Handle).ElementFromReactTag(tag) as FrameworkElement;
flyout.ShowAt(control);
});
}
}
}
First, notice how the class is decorated with the [ReactModule]
attribute, while the ShowNotification()
method is decorated with the [ReactMethod]
attribute. They will enable us to access them from the JavaScript layer of the application. The ShowNotification()
method accepts two parameters: one is the title of the notification, while the other one is a tag, which is an index that we can use to reference a JSX control. We'll learn later in the post, when we'll talk about the JavaScript layer, how to pass this information to the native module.
The next step is to create our Flyout
control. In the previous snippet, we're using a very simple approach and we're setting the Content
property with a new TextBlock
control; then we set its Text
property with the title that we're passing from the JavaScript layer. Since we are using native code, you have the flexibility to customize the Flyout
control as you prefer: you can use a more complex XAML to define the Content
; or you can customize other properties like LightDismissOverlayMode
or ShowMode
.
Now that we have our flyout, we need to display it near the control that triggered the action. As such, we need to use the tag we have received from the JavaScript layer to retrieve a reference to the corresponding XAML control. We can achieve this goal thanks to the XamlUIService
class offered by React Native for Windows. First, we get an instance from the current context (which is exposed by the Handle
property of the ReactContext
object that is set in the Initialize()
method of the module). Then, we call the ElemenFromReactTag()
method, passing as parameter the tag we have received. In the end, we cast the result into a FrameworkElement
object, which is the base class of XAML controls.
Now we have access to underline XAML control which triggered the event, so we can just pass it to ShowAt()
method exposed by the Flyout
control. This will make sure that the Flyout
will be displayed near the control.
Let's see now the code that we have to write in the React Native layer to use the native module we have just written.
Invoke the native module from JavaScript
Let's now create a React Native component that we can use to display the notification. Let's see the code first, then we'll comment it.
import React from 'react';
import {Button, NativeModules, findNodeHandle} from 'react-native';
class NotificationComponent extends React.Component {
constructor(props) {
super(props);
this.myButton = React.createRef();
}
showNotification = async () => {
await NativeModules.inappnotifications.showNotification(findNodeHandle(this.myButton.current), 'Hello World');
};
render() {
return (
<Button
title="Click me"
onPress={this.showNotification}
ref={this.myButton} />
);
}
}
export default NotificationComponent;
In order to pass to the native module the JSX control which triggered the event, we need to use the concept of reference in React Native. You can think of it like the x:Name
property in XAML: it's a way to directly reference a control from code. If this approach is frequently used in other platforms (like WPF or Windows Forms), it's not very common in React Native. In most of the scenarios, in fact, you won't need to directly access to the control, but you're going to use properties, events and state to implement the logic you need.
However, there are scenarios (like this one) in which a reference is the only way to achieve a goal and, as such, React Native provides the infrastructure to implement it. This is achieved with two changes to the code:
- In the constructor of the component, we define a variable (in this case,
myButton
) and we initialize it by callingReact.createRef()
. - In JSX, we set the
ref
property of the control we want to reference (in this sample, aButton
control) with the same variable we have created in the constructor (in this case,myButton
).
Now we can access to the control by using the current
property of the myButton
object. We have everything we need now, so we can call the showNotification()
method exposed by our native module, through the NativeModules
object offered by React Native. Remember that the syntax to use a native module is NativeModule.<class name>.<method name>
. In our example, it's NativeModules.inappnotifications.showNotification
.
Well, we have almost everything we need, there's one last thing to add. Remember that the ShowNotification()
method in the native module requires a tag to reference the XAML control? To translate our reference to a tag, we must use the findNodeHandle()
method offered by React Native. This is how the implementation looks like:
showNotification = async () => {
await NativeModules.inappnotifications.showNotification(findNodeHandle(this.myButton.current), 'Hello World');
};
To get the tag, we invoke findNodeHandle()
passing, as parameter, the current
property of the myButton
reference we have created earlier.
In the end, we just connect the showNotification()
method we have just created to the onPress
event of the Button
control, so that we can invoke it when it's clicked.
Wrapping up
That's it! Now, if you launch your React Native for Windows application and you press the button, you will see the flyout being displayed right at the top of the Button:
You can test the full code with the sample application on GitHub.
In the second part of this post, we'll see another and more powerful approach to support this scenario: using the react-native-xaml library.
Happy coding!
[UPDATE] The second part of the post is now live.