react-native-windows-samples icon indicating copy to clipboard operation
react-native-windows-samples copied to clipboard

Add Native UI Component example using built-in control

Open dstaley opened this issue 4 years ago • 5 comments

Proposal: Add a sample implementation of a Native UI Component that uses a built-in control

Summary

Currently, the docs for Native UI Components uses a fully custom control. It would be nice to have an example to show how developers can integrate existing UI controls instead of writing their own.

Motivation

As a React developer who is new to Windows development, it is unclear which portions of the existing sample are required if I'm using an existing XAML control. For instance, if I wanted to add a Command bar, do I need to create a class that extends from Control? Do I need to create a class for each different XAML tag type for Command bar? Having a through-yet-understandable example demonstrating a complex XAML control would be a great resource for developers to begin building out React versions of Windows UI components.

Open Questions

  1. Which XAML component should the sample use?

Additional Comments

I'm willing to contribute this documentation provided someone can point me in the right direction (particularly how to handle in JSX components that have nested XAML components).

dstaley avatar May 27 '20 17:05 dstaley

@dstaley this is already available in the playground app (packages\playground\windows\playground.sln), check it out.

asklar avatar May 27 '20 22:05 asklar

@asklar Could you point to which file is implementing a native UI component? I searched for ViewManagerPropertyType, but didn't find anything in the playground that used that API. I'm essentially looking for a more complex example of the one given in the docs. It might be that the docs are out of date with how this is being done now, and in that case an updated sample would also be appreciated!

(For reference, my goal is to add a Command bar to my application, but I figured I'd contribute to the docs in the process!)

dstaley avatar May 27 '20 22:05 dstaley

@dstaley there are 2 main ways to include native UI in a RNW app:

  1. Create a native module/view manager - this is exemplified by the SampleCppLibrary sample. You can also check out some of the native community modules out there, e.g. react-native-datetimepicker.
  2. Mix XAML markup with RNW content - this is exemplified by the playground app - see MainPage.xaml. This file defines the XAML markup for the page and includes a ReactRootView control which will host the RN content. However, you can add any XAML controls to this page (e.g. the playground app includes a play button, comboboxes, etc., that are not being created via RNW but are created natively). Hope this answers your question!

asklar avatar May 28 '20 09:05 asklar

So I actually think the CircleViewManager is more along the lines of what I think a good sample would accomplish. It looks to be managing children, which is what something like the CommandBar would need.

I tried to follow the sample as closely as I could, and I managed to get the app to at least load and not throw an error, but it doesn't seem to be visually rendering a CommandBar or an AppBarButton. By using the XAML tree inspector I can see that it's in the tree, but the app is just a black screen. Turning on the element highlighting functions highlights the space I'd expect the CommandBar to render in, so there's definitely some layout work being done. Do UI controls need additional properties in order to render correctly? Does the renderer inject additional elements that might be causing the CommandBar to fail to render?

CommandBarViewManager.cs

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

using Microsoft.ReactNative;

namespace RNCommandBarSample
{
    class CommandBarViewManager : IViewManager, IViewManagerWithChildren
    {
        public string Name => "CommandBar";

        public FrameworkElement CreateView()
        {
            var view = new CommandBar();
            return view;
        }

        public void AddView(FrameworkElement parent, UIElement child, long index)
        {
            if (parent is CommandBar view && child is AppBarButton button)
            {
                view.PrimaryCommands.Add(button);
            }
        }

        public void RemoveAllChildren(FrameworkElement parent)
        {
            if (parent is CommandBar view)
            {
                view.PrimaryCommands.Clear();
            }
        }

        public void RemoveChildAt(FrameworkElement parent, long index)
        {
            if (parent is CommandBar view)
            {
                view.PrimaryCommands.RemoveAt((int)index);
            }
        }

        public void ReplaceChild(FrameworkElement parent, UIElement oldChild, UIElement newChild)
        {
            if (parent is CommandBar view && oldChild is AppBarButton oldButton && newChild is AppBarButton newButton)
            {
                var oldIndex = view.PrimaryCommands.IndexOf(oldButton);
                view.PrimaryCommands.RemoveAt(oldIndex);
                view.PrimaryCommands.Insert(oldIndex, newButton);
            }
        }
    }
}

AppBarButtonViewManager.cs

using Microsoft.ReactNative.Managed;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace RNCommandBarSample
{
    internal class AppBarButtonViewManager : AttributedViewManager<AppBarButton>
    {
        public override FrameworkElement CreateView()
        {
            var view = new AppBarButton();

            view.RegisterPropertyChangedCallback(AppBarButton.LabelProperty, (obj, prop) =>
            {
                if (obj is AppBarButton b)
                {
                    LabelChanged(b, b.Label);
                }
            });

            return view;
        }

        [ViewManagerProperty("label")]
        public void SetLabel(AppBarButton view, string value)
        {
            if (value != null)
            {
                view.SetValue(AppBarButton.LabelProperty, value);
            } else
            {
                view.ClearValue(AppBarButton.LabelProperty);
            }
        }

        [ViewManagerExportedDirectEventTypeConstant]
        public ViewManagerEvent<AppBarButton, string> LabelChanged;
    }
}

App.tsx

import React from 'react';
import {
  View,
  requireNativeComponent,
} from 'react-native';

const CommandBar = requireNativeComponent('CommandBar');
const AppBarButton = requireNativeComponent('AppBarButton');

const App = () => {
  return (
    <View>
      <CommandBar>
        <AppBarButton label="Button" />
      </CommandBar>
    </View>
  );
};

export default App;

dstaley avatar May 28 '20 15:05 dstaley

We have Playground available as a stopgap example, but we should beef up the documentation for this on the website and/or samples repo.

chrisglein avatar May 28 '20 18:05 chrisglein