diff --git a/ReactWindows/Playground/App.xaml.cs b/ReactWindows/Playground/App.xaml.cs index 5cf14d70460..f7562c9b5d1 100644 --- a/ReactWindows/Playground/App.xaml.cs +++ b/ReactWindows/Playground/App.xaml.cs @@ -1,18 +1,11 @@ -using System; -using System.Collections.Generic; -using System.IO; +using ReactNative; +using ReactNative.Shell; +using System; using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; -using Windows.Foundation; -using Windows.Foundation.Collections; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; namespace Playground @@ -22,6 +15,8 @@ namespace Playground /// sealed partial class App : Application { + private readonly ReactPage _reactPage; + /// /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). @@ -33,6 +28,13 @@ public App() Microsoft.ApplicationInsights.WindowsCollectors.Session); this.InitializeComponent(); this.Suspending += OnSuspending; + this.Resuming += OnResuming; + + _reactPage = new ReactPage( + "ms-appx:///Resources/main.dev.jsbundle", + "ReactRoot", + new[] { new MainReactPackage() }.ToList(), + () => { /* TODO: back button handling */ }); } /// @@ -42,6 +44,8 @@ public App() /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs e) { + _reactPage.OnCreate(); + _reactPage.OnResume(); #if DEBUG if (System.Diagnostics.Debugger.IsAttached) @@ -75,8 +79,9 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) // When the navigation stack isn't restored navigate to the first page, // configuring the new page by passing required information as a navigation // parameter - rootFrame.Navigate(typeof(MainPage), e.Arguments); + rootFrame.Content = _reactPage; } + // Ensure the current window is active Window.Current.Activate(); } @@ -101,8 +106,21 @@ void OnNavigationFailed(object sender, NavigationFailedEventArgs e) private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); + //TODO: Save application state and stop any background activity + _reactPage.OnSuspend(); + deferral.Complete(); } + + /// + /// Invoked when application execution is being resumed. + /// + /// The source of the resume request. + /// Details about the resume request. + private void OnResuming(object sender, object e) + { + _reactPage.OnResume(); + } } } diff --git a/ReactWindows/Playground/MainPage.xaml b/ReactWindows/Playground/MainPage.xaml deleted file mode 100644 index 778358f2df0..00000000000 --- a/ReactWindows/Playground/MainPage.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/ReactWindows/Playground/MainPage.xaml.cs b/ReactWindows/Playground/MainPage.xaml.cs deleted file mode 100644 index 1fc6bd88177..00000000000 --- a/ReactWindows/Playground/MainPage.xaml.cs +++ /dev/null @@ -1,46 +0,0 @@ -using ReactNative.Hosting; -using ReactNative.Hosting.Bridge; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Navigation; - -// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 - -namespace Playground -{ - /// - /// An empty page that can be used on its own or navigated to within a Frame. - /// - public sealed partial class MainPage : Page - { - public MainPage() - { - this.InitializeComponent(); - } - - private void LiftReactNativeApp() - { - var jsModuleName = "ReactRoot"; - var bundleAssetName = "ms-appx:///Resources/main.dev.jsbundle"; - RootView?.LiftAsync(bundleAssetName, jsModuleName); - } - - protected override void OnNavigatedTo(NavigationEventArgs e) - { - base.OnNavigatedTo(e); - this.LiftReactNativeApp(); - } - } -} diff --git a/ReactWindows/Playground/Playground.csproj b/ReactWindows/Playground/Playground.csproj index e5ea7c85dcc..4c216aeedae 100644 --- a/ReactWindows/Playground/Playground.csproj +++ b/ReactWindows/Playground/Playground.csproj @@ -104,9 +104,6 @@ App.xaml - - MainPage.xaml - @@ -130,10 +127,6 @@ MSBuild:Compile Designer - - MSBuild:Compile - Designer - diff --git a/ReactWindows/ReactNative.Tests/Bridge/CatalystInstanceTests.cs b/ReactWindows/ReactNative.Tests/Bridge/CatalystInstanceTests.cs index 6a4f7caf344..bad3debb068 100644 --- a/ReactWindows/ReactNative.Tests/Bridge/CatalystInstanceTests.cs +++ b/ReactWindows/ReactNative.Tests/Bridge/CatalystInstanceTests.cs @@ -31,7 +31,7 @@ public async Task CatalystInstance_GetModules() QueueConfigurationSpec = CatalystQueueConfigurationSpec.Default, Registry = registry, JavaScriptModulesConfig = jsConfig, - JavaScriptExecutor = executor, + JavaScriptExecutorFactory = () => executor, BundleLoader = JavaScriptBundleLoader.CreateFileLoader("ms-appx:///Resources/test.js"), NativeModuleCallExceptionHandler = _ => { } }; @@ -63,7 +63,7 @@ public async Task CatalystInstance_Initialize_Dispose() QueueConfigurationSpec = CatalystQueueConfigurationSpec.Default, Registry = registry, JavaScriptModulesConfig = jsConfig, - JavaScriptExecutor = executor, + JavaScriptExecutorFactory = () => executor, BundleLoader = JavaScriptBundleLoader.CreateFileLoader("ms-appx:///Resources/test.js"), NativeModuleCallExceptionHandler = _ => { }, }; @@ -109,7 +109,7 @@ public async Task CatalystInstance_ExceptionHandled_Disposes() QueueConfigurationSpec = CatalystQueueConfigurationSpec.Default, Registry = registry, JavaScriptModulesConfig = jsConfig, - JavaScriptExecutor = executor, + JavaScriptExecutorFactory = () => executor, BundleLoader = JavaScriptBundleLoader.CreateFileLoader("ms-appx:///Resources/test.js"), NativeModuleCallExceptionHandler = handler, }; diff --git a/ReactWindows/ReactNative.Tests/Bridge/ReactContextNativeModuleBaseTests.cs b/ReactWindows/ReactNative.Tests/Bridge/ReactContextNativeModuleBaseTests.cs index f30d07bf7b8..1571dbd68be 100644 --- a/ReactWindows/ReactNative.Tests/Bridge/ReactContextNativeModuleBaseTests.cs +++ b/ReactWindows/ReactNative.Tests/Bridge/ReactContextNativeModuleBaseTests.cs @@ -14,14 +14,14 @@ public void ReactContextNativeModuleBase_ArgumentChecks() () => new TestModule(null), ex => Assert.AreEqual("reactContext", ex.ParamName)); - var context = new ReactApplicationContext(); + var context = new ReactContext(); var module = new TestModule(context); Assert.AreSame(context, module.Context); } class TestModule : ReactContextNativeModuleBase { - public TestModule(ReactApplicationContext reactContext) + public TestModule(ReactContext reactContext) : base(reactContext) { } diff --git a/ReactWindows/ReactNative.Tests/Internal/JavaScriptHelpers.cs b/ReactWindows/ReactNative.Tests/Internal/JavaScriptHelpers.cs index 01103dc6720..5d3b10e7380 100644 --- a/ReactWindows/ReactNative.Tests/Internal/JavaScriptHelpers.cs +++ b/ReactWindows/ReactNative.Tests/Internal/JavaScriptHelpers.cs @@ -62,8 +62,6 @@ public static async Task Initialize(ChakraJavaScriptExecutor executor, IMessageQ await jsQueueThread.CallOnQueue(() => { - executor.Initialize(); - foreach (var script in scripts) { executor.RunScript(script); diff --git a/ReactWindows/ReactNative.Tests/Modules/Toast/ToastNotificationTests.cs b/ReactWindows/ReactNative.Tests/Modules/Toast/ToastNotificationTests.cs index 1e3bc6125da..fa41bdb0889 100644 --- a/ReactWindows/ReactNative.Tests/Modules/Toast/ToastNotificationTests.cs +++ b/ReactWindows/ReactNative.Tests/Modules/Toast/ToastNotificationTests.cs @@ -19,7 +19,7 @@ public void ToastModule_Null_ArgumentsTest() ex => Assert.AreEqual("reactContext", ex.ParamName)); - var context = new ReactApplicationContext(); + var context = new ReactContext(); var module = new ToastModule(context); Assert.AreSame(context, module.Context); @@ -29,7 +29,7 @@ public void ToastModule_Null_ArgumentsTest() [TestCategory(TEST_CATEGORY)] public void Send_Toast_Invalid_Duration() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var module = new ToastModule(context); AssertEx.Throws( @@ -41,7 +41,7 @@ public void Send_Toast_Invalid_Duration() [TestCategory(TEST_CATEGORY)] public void Send_Basic_Toast() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var module = new ToastModule(context); module.show("SHORT TOAST", 0); @@ -51,7 +51,7 @@ public void Send_Basic_Toast() [TestCategory(TEST_CATEGORY)] public void Send_Long_Toast() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var module = new ToastModule(context); module.show("LONG TOAST container", 1); diff --git a/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs b/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs index 2aaa7df2bbd..e81255fb06c 100644 --- a/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs +++ b/ReactWindows/ReactNative.Tests/ReactInstanceManagerTests.cs @@ -1,5 +1,8 @@ using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; -using ReactNative.Views; +using ReactNative.Bridge; +using ReactNative.Modules.Core; +using System; +using System.Threading; using System.Threading.Tasks; namespace ReactNative.Tests @@ -7,14 +10,161 @@ namespace ReactNative.Tests [TestClass] public class ReactInstanceManagerTests { - //TODO: Looking into XAML custom control issue. This test is currently being ignored for the meantime until the issue has been resolved. - public async Task ReactInstanceManagerInitializationSuccess() - { - var jsModuleName = "index.windows"; - var bundleAssetName = "ms-appx:///Resources/main.jsbundle"; - var rootView = await DispatcherHelpers.CallOnDispatcherAsync(() => new ReactRootView()); - rootView.LiftAsync(bundleAssetName, jsModuleName); - Assert.AreEqual(rootView.TagId, 0); + [TestMethod] + public void ReactInstanceManager_Builder_SetterChecks() + { + AssertEx.Throws( + () => new ReactInstanceManager.Builder + { + JavaScriptBundleFile = "ms-appx:///Resources/main.jsbundle", + }.Build()); + + AssertEx.Throws( + () => new ReactInstanceManager.Builder + { + InitialLifecycleState = LifecycleState.Resumed, + }.Build()); + } + + [TestMethod] + public void ReactInstanceManager_ArgumentChecks() + { + var manager = CreateReactInstanceManager(); + + AssertEx.Throws( + () => manager.AttachMeasuredRootView(null), + ex => Assert.AreEqual("rootView", ex.ParamName)); + + AssertEx.Throws( + () => manager.CreateAllViewManagers(null), + ex => Assert.AreEqual("reactContext", ex.ParamName)); + + AssertEx.Throws( + () => manager.DetachRootView(null), + ex => Assert.AreEqual("rootView", ex.ParamName)); + + AssertEx.Throws( + () => manager.OnResume(null), + ex => Assert.AreEqual("onBackPressed", ex.ParamName)); + } + + [TestMethod] + public async Task ReactInstanceManager_CreateInBackground() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var waitHandle = new AutoResetEvent(false); + manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); + + await DispatcherHelpers.RunOnDispatcherAsync( + () => manager.CreateReactContextInBackground()); + + Assert.IsTrue(waitHandle.WaitOne()); + Assert.AreEqual(jsBundleFile, manager.SourceUrl); + } + + [TestMethod] + public async Task ReactInstanceManager_CreateInBackground_EnsuresOneCall() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var waitHandle = new AutoResetEvent(false); + manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); + + await AssertEx.ThrowsAsync(() => + DispatcherHelpers.RunOnDispatcherAsync(() => + { + manager.CreateReactContextInBackground(); + manager.CreateReactContextInBackground(); + })); + } + + [TestMethod] + public async Task ReactInstanceManager_RecreateInBackground() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var waitHandle = new AutoResetEvent(false); + manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); + + await DispatcherHelpers.RunOnDispatcherAsync(() => + { + manager.CreateReactContextInBackground(); + manager.RecreateReactContextInBackground(); + }); + + Assert.IsTrue(waitHandle.WaitOne()); + Assert.IsTrue(waitHandle.WaitOne()); + Assert.AreEqual(jsBundleFile, manager.SourceUrl); + } + + [TestMethod] + public async Task ReactInstanceManager_RecreateInBackground_EnsuresCalledOnce() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var waitHandle = new AutoResetEvent(false); + manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); + + await AssertEx.ThrowsAsync(() => + DispatcherHelpers.RunOnDispatcherAsync(() => + manager.RecreateReactContextInBackground())); + } + + [TestMethod] + public async Task ReactInstanceManager_OnBackPressed_NoContext() + { + var waitHandle = new AutoResetEvent(false); + var manager = CreateReactInstanceManager(); + await DispatcherHelpers.RunOnDispatcherAsync(() => + { + manager.OnResume(() => waitHandle.Set()); + manager.OnBackPressed(); + }); + + Assert.IsTrue(waitHandle.WaitOne()); + } + + [TestMethod] + public async Task ReactInstanceManager_OnDestroy_CreateInBackground() + { + var jsBundleFile = "ms-appx:///Resources/test.js"; + var manager = CreateReactInstanceManager(jsBundleFile); + + var waitHandle = new AutoResetEvent(false); + manager.ReactContextInitialized += (sender, args) => waitHandle.Set(); + + await DispatcherHelpers.RunOnDispatcherAsync( + () => manager.CreateReactContextInBackground()); + + Assert.IsTrue(waitHandle.WaitOne()); + Assert.AreEqual(jsBundleFile, manager.SourceUrl); + + await DispatcherHelpers.RunOnDispatcherAsync( + () => manager.OnDestroy()); + + await DispatcherHelpers.RunOnDispatcherAsync( + () => manager.CreateReactContextInBackground()); + + Assert.IsTrue(waitHandle.WaitOne()); + } + + private static ReactInstanceManager CreateReactInstanceManager() + { + return CreateReactInstanceManager("ms-appx:///Resources/main.jsbundle"); + } + + private static ReactInstanceManager CreateReactInstanceManager(string jsBundleFile) + { + return new ReactInstanceManager.Builder + { + InitialLifecycleState = LifecycleState.Resumed, + JavaScriptBundleFile = jsBundleFile, + }.Build(); } } } diff --git a/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs b/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs index fc2cec07392..80b2bab9057 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/Events/EventDispatcherTests.cs @@ -18,7 +18,7 @@ public void EventDispatcher_ArgumentChecks() { AssertEx.Throws(() => new EventDispatcher(null), ex => Assert.AreEqual("reactContext", ex.ParamName)); - var context = new ReactApplicationContext(); + var context = new ReactContext(); var dispatcher = new EventDispatcher(context); AssertEx.Throws(() => dispatcher.DispatchEvent(null), ex => Assert.AreEqual("event", ex.ParamName)); } @@ -26,7 +26,7 @@ public void EventDispatcher_ArgumentChecks() [TestMethod] public void EventDispatcher_IncorrectThreadCalls() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var dispatcher = new EventDispatcher(context); AssertEx.Throws(() => dispatcher.OnResume()); @@ -269,7 +269,7 @@ public async Task EventDispatcher_OnShutdown_EventDoesNotDispatch() using (BlockJavaScriptThread(context)) { dispatcher.DispatchEvent(testEvent); - await DispatcherHelpers.RunOnDispatcherAsync(dispatcher.OnShutdown); + await DispatcherHelpers.RunOnDispatcherAsync(dispatcher.OnDestroy); } Assert.IsFalse(waitDispatched.WaitOne(500)); @@ -325,11 +325,11 @@ public async Task EventDispatcher_DispatchedAfterSuspend_ThenResume() Assert.IsTrue(waitDispatched.WaitOne()); } - private static async Task CreateContextAsync(IJavaScriptExecutor executor) + private static async Task CreateContextAsync(IJavaScriptExecutor executor) { var catalystInstance = await DispatcherHelpers.CallOnDispatcherAsync(() => CreateCatalystInstance(executor)); await InitializeCatalystInstanceAsync(catalystInstance); - var context = new ReactApplicationContext(); + var context = new ReactContext(); context.InitializeWithInstance(catalystInstance); return context; } @@ -347,7 +347,7 @@ private static CatalystInstance CreateCatalystInstance(IJavaScriptExecutor execu BundleLoader = JavaScriptBundleLoader.CreateFileLoader("ms-appx:///Resources/test.js"), JavaScriptModulesConfig = jsModules, Registry = registry, - JavaScriptExecutor = executor, + JavaScriptExecutorFactory = () => executor, NativeModuleCallExceptionHandler = ex => Assert.Fail(ex.ToString()), }.Build(); diff --git a/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs b/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs index 28ac4c75a22..7cdcfa419b3 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs @@ -15,7 +15,7 @@ public class UIManagerModuleTests [TestMethod] public void UIManagerModule_ArgumentChecks() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var viewManagers = new List(); var uiImplementation = new UIImplementation(context, viewManagers); @@ -31,7 +31,7 @@ public void UIManagerModule_ArgumentChecks() [TestMethod] public void UIManagerModule_CustomEvents_Constants() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var viewManagers = new List(); var uiImplementation = new UIImplementation(context, viewManagers); @@ -59,7 +59,7 @@ public void UIManagerModule_CustomEvents_Constants() [TestMethod] public void UIManagerModule_Constants_ViewManagerOverrides() { - var context = new ReactApplicationContext(); + var context = new ReactContext(); var viewManagers = new List { new TestViewManager() }; var uiImplementation = new UIImplementation(context, viewManagers); diff --git a/ReactWindows/ReactNative/Bridge/CatalystInstance.cs b/ReactWindows/ReactNative/Bridge/CatalystInstance.cs index 5866e86c8fb..ab1186f6aee 100644 --- a/ReactWindows/ReactNative/Bridge/CatalystInstance.cs +++ b/ReactWindows/ReactNative/Bridge/CatalystInstance.cs @@ -20,7 +20,7 @@ class CatalystInstance : ICatalystInstance, IDisposable { private readonly NativeModuleRegistry _registry; private readonly JavaScriptModuleRegistry _jsRegistry; - private readonly IJavaScriptExecutor _jsExecutor; + private readonly Func _jsExecutorFactory; private readonly JavaScriptBundleLoader _bundleLoader; private readonly JavaScriptModulesConfig _jsModulesConfig; private readonly Action _nativeModuleCallExceptionHandler; @@ -31,14 +31,14 @@ class CatalystInstance : ICatalystInstance, IDisposable private CatalystInstance( CatalystQueueConfigurationSpec catalystQueueConfigurationSpec, - IJavaScriptExecutor jsExecutor, + Func jsExecutorFactory, NativeModuleRegistry registry, JavaScriptModulesConfig jsModulesConfig, JavaScriptBundleLoader bundleLoader, Action nativeModuleCallExceptionHandler) { _registry = registry; - _jsExecutor = jsExecutor; + _jsExecutorFactory = jsExecutorFactory; _jsModulesConfig = jsModulesConfig; _nativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler; _bundleLoader = bundleLoader; @@ -90,6 +90,35 @@ public void Initialize() _registry.NotifyCatalystInstanceInitialize(); } + public async Task InitializeBridgeAsync() + { + await _bundleLoader.InitializeAsync(); + + await QueueConfiguration.JSQueueThread.CallOnQueue(() => + { + QueueConfiguration.JSQueueThread.AssertIsOnThread(); + + var jsExecutor = _jsExecutorFactory(); + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "ReactBridgeCtor")) + { + _bridge = new ReactBridge( + jsExecutor, + new NativeModulesReactCallback(this), + QueueConfiguration.NativeModulesQueueThread); + } + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "setBatchedBridgeConfig")) + { + _bridge.SetGlobalVariable("__fbBatchedBridgeConfig", BuildModulesConfig()); + } + + _bundleLoader.LoadScript(jsExecutor); + + return _bridge; + }); + } + public void InvokeCallback(int callbackId, JArray arguments) { if (IsDisposed) @@ -151,35 +180,6 @@ public void Dispose() // TODO: notify bridge idle listeners } - public async Task InitializeBridgeAsync() - { - await _bundleLoader.InitializeAsync(); - - await QueueConfiguration.JSQueueThread.CallOnQueue(() => - { - QueueConfiguration.JSQueueThread.AssertIsOnThread(); - - _jsExecutor.Initialize(); - - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "ReactBridgeCtor")) - { - _bridge = new ReactBridge( - _jsExecutor, - new NativeModulesReactCallback(this), - QueueConfiguration.NativeModulesQueueThread); - } - - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "setBatchedBridgeConfig")) - { - _bridge.SetGlobalVariable("__fbBatchedBridgeConfig", BuildModulesConfig()); - } - - _bundleLoader.LoadScript(_jsExecutor); - - return _bridge; - }); - } - private string BuildModulesConfig() { using (var stringWriter = new StringWriter()) @@ -209,7 +209,7 @@ public sealed class Builder private CatalystQueueConfigurationSpec _catalystQueueConfigurationSpec; private NativeModuleRegistry _registry; private JavaScriptModulesConfig _jsModulesConfig; - private IJavaScriptExecutor _jsExecutor; + private Func _jsExecutorFactory; private JavaScriptBundleLoader _bundleLoader; private Action _nativeModuleCallExceptionHandler; @@ -237,11 +237,11 @@ public JavaScriptModulesConfig JavaScriptModulesConfig } } - public IJavaScriptExecutor JavaScriptExecutor + public Func JavaScriptExecutorFactory { set { - _jsExecutor = value; + _jsExecutorFactory = value; } } @@ -264,7 +264,7 @@ public Action NativeModuleCallExceptionHandler public CatalystInstance Build() { AssertNotNull(_catalystQueueConfigurationSpec, nameof(QueueConfigurationSpec)); - AssertNotNull(_jsExecutor, nameof(IJavaScriptExecutor)); + AssertNotNull(_jsExecutorFactory, nameof(JavaScriptExecutorFactory)); AssertNotNull(_registry, nameof(Registry)); AssertNotNull(_jsModulesConfig, nameof(JavaScriptModulesConfig)); AssertNotNull(_bundleLoader, nameof(BundleLoader)); @@ -272,7 +272,7 @@ public CatalystInstance Build() return new CatalystInstance( _catalystQueueConfigurationSpec, - _jsExecutor, + _jsExecutorFactory, _registry, _jsModulesConfig, _bundleLoader, diff --git a/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs b/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs index e2cd23207a9..1a226e0bab2 100644 --- a/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs +++ b/ReactWindows/ReactNative/Bridge/IJavaScriptExecutor.cs @@ -8,14 +8,6 @@ namespace ReactNative.Bridge /// public interface IJavaScriptExecutor : IDisposable { - /// - /// Initializes the JavaScript runtime. - /// - /// - /// Must be called from the JavaScript thread. - /// - void Initialize(); - /// /// Call the JavaScript method from the given module. /// diff --git a/ReactWindows/ReactNative/Bridge/ILifecycleEventListener.cs b/ReactWindows/ReactNative/Bridge/ILifecycleEventListener.cs index 55f17974718..86f40947bf7 100644 --- a/ReactWindows/ReactNative/Bridge/ILifecycleEventListener.cs +++ b/ReactWindows/ReactNative/Bridge/ILifecycleEventListener.cs @@ -18,6 +18,6 @@ public interface ILifecycleEventListener /// /// Called when the host is shutting down. /// - void OnShutdown(); + void OnDestroy(); } } diff --git a/ReactWindows/ReactNative/Bridge/ReactApplicationContext.cs b/ReactWindows/ReactNative/Bridge/ReactApplicationContext.cs deleted file mode 100644 index be30cf4a7ed..00000000000 --- a/ReactWindows/ReactNative/Bridge/ReactApplicationContext.cs +++ /dev/null @@ -1,13 +0,0 @@ - -namespace ReactNative.Bridge -{ - /// - /// This class serves as a base class on top of . - /// - public class ReactApplicationContext : ReactContext - { - public ReactApplicationContext() - { - } - } -} diff --git a/ReactWindows/ReactNative/Bridge/ReactContext.cs b/ReactWindows/ReactNative/Bridge/ReactContext.cs index 738825d7985..39c40007d79 100644 --- a/ReactWindows/ReactNative/Bridge/ReactContext.cs +++ b/ReactWindows/ReactNative/Bridge/ReactContext.cs @@ -9,7 +9,7 @@ namespace ReactNative.Bridge /// Abstract context wrapper for the catalyst instance to manage /// lifecycle events. /// - public abstract class ReactContext + public class ReactContext { private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); private readonly List _lifecycleEventListeners = @@ -162,7 +162,7 @@ public void OnResume() /// /// Called by the host when the application shuts down. /// - public void OnShutdown() + public void OnDestroy() { DispatcherHelpers.AssertOnDispatcher(); @@ -180,7 +180,13 @@ public void OnShutdown() foreach (var listener in clone) { - listener.OnShutdown(); + listener.OnDestroy(); + } + + var catalystInstance = _catalystInstance; + if (catalystInstance != null) + { + catalystInstance.Dispose(); } } diff --git a/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs b/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs index 8c70c2bc6b1..ae32f2b9116 100644 --- a/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs +++ b/ReactWindows/ReactNative/Bridge/ReactContextNativeModuleBase.cs @@ -12,7 +12,7 @@ public abstract class ReactContextNativeModuleBase : NativeModuleBase /// Instantiates the . /// /// The React context. - protected ReactContextNativeModuleBase(ReactApplicationContext reactContext) + protected ReactContextNativeModuleBase(ReactContext reactContext) { if (reactContext == null) throw new ArgumentNullException(nameof(reactContext)); @@ -23,6 +23,6 @@ protected ReactContextNativeModuleBase(ReactApplicationContext reactContext) /// /// The React context. /// - public ReactApplicationContext Context { get; } + public ReactContext Context { get; } } } diff --git a/ReactWindows/ReactNative/CoreModulesPackage.cs b/ReactWindows/ReactNative/CoreModulesPackage.cs index 40b720f582c..cc7ee34b8a5 100644 --- a/ReactWindows/ReactNative/CoreModulesPackage.cs +++ b/ReactWindows/ReactNative/CoreModulesPackage.cs @@ -23,12 +23,12 @@ namespace ReactNative class CoreModulesPackage : IReactPackage { private readonly IReactInstanceManager _reactInstanceManager; - private readonly IDefaultHardwareBackButtonHandler _hardwareBackButtonHandler; + private readonly Action _hardwareBackButtonHandler; private readonly UIImplementationProvider _uiImplementationProvider; public CoreModulesPackage( IReactInstanceManager reactInstanceManager, - IDefaultHardwareBackButtonHandler hardwareBackButtonHandler, + Action hardwareBackButtonHandler, UIImplementationProvider uiImplementationProvider) { _reactInstanceManager = reactInstanceManager; @@ -36,7 +36,7 @@ public CoreModulesPackage( _uiImplementationProvider = uiImplementationProvider; } - public IReadOnlyList CreateNativeModules(ReactApplicationContext reactContext) + public IReadOnlyList CreateNativeModules(ReactContext reactContext) { var uiManagerModule = default(INativeModule); using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createUIManagerModule")) @@ -71,7 +71,7 @@ public IReadOnlyList CreateJavaScriptModulesConfig() } public IReadOnlyList CreateViewManagers( - ReactApplicationContext reactContext) + ReactContext reactContext) { return new List(0); } diff --git a/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs b/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs index 9d8beecae65..9cd1060f023 100644 --- a/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs +++ b/ReactWindows/ReactNative/Hosting/Bridge/ChakraJavaScriptExecutor.cs @@ -14,24 +14,16 @@ namespace ReactNative.Hosting.Bridge /// public class ChakraJavaScriptExecutor : IJavaScriptExecutor { - private JavaScriptRuntime _runtime; - private JavaScriptValue _globalObject; + private readonly JavaScriptRuntime _runtime; + private readonly JavaScriptValue _globalObject; private JavaScriptSourceContext _sourceContext = JavaScriptSourceContext.None; /// - /// Initializes the JavaScript runtime. + /// Instantiates the . /// - /// - /// Must be called from the JavaScript thread. - /// - public void Initialize() + public ChakraJavaScriptExecutor() { - if (_runtime.IsValid) - { - throw new InvalidOperationException("JavaScript runtime already initialized for this thread."); - } - _runtime = JavaScriptRuntime.Create(); InitializeChakra(); _globalObject = JavaScriptValue.GlobalObject; diff --git a/ReactWindows/ReactNative/IReactInstanceManager.cs b/ReactWindows/ReactNative/IReactInstanceManager.cs index 31dcd595b22..3e90c2cdce4 100644 --- a/ReactWindows/ReactNative/IReactInstanceManager.cs +++ b/ReactWindows/ReactNative/IReactInstanceManager.cs @@ -11,58 +11,114 @@ namespace ReactNative { /// - /// This interface manages instances of . It expose a way to configure - /// catalyst instance using and keeps track of the lifecycle of that - /// instance.It also sets up connection between the instance and developers support functionality - /// of the framework. + /// This interface manages instances of . + /// It exposes a way to configure catalyst instances using + /// and keeps track of the lifecycle of that + /// instance. It also sets up a connection between the instance and the + /// developer support functionality of the framework. /// - /// An instance of this manager is required to start JS application in - /// #startReactApplication for more info). + /// An instance of this manager is required to start the JavaScript + /// application in + /// (). /// - /// The lifecycle of the instance of should be bound to the activity - /// that owns the that is used to render react application using this - /// instance manager . It's required to pass - /// owning activity's lifecycle events to the instance manager #onPause, #onDestroy, #onResume - /// TODO: - /// 1.Add lifecycle event hooks - /// 2.Add background mode + /// The lifecycle of the instance of + /// should be bound to the application that owns the + /// that is used to render the react + /// application using this instance manager. It is required to pass + /// lifecycle events to the instance manager (i.e., , + /// , and ). /// - public interface IReactInstanceManager : IDisposable + public interface IReactInstanceManager { - IReadOnlyList CreateAllViewManagers(ReactApplicationContext catalystApplicationContext); + /// + /// Event triggered when a react context has been initialized. + /// + event EventHandler ReactContextInitialized; + + /// + /// Signals whether has + /// been called. Will return false after + /// until a new initial context has been created. + /// + bool HasStartedCreatingInitialContext { get; } + + /// + /// The URL where the last bundle was loaded from. + /// + string SourceUrl { get; } + + /// + /// The current react context. + /// + ReactContext CurrentReactContext { get; } /// - /// Trigger react context initialization asynchronously in a background async task. + /// Trigger the react context initialization asynchronously in a + /// background task. This enables applications to pre-load the + /// application JavaScript, and execute global core code before the + /// is available and measure. This should + /// only be called the first time the application is set up, which is + /// enforced to keep developers from accidentally creating their + /// applications multiple times. /// - //public abstract void createReactContextInBackground(); + void CreateReactContextInBackground(); /// - /// return whether createReactContextInBackground has been called + /// Method that gives JavaScript the opportunity to consume the back + /// button event. If JavaScript does not consume the event, the + /// default back press action will be invoked at the end of the + ///roundtrip to JavaScript. /// - /// - //public abstract bool hasStartedCreatingInitialContext(); + void OnBackPressed(); - //public abstract void onBackPressed(); - //public abstract void onPause(); + /// + /// Invoked when the application is suspended. + /// + void OnSuspend(); - //public abstract void onResume(DefaultHardwareBackBtnHandler defaultBackButtonImpl); + /// + /// Used when the application resumes to reset the back button handling + /// in JavaScript. + /// + /// + /// The action to take when back is pressed. + /// + void OnResume(Action onBackPressed); + + /// + /// Destroy the . + /// + void OnDestroy(); /// - /// Attach given {@param rootView} to a catalyst instance manager and start JS application + /// Attach given to a catalyst instance + /// manager and start the JavaScript application using the JavaScript + /// module provided by the . If + /// the react context is currently being (re-)created, or if the react + /// context has not been created yet, the JavaScript application + /// associated with the provided root view will be started + /// asynchronously. This view will then be tracked by this manager and + /// in case of catalyst instance restart, it will be re-attached. /// - /// The root view of the ReactJS app + /// The root view. void AttachMeasuredRootView(ReactRootView rootView); /// - /// Detach given rootView from current catalyst instance. + /// Detach given from the current catalyst + /// instance. This method is idempotent and can be called multiple + /// times on the same instance. /// - /// The root view of the ReactJS app + /// The root view. void DetachRootView(ReactRootView rootView); /// - /// Loads the based on the user configured bundle instances to create + /// all s. /// - Task RecreateReactContextInBackgroundFromBundleFileAsync(); - + /// + /// The application context. + /// + /// The list of view managers. + IReadOnlyList CreateAllViewManagers(ReactContext reactContext); } } diff --git a/ReactWindows/ReactNative/IReactPackage.cs b/ReactWindows/ReactNative/IReactPackage.cs index 2ed4b0bbe96..bf43ad34aee 100644 --- a/ReactWindows/ReactNative/IReactPackage.cs +++ b/ReactWindows/ReactNative/IReactPackage.cs @@ -30,7 +30,7 @@ public interface IReactPackage /// /// The react application context. /// The list of native modules. - IReadOnlyList CreateNativeModules(ReactApplicationContext reactContext); + IReadOnlyList CreateNativeModules(ReactContext reactContext); /// /// Creates the list of JavaScript modules to register with the @@ -51,6 +51,6 @@ public interface IReactPackage /// /// The react application context. /// The list of view managers. - IReadOnlyList CreateViewManagers(ReactApplicationContext reactContext); + IReadOnlyList CreateViewManagers(ReactContext reactContext); } } diff --git a/ReactWindows/ReactNative/LifecycleState.cs b/ReactWindows/ReactNative/LifecycleState.cs index bb3816add09..8dc003c3a8e 100644 --- a/ReactWindows/ReactNative/LifecycleState.cs +++ b/ReactWindows/ReactNative/LifecycleState.cs @@ -1,11 +1,11 @@ namespace ReactNative { /// - /// A enumeration to signify the current lifecycle state for a . + /// A enumeration to signify the current lifecycle state for a . /// public enum LifecycleState { - BEFORE_RESUME, - RESUMED, + BeforeResume, + Resumed, } } diff --git a/ReactWindows/ReactNative/Modules/Core/DeviceEventManagerModule.cs b/ReactWindows/ReactNative/Modules/Core/DeviceEventManagerModule.cs index 9e13fe7cfe3..0773b31a997 100644 --- a/ReactWindows/ReactNative/Modules/Core/DeviceEventManagerModule.cs +++ b/ReactWindows/ReactNative/Modules/Core/DeviceEventManagerModule.cs @@ -14,16 +14,18 @@ public class DeviceEventManagerModule : ReactContextNativeModuleBase /// Instantiates the . /// /// The react context. - /// The back button handler. + /// + /// The action to take when back is pressed. + /// public DeviceEventManagerModule( - ReactApplicationContext reactContext, - IDefaultHardwareBackButtonHandler backButtonHandler) + ReactContext reactContext, + Action onBackPressed) : base(reactContext) { _invokeDefaultBackPressAction = () => { DispatcherHelpers.AssertOnDispatcher(); - backButtonHandler.InvokeDefaultOnBackPressed(); + onBackPressed(); }; } diff --git a/ReactWindows/ReactNative/Modules/Core/IDefaultHardwareBackButtonHandler.cs b/ReactWindows/ReactNative/Modules/Core/IDefaultHardwareBackButtonHandler.cs deleted file mode 100644 index eceba259653..00000000000 --- a/ReactWindows/ReactNative/Modules/Core/IDefaultHardwareBackButtonHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ReactNative.Modules.Core -{ - /// - /// Interface used by to delegate - /// hardware back button events. It is suppose to provide a default - /// behavior since it would be triggered in the case when the JavaScript - /// side does not want to handle back press events. - /// - public interface IDefaultHardwareBackButtonHandler - { - /// - /// By default, all back press calls should not execute the default - /// backpress handler and should instead propagate it to the JavaScript - /// instance. If JavaScript doesn't want to handle the back press - /// itself, it shall call back into native to invoke this function, - /// which should execute the default handler. - /// - void InvokeDefaultOnBackPressed(); - } -} diff --git a/ReactWindows/ReactNative/Modules/Core/Timing.cs b/ReactWindows/ReactNative/Modules/Core/Timing.cs index 339152c82a1..d8b31b13f08 100644 --- a/ReactWindows/ReactNative/Modules/Core/Timing.cs +++ b/ReactWindows/ReactNative/Modules/Core/Timing.cs @@ -8,7 +8,7 @@ namespace ReactNative.Modules.Core /// public class Timing : ReactContextNativeModuleBase { - public Timing(ReactApplicationContext reactContext) + public Timing(ReactContext reactContext) : base(reactContext) { } diff --git a/ReactWindows/ReactNative/Modules/Toast/ToastModule.cs b/ReactWindows/ReactNative/Modules/Toast/ToastModule.cs index 0b962575936..0ea184422ac 100644 --- a/ReactWindows/ReactNative/Modules/Toast/ToastModule.cs +++ b/ReactWindows/ReactNative/Modules/Toast/ToastModule.cs @@ -11,7 +11,7 @@ public sealed class ToastModule : ReactContextNativeModuleBase const string DURATION_SHORT_KEY = "SHORT"; const string DURATION_LONG_KEY = "LONG"; - public ToastModule(ReactApplicationContext reactContext) + public ToastModule(ReactContext reactContext) : base(reactContext) { } diff --git a/ReactWindows/ReactNative/Modules/WebSocket/WebSocketModule.cs b/ReactWindows/ReactNative/Modules/WebSocket/WebSocketModule.cs index 338b1a2adb2..8fc68876ef5 100644 --- a/ReactWindows/ReactNative/Modules/WebSocket/WebSocketModule.cs +++ b/ReactWindows/ReactNative/Modules/WebSocket/WebSocketModule.cs @@ -4,7 +4,7 @@ namespace ReactNative.Modules.WebSocket { class WebSocketModule : ReactContextNativeModuleBase { - public WebSocketModule(ReactApplicationContext reactContext) + public WebSocketModule(ReactContext reactContext) : base(reactContext) { } diff --git a/ReactWindows/ReactNative/ReactContextInitializedEventArgs.cs b/ReactWindows/ReactNative/ReactContextInitializedEventArgs.cs new file mode 100644 index 00000000000..dcb5fc9f9b0 --- /dev/null +++ b/ReactWindows/ReactNative/ReactContextInitializedEventArgs.cs @@ -0,0 +1,26 @@ +using ReactNative.Bridge; +using System; + +namespace ReactNative +{ + /// + /// Event arguments for the + /// event. + /// + public sealed class ReactContextInitializedEventArgs : EventArgs + { + /// + /// Instantiates the . + /// + /// The react context. + internal ReactContextInitializedEventArgs(ReactContext context) + { + Context = context; + } + + /// + /// The react context. + /// + public ReactContext Context { get; } + } +} diff --git a/ReactWindows/ReactNative/ReactInstanceManager.cs b/ReactWindows/ReactNative/ReactInstanceManager.cs new file mode 100644 index 00000000000..015d5dc6270 --- /dev/null +++ b/ReactWindows/ReactNative/ReactInstanceManager.cs @@ -0,0 +1,685 @@ +using ReactNative.Bridge; +using ReactNative.Bridge.Queue; +using ReactNative.Common; +using ReactNative.Hosting.Bridge; +using ReactNative.Modules.Core; +using ReactNative.Tracing; +using ReactNative.UIManager; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +namespace ReactNative +{ + /// + /// This interface manages instances of . + /// It exposes a way to configure catalyst instances using + /// and keeps track of the lifecycle of that + /// instance. It also sets up a connection between the instance and the + /// developer support functionality of the framework. + /// + /// An instance of this manager is required to start the JavaScript + /// application in + /// (). + /// + /// The lifecycle of the instance of + /// should be bound to the application that owns the + /// that is used to render the react + /// application using this instance manager. It is required to pass + /// lifecycle events to the instance manager (i.e., , + /// , and ). + /// + /// TODO: + /// 1.Implement background task functionality and ReactContextInitAsyncTask class hierarchy. + /// 2.Lifecycle managment functoinality. i.e. resume, pause, etc + /// 3.Implement Backbutton handler + /// 4.Implement js bundler load progress checks to ensure thread safety + /// 5.Implement the ViewGroupManager as well as the main ReactViewManager + /// 6.Create DevManager functionality to manage things like exceptions. + /// + public class ReactInstanceManager : IReactInstanceManager + { + private readonly List _attachedRootViews = new List(); + + private readonly string _jsBundleFile; + private readonly IReadOnlyList _packages; + private readonly UIImplementationProvider _uiImplementationProvider; + private readonly Action _nativeModuleCallExceptionHandler; + + private LifecycleState _lifecycleState; + private bool _hasStartedCreatingInitialContext; + private Task _contextInitializationTask; + private Func _pendingJsExecutorFactory; + private JavaScriptBundleLoader _pendingJsBundleLoader; + private string _sourceUrl; + private ReactContext _currentReactContext; + private Action _defaultBackButtonHandler; + + private ReactInstanceManager( + string jsBundleFile, + IReadOnlyList packages, + LifecycleState initialLifecycleState, + UIImplementationProvider uiImplementationProvider, + Action nativeModuleCallExceptionHandler) + { + if (packages == null) + throw new ArgumentNullException(nameof(packages)); + if (uiImplementationProvider == null) + throw new ArgumentNullException(nameof(uiImplementationProvider)); + + _jsBundleFile = jsBundleFile; + _packages = packages; + _lifecycleState = initialLifecycleState; + _uiImplementationProvider = uiImplementationProvider; + _nativeModuleCallExceptionHandler = nativeModuleCallExceptionHandler + ?? /* TODO: use dev support manager */ (_ => { }); + } + + /// + /// Event triggered when a react context has been initialized. + /// + public event EventHandler ReactContextInitialized; + + /// + /// Signals whether has + /// been called. Will return false after + /// until a new initial context has been created. + /// + public bool HasStartedCreatingInitialContext + { + get + { + return _hasStartedCreatingInitialContext; + } + } + + /// + /// The URL where the last bundle was loaded from. + /// + public string SourceUrl + { + get + { + return _sourceUrl; + } + } + + /// + /// Gets the current react context instance. + /// + public ReactContext CurrentReactContext + { + get + { + return _currentReactContext; + } + } + + /// + /// Trigger the react context initialization asynchronously in a + /// background task. This enables applications to pre-load the + /// application JavaScript, and execute global core code before the + /// is available and measure. This should + /// only be called the first time the application is set up, which is + /// enforced to keep developers from accidentally creating their + /// applications multiple times. + /// + public void CreateReactContextInBackground() + { + if (_hasStartedCreatingInitialContext) + { + throw new InvalidOperationException( + "React context creation should only be called when creating the react " + + "application for the first time. When reloading JavaScript, e.g., from " + + "a new file, explicitly, use the re-create method."); + } + + _hasStartedCreatingInitialContext = true; + RecreateReactContextInBackgroundInner(); + } + + /// + /// Recreate the react application and context. This should be called + /// if configuration has changed or the developer has requested the + /// applicatio + /// + public void RecreateReactContextInBackground() + { + if (!_hasStartedCreatingInitialContext) + { + throw new InvalidOperationException( + "React context re-creation should only be called after the initial " + + "create context background call."); + } + + RecreateReactContextInBackgroundInner(); + } + + /// + /// Method that gives JavaScript the opportunity to consume the back + /// button event. If JavaScript does not consume the event, the + /// default back press action will be invoked at the end of the + /// roundtrip to JavaScript. + /// + public void OnBackPressed() + { + DispatcherHelpers.AssertOnDispatcher(); + var reactContext = _currentReactContext; + if (reactContext == null) + { + Tracer.Write(ReactConstants.Tag, "Instance detached from instance manager."); + InvokeDefaultOnBackPressed(); + } + else + { + reactContext.GetNativeModule().EmitHardwareBackPressed(); + } + } + + /// + /// Called when the application is suspended. + /// + public void OnSuspend() + { + DispatcherHelpers.AssertOnDispatcher(); + + _lifecycleState = LifecycleState.BeforeResume; + _defaultBackButtonHandler = null; + + // TODO: dev support manager settings + + var currentReactContext = _currentReactContext; + if (currentReactContext != null) + { + _currentReactContext.OnSuspend(); + } + } + + /// + /// Used when the application resumes to reset the back button handling + /// in JavaScript. + /// + /// + /// The action to take when back is pressed. + /// + public void OnResume(Action onBackPressed) + { + if (onBackPressed == null) + throw new ArgumentNullException(nameof(onBackPressed)); + + DispatcherHelpers.AssertOnDispatcher(); + + _lifecycleState = LifecycleState.Resumed; + + _defaultBackButtonHandler = onBackPressed; + + // TODO: dev support manager notifications + + var currentReactContext = _currentReactContext; + if (currentReactContext != null) + { + currentReactContext.OnResume(); + } + } + + /// + /// Destroy the . + /// + public void OnDestroy() + { + DispatcherHelpers.AssertOnDispatcher(); + + // TODO: dev support manager and memory pressure hooks + + var currentReactContext = _currentReactContext; + if (currentReactContext != null) + { + currentReactContext.OnDestroy(); + _currentReactContext = null; + _hasStartedCreatingInitialContext = false; + } + } + + /// + /// Attach given to a catalyst instance + /// manager and start the JavaScript application using the JavaScript + /// module provided by the . If + /// the react context is currently being (re-)created, or if the react + /// context has not been created yet, the JavaScript application + /// associated with the provided root view will be started + /// asynchronously. This view will then be tracked by this manager and + /// in case of catalyst instance restart, it will be re-attached. + /// + /// The root view. + public void AttachMeasuredRootView(ReactRootView rootView) + { + if (rootView == null) + throw new ArgumentNullException(nameof(rootView)); + + DispatcherHelpers.AssertOnDispatcher(); + + _attachedRootViews.Add(rootView); + + // If the react context is being created in the background, the + // JavaScript application will be started automatically when + // creation completes, as root view is part of the attached root + // view list. + var currentReactContext = _currentReactContext; + if (_contextInitializationTask == null && currentReactContext != null) + { + AttachMeasuredRootViewToInstance(rootView, currentReactContext.CatalystInstance); + } + } + + /// + /// Detach given from the current catalyst + /// instance. This method is idempotent and can be called multiple + /// times on the same instance. + /// + /// The root view. + public void DetachRootView(ReactRootView rootView) + { + if (rootView == null) + throw new ArgumentNullException(nameof(rootView)); + + DispatcherHelpers.AssertOnDispatcher(); + + if (_attachedRootViews.Remove(rootView)) + { + var currentReactContext = _currentReactContext; + if (currentReactContext != null && currentReactContext.HasActiveCatalystInstance) + { + DetachViewFromInstance(rootView, currentReactContext.CatalystInstance); + } + } + } + + /// + /// Uses the configured instances to create + /// all s. + /// + /// + /// The application context. + /// + /// The list of view managers. + public IReadOnlyList CreateAllViewManagers(ReactContext reactContext) + { + if (reactContext == null) + throw new ArgumentNullException(nameof(reactContext)); + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAllViewManagers")) + { + var allViewManagers = new List(); + foreach (var package in _packages) + { + allViewManagers.AddRange( + package.CreateViewManagers(reactContext)); + } + + return allViewManagers; + } + } + + private void RecreateReactContextInBackgroundInner() + { + DispatcherHelpers.AssertOnDispatcher(); + // TODO: handle developer support loading. + RecreateReactContextInBackgroundFromBundleFile(); + } + + private void RecreateReactContextInBackgroundFromBundleFile() + { + RecreateReactContextInBackground( + () => new ChakraJavaScriptExecutor(), + JavaScriptBundleLoader.CreateFileLoader(_jsBundleFile)); + } + + private void InvokeDefaultOnBackPressed() + { + DispatcherHelpers.AssertOnDispatcher(); + + var defaultBackButtonHandler = _defaultBackButtonHandler; + if (defaultBackButtonHandler != null) + { + defaultBackButtonHandler(); + } + } + + private void RecreateReactContextInBackground( + Func jsExecutorFactory, + JavaScriptBundleLoader jsBundleLoader) + { + if (_contextInitializationTask == null) + { + _contextInitializationTask = InitializeReactContextAsync(jsExecutorFactory, jsBundleLoader); + } + else + { + _pendingJsExecutorFactory = jsExecutorFactory; + _pendingJsBundleLoader = jsBundleLoader; + } + } + private async Task InitializeReactContextAsync( + Func jsExecutorFactory, + JavaScriptBundleLoader jsBundleLoader) + { + var currentReactContext = _currentReactContext; + if (currentReactContext != null) + { + TearDownReactContext(currentReactContext); + _currentReactContext = null; + } + + try + { + var reactContext = await CreateReactContextAsync(jsExecutorFactory, jsBundleLoader); + SetupReactContext(reactContext); + } + catch + { + // TODO: add exception handler through dev support manager. + } + finally + { + _contextInitializationTask = null; + } + + if (_pendingJsExecutorFactory != null) + { + var pendingJsExecutorFactory = _pendingJsExecutorFactory; + var pendingJsBundleLoader = _pendingJsBundleLoader; + + _pendingJsExecutorFactory = null; + _pendingJsBundleLoader = null; + + RecreateReactContextInBackground( + pendingJsExecutorFactory, + pendingJsBundleLoader); + } + } + + private void SetupReactContext(ReactContext reactContext) + { + DispatcherHelpers.AssertOnDispatcher(); + if (_currentReactContext != null) + { + throw new InvalidOperationException( + "React context has already been setup and has not been destroyed."); + } + + _currentReactContext = reactContext; + var catalystInstance = reactContext.CatalystInstance; + + catalystInstance.Initialize(); + + // TODO: set up dev support and memory pressure hooks + + MoveReactContextToCurrentLifecycleState(reactContext); + + foreach (var rootView in _attachedRootViews) + { + AttachMeasuredRootViewToInstance(rootView, catalystInstance); + } + + OnReactContextInitialized(reactContext); + } + + private void AttachMeasuredRootViewToInstance( + ReactRootView rootView, + ICatalystInstance catalystInstance) + { + DispatcherHelpers.AssertOnDispatcher(); + + // Reset view content as it's going to be populated by the + // application content from JavaScript + rootView.Children.Clear(); + // TODO: reset root view tag? + + var uiManagerModule = catalystInstance.GetNativeModule(); + var rootTag = uiManagerModule.AddMeasuredRootView(rootView); + + var jsAppModuleName = rootView.JavaScriptModuleName; + var appParameters = new Dictionary + { + { "rootTag", rootTag }, + { "initalProps", null /* TODO: add launch options to root view */ } + }; + + catalystInstance.GetJavaScriptModule().runApplication(jsAppModuleName, appParameters); + } + + private void DetachViewFromInstance(ReactRootView rootView, ICatalystInstance catalystInstance) + { + DispatcherHelpers.AssertOnDispatcher(); + catalystInstance.GetJavaScriptModule().unmountApplicationComponentAtRootTag(rootView.GetTag()); + } + + private void TearDownReactContext(ReactContext reactContext) + { + DispatcherHelpers.AssertOnDispatcher(); + + if (_lifecycleState == LifecycleState.Resumed) + { + reactContext.OnSuspend(); + } + + foreach (var rootView in _attachedRootViews) + { + DetachViewFromInstance(rootView, reactContext.CatalystInstance); + } + + reactContext.OnDestroy(); + // TODO: add dev manager and memory pressure hooks + } + + private async Task CreateReactContextAsync( + Func jsExecutorFactory, + JavaScriptBundleLoader jsBundleLoader) + { + Tracer.Write(ReactConstants.Tag, "Creating react context."); + + _sourceUrl = jsBundleLoader.SourceUrl; + + var nativeRegistryBuilder = new NativeModuleRegistry.Builder(); + var jsModulesBuilder = new JavaScriptModulesConfig.Builder(); + + var reactContext = new ReactContext(); + + // TODO: set dev support manager on the context. + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCoreModulesPackage")) + { + var coreModulesPackage = + new CoreModulesPackage(this, OnBackPressed, _uiImplementationProvider); + + ProcessPackage(coreModulesPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } + + foreach (var reactPackage in _packages) + { + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCustomReactPackage")) + { + ProcessPackage(reactPackage, reactContext, nativeRegistryBuilder, jsModulesBuilder); + } + } + + var nativeModuleRegistry = default(NativeModuleRegistry); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "buildNativeModuleRegistry")) + { + nativeModuleRegistry = nativeRegistryBuilder.Build(); + } + + var javaScriptModulesConfig = default(JavaScriptModulesConfig); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "buildJSModuleConfig")) + { + javaScriptModulesConfig = jsModulesBuilder.Build(); + } + + var catalystInstanceBuilder = new CatalystInstance.Builder + { + QueueConfigurationSpec = CatalystQueueConfigurationSpec.Default, + JavaScriptExecutorFactory = jsExecutorFactory, + Registry = nativeModuleRegistry, + JavaScriptModulesConfig = javaScriptModulesConfig, + BundleLoader = jsBundleLoader, + NativeModuleCallExceptionHandler = _nativeModuleCallExceptionHandler, + }; + + var catalystInstance = default(CatalystInstance); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createCatalystInstance")) + { + catalystInstance = catalystInstanceBuilder.Build(); + } + + // TODO: add bridge idle debug listener + + reactContext.InitializeWithInstance(catalystInstance); + + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "RunJavaScriptBundle")) + { + await catalystInstance.InitializeBridgeAsync(); + } + + return reactContext; + } + + private void ProcessPackage( + IReactPackage reactPackage, + ReactContext reactContext, + NativeModuleRegistry.Builder nativeRegistryBuilder, + JavaScriptModulesConfig.Builder jsModulesBuilder) + { + foreach (var nativeModule in reactPackage.CreateNativeModules(reactContext)) + { + nativeRegistryBuilder.Add(nativeModule); + } + + foreach (var type in reactPackage.CreateJavaScriptModulesConfig()) + { + if (JavaScriptModulesConfig.Builder.ValidJavaScriptModuleType(type)) + { + jsModulesBuilder.Add(type); + } + } + } + + private void MoveReactContextToCurrentLifecycleState(ReactContext reactContext) + { + if (_lifecycleState == LifecycleState.Resumed) + { + reactContext.OnResume(); + } + } + + private void OnReactContextInitialized(ReactContext reactContext) + { + var reactContextInitialized = ReactContextInitialized; + if (reactContextInitialized != null) + { + reactContextInitialized(this, new ReactContextInitializedEventArgs(reactContext)); + } + } + + /// + /// A Builder responsible for creating a React Instance Manager. + /// + public sealed class Builder + { + private readonly List _packages = new List(); + + private string _jsBundleFile; + private LifecycleState? _initialLifecycleState; + private UIImplementationProvider _uiImplementationProvider; + private Action _nativeModuleCallExceptionHandler; + + /// + /// A provider of . + /// + public UIImplementationProvider UIImplementationProvider + { + set + { + _uiImplementationProvider = value; + } + } + + /// + /// Path to the JavaScript bundle file to be loaded from the file + /// system. + /// + public string JavaScriptBundleFile + { + set + { + _jsBundleFile = value; + } + } + + /// + /// The mutable list of react packages. + /// + public IList Packages + { + get + { + return _packages; + } + } + + /// + /// The initial lifecycle state of the host. + /// + public LifecycleState InitialLifecycleState + { + set + { + _initialLifecycleState = value; + } + } + + /// + /// The exception handler for all native module calls. + /// + public Action NativeModuleCallExceptionHandler + { + set + { + _nativeModuleCallExceptionHandler = value; + } + } + + /// + /// Instantiates a new . + /// + /// A react instance manager. + public ReactInstanceManager Build() + { + AssertNotNull(_jsBundleFile, nameof(JavaScriptBundleFile)); + AssertNotNull(_initialLifecycleState, nameof(InitialLifecycleState)); + + if (_uiImplementationProvider == null) + { + _uiImplementationProvider = new UIImplementationProvider(); + } + + return new ReactInstanceManager( + _jsBundleFile, + _packages, + _initialLifecycleState.Value, + _uiImplementationProvider, + _nativeModuleCallExceptionHandler); + } + + private void AssertNotNull(object value, string name) + { + if (value == null) + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "{0} has not been set.", + name)); + } + } + } +} diff --git a/ReactWindows/ReactNative/ReactInstanceManagerImpl.cs b/ReactWindows/ReactNative/ReactInstanceManagerImpl.cs deleted file mode 100644 index e648cf72516..00000000000 --- a/ReactWindows/ReactNative/ReactInstanceManagerImpl.cs +++ /dev/null @@ -1,398 +0,0 @@ -using ReactNative.Bridge; -using ReactNative.Bridge.Queue; -using ReactNative.Common; -using ReactNative.Hosting.Bridge; -using ReactNative.Modules.Core; -using ReactNative.Tracing; -using ReactNative.UIManager; -using ReactNative.Views; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Windows.UI.Xaml; - -namespace ReactNative -{ - /// - /// This class is managing instances of . It expose a way to configure - /// catalyst instance using and keeps track of the lifecycle of that - /// instance. It also sets up connection between the instance and developers support functionality - /// of the framework. - /// - /// An instance of this manager is required to start JS application in (see - /// for more info). - /// - /// TODO: - /// 1.Implement background task functionality and ReactContextInitAsyncTask class hierarchy. - /// 2.Lifecycle managment functoinality. i.e. resume, pause, etc - /// 3.Implement Backbutton handler - /// 4.Implement js bundler load progress checks to ensure thread safety - /// 5.Implement the ViewGroupManager as well as the main ReactViewManager - /// 6.Create DevManager functionality to manage things like exceptions. - /// - public class ReactInstanceManagerImpl : IReactInstanceManager - { - private readonly List _attachedRootViews = new List(); - private LifecycleState _lifecycleState; - private readonly string _jsBundleFile; - private readonly List _packages; - private ReactContext _reactContext; - private readonly string _jsMainModuleName; - private readonly UIImplementationProvider _uiImplementationProvider; - private readonly IDefaultHardwareBackButtonHandler _defaultHardwareBackButtonHandler; - - public ReactInstanceManagerImpl( - string jsMainModuleName, - List packages, - LifecycleState initialLifecycleState, - UIImplementationProvider uiImplementationProvider, - string jsBundleFile) - { - _jsBundleFile = jsBundleFile; - _jsMainModuleName = jsMainModuleName; - _packages = packages; - _reactContext = new ReactApplicationContext(); - _lifecycleState = initialLifecycleState; - _uiImplementationProvider = uiImplementationProvider; - _defaultHardwareBackButtonHandler = new DefaultHardwareBackButtonHandlerImpl(this); - } - - /// - /// Uses configured instances to create all view managers. - /// - /// react application instance - /// - public IReadOnlyList CreateAllViewManagers(ReactApplicationContext catalystApplicationContext) - { - var allViewManagers = new List(); - _packages.ForEach(reactPackage => - allViewManagers.AddRange( - reactPackage.CreateViewManagers(catalystApplicationContext))); - return allViewManagers; - } - - /// - /// Loads the based on the user configured bundle. - /// - public async Task RecreateReactContextInBackgroundFromBundleFileAsync() - { - var jsExecutor = new ChakraJavaScriptExecutor(); - var jsBundler = JavaScriptBundleLoader.CreateFileLoader(_jsBundleFile); - - try - { - _reactContext = await CreateReactContextAsync(jsExecutor, jsBundler); - } - catch (Exception ex) - { - throw ex; - } - - return _reactContext; - } - - /// - /// Creates the react context instance which is based off the . - /// - /// The Javascript Executor instance - /// The Javascript bundle loader responsible for loading the assembly - /// An async task containing the created react context - private async Task CreateReactContextAsync(IJavaScriptExecutor jsExecutor, JavaScriptBundleLoader jsBundleLoader) - { - var reactContext = new ReactApplicationContext(); - var nativeRegistryBuilder = new NativeModuleRegistry.Builder(); - var jsModulesBuilder = new JavaScriptModulesConfig.Builder(); - - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCoreModulesPackage")) - { - var coreModulesPackage = new CoreModulesPackage( - this, - _defaultHardwareBackButtonHandler, - _uiImplementationProvider); - - ProcessPackage( - coreModulesPackage, - reactContext, - nativeRegistryBuilder, - jsModulesBuilder); - } - - foreach (var reactPackage in _packages) - { - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "createAndProcessCustomReactPackage")) - { - ProcessPackage( - reactPackage, - reactContext, - nativeRegistryBuilder, - jsModulesBuilder); - } - } - - var nativeModuleRegistry = default(NativeModuleRegistry); - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "buildNativeModuleRegistry")) - { - nativeModuleRegistry = nativeRegistryBuilder.Build(); - } - - var javaScriptModulesConfig = default(JavaScriptModulesConfig); - using (Tracer.Trace(Tracer.TRACE_TAG_REACT_BRIDGE, "buildJSModuleConfig")) - { - javaScriptModulesConfig = jsModulesBuilder.Build(); - } - - var exceptionHandler = new Action(ex => - { - Tracer.Write(ReactConstants.Tag, String.Format("Exception Occured {0}", ex.Message)); - }); - - var javascriptRuntime = new CatalystInstance.Builder - { - QueueConfigurationSpec = CatalystQueueConfigurationSpec.Default, - JavaScriptExecutor = jsExecutor, - Registry = nativeModuleRegistry, - JavaScriptModulesConfig = javaScriptModulesConfig, - BundleLoader = jsBundleLoader, - NativeModuleCallExceptionHandler = exceptionHandler - }.Build(); - - reactContext.InitializeWithInstance(javascriptRuntime); - - await javascriptRuntime.InitializeBridgeAsync(); - - return reactContext; - } - - private void ProcessPackage( - IReactPackage reactPackage, - ReactApplicationContext reactContext, - NativeModuleRegistry.Builder nativeRegistryBuilder, - JavaScriptModulesConfig.Builder jsModulesBuilder) - { - foreach (var nativeModule in reactPackage.CreateNativeModules(reactContext)) - { - nativeRegistryBuilder.Add(nativeModule); - } - - foreach (var type in reactPackage.CreateJavaScriptModulesConfig()) - { - if (JavaScriptModulesConfig.Builder.ValidJavaScriptModuleType(type)) - { - jsModulesBuilder.Add(type); - } - } - } - - /// - /// Attaches the to the list of tracked root views. - /// - /// The root view for the ReactJS app - public void AttachMeasuredRootView(ReactRootView rootView) - { - _attachedRootViews.Add(rootView); - - if (_reactContext != null) - { - AttachMeasuredRootViewToInstance(rootView, _reactContext.CatalystInstance); - } - } - - /// - /// Detach given from current catalyst instance. - /// - /// The root view for the ReactJS app - public void DetachRootView(ReactRootView rootView) - { - if (_attachedRootViews.RemoveAll(view => view.TagId == rootView.TagId) > -1) - { - if (_reactContext != null) - { - DetachViewFromInstance(rootView, _reactContext.CatalystInstance); - } - } - } - - private void DetachViewFromInstance(ReactRootView rootView, ICatalystInstance catalystInstance) - { - try - { - catalystInstance.GetJavaScriptModule()?.unmountApplicationComponentAtRootTag(rootView.TagId); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Unable to load AppRegistry JS module. Error message: " + ex.Message, ex); - } - } - - private void AttachMeasuredRootViewToInstance(ReactRootView rootView, ICatalystInstance catalystInstance) - { - DispatcherHelpers.AssertOnDispatcher(); - - // Reset view content as it's going to be populated by the - // application content from JavaScript - var uiManagerModule = catalystInstance.GetNativeModule(); - - var rootTag = uiManagerModule.AddMeasuredRootView(rootView); - var initialProps = new Dictionary(); - initialProps.Add("rootTag", rootTag); - - try - { - catalystInstance.GetJavaScriptModule()?.runApplication(rootView.JSModuleName, initialProps); - } - catch (InvalidOperationException ex) - { - throw new InvalidOperationException("Unable to load AppRegistry JS module. Error message: " + ex.Message, ex); - } - } - - private void InvokeDefaultOnBackPressed() - { - DispatcherHelpers.AssertOnDispatcher(); - // TODO: implement - } - - class DefaultHardwareBackButtonHandlerImpl : IDefaultHardwareBackButtonHandler - { - private readonly ReactInstanceManagerImpl _parent; - - public DefaultHardwareBackButtonHandlerImpl(ReactInstanceManagerImpl parent) - { - _parent = parent; - } - - public void InvokeDefaultOnBackPressed() - { - _parent.InvokeDefaultOnBackPressed(); - } - } - - /// - /// A Builder responsible for creating a React Instance Manager. - /// - public sealed class Builder - { - private readonly List _reactPackages = new List(); - private LifecycleState _LifecycleState; - private UIImplementationProvider _UIImplementationProvider; - private string _jsBundleFile; - private string _jsMainModuleName; - - /// - /// Sets a provider of . - /// - public UIImplementationProvider UIImplementationProvider - { - set - { - _UIImplementationProvider = value; - } - } - - /// - /// Path to the JS bundle file to be loaded from the file system. - /// - public string JSBundleFile - { - set - { - _jsBundleFile = value; - } - } - - /// - /// Path to your app's main module on the packager server. This is used when - /// reloading JS during development. All paths are relative to the root folder - /// the packager is serving files from. - /// - public string JSMainModuleName - { - set - { - _jsMainModuleName = value; - } - } - - /// - /// Adds a specified to the package list. - /// - /// Client requested package to load. - /// - public Builder AddPackage(IReactPackage reactPackage) - { - _reactPackages.Add(reactPackage); - return this; - } - - /// - /// Concatenates a list of packages to the builder list. - /// - /// Client requested package list to include - /// - public Builder AddPackages(List packages) - { - _reactPackages.AddRange(packages); - return this; - } - - /// - /// Instantiates a new {@link ReactInstanceManagerImpl}. - /// - /// - public LifecycleState InitialLifecycleState - { - set - { - _LifecycleState = value; - } - } - - /// - /// Instantiates a new . - /// Before calling , the following must be called: setApplication then setJSMainModuleName - /// - /// A IReactInstanceManager instance - public IReactInstanceManager Build() - { - AssertNotNull(_LifecycleState, nameof(LifecycleState)); - AssertNotNull(_jsMainModuleName, "string"); - AssertNotNull(_jsBundleFile, "string"); - - if (_UIImplementationProvider == null) - { - // create default UIImplementationProvider if the provided one is null. - _UIImplementationProvider = new UIImplementationProvider(); - } - - return new ReactInstanceManagerImpl(_jsMainModuleName, _reactPackages, - _LifecycleState, _UIImplementationProvider, _jsBundleFile); - } - - private void AssertNotNull(object value, string name) - { - if (value == null) - throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "{0} has not been set.", - name)); - } - } - - /// - /// Dispose the instance of , which entails disposing - /// and detaching all from the instance. - /// - public void Dispose() - { - _reactContext.CatalystInstance.Dispose(); - - foreach (var rootView in _attachedRootViews) - { - this.DetachRootView(rootView); - } - } - } -} diff --git a/ReactWindows/ReactNative/ReactNative.csproj b/ReactWindows/ReactNative/ReactNative.csproj index 5adcade4909..8c81f091ad9 100644 --- a/ReactWindows/ReactNative/ReactNative.csproj +++ b/ReactWindows/ReactNative/ReactNative.csproj @@ -152,8 +152,10 @@ - + + + @@ -165,7 +167,7 @@ - + @@ -190,11 +192,9 @@ - - diff --git a/ReactWindows/ReactNative/ReactPage.cs b/ReactWindows/ReactNative/ReactPage.cs new file mode 100644 index 00000000000..23c2234e58a --- /dev/null +++ b/ReactWindows/ReactNative/ReactPage.cs @@ -0,0 +1,110 @@ +using ReactNative.Modules.Core; +using System; +using System.Collections.Generic; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace ReactNative +{ + /// + /// Base page for React Native applications. + /// + public class ReactPage : Page + { + private readonly IReactInstanceManager _reactInstanceManager; + private readonly string _mainComponentName; + private readonly Action _onBackPressed; + + /// + /// Instantiates the . + /// + /// The JavaScript bundle file. + /// The main component name. + /// The list of react packages. + public ReactPage( + string jsBundleFile, + string mainComponentName, + IReadOnlyList packages, + Action onBackPressed) + { + _mainComponentName = mainComponentName; + _onBackPressed = onBackPressed; + _reactInstanceManager = CreateReactInstanceManager(jsBundleFile, packages); + + RootView = CreateRootView(); + RootView.HorizontalAlignment = HorizontalAlignment.Stretch; + RootView.VerticalAlignment = VerticalAlignment.Stretch; + + Content = RootView; + } + + /// + /// The root view managed by the page. + /// + public ReactRootView RootView + { + get; + } + + /// + /// Called when the application is first initialized. + /// + public void OnCreate() + { + RootView.StartReactApplication(_reactInstanceManager, _mainComponentName); + } + + /// + /// Called before the application is suspended. + /// + public void OnSuspend() + { + _reactInstanceManager.OnSuspend(); + } + + /// + /// Called when the application is resumed. + /// + public void OnResume() + { + _reactInstanceManager.OnResume(_onBackPressed); + } + + /// + /// Called before the application shuts down. + /// + public void OnDestroy() + { + _reactInstanceManager.OnDestroy(); + } + + /// + /// Creates the React root view. + /// + /// The root view. + /// + /// Subclasses may override this method if it needs to use a custom + /// root view. + /// + protected virtual ReactRootView CreateRootView() + { + return new ReactRootView(); + } + + private IReactInstanceManager CreateReactInstanceManager(string jsBundleFile, IReadOnlyList packages) + { + var builder = new ReactInstanceManager.Builder + { + InitialLifecycleState = LifecycleState.Resumed, + JavaScriptBundleFile = jsBundleFile, + }; + + foreach (var package in packages) + { + builder.Packages.Add(package); + } + + return builder.Build(); + } + } +} diff --git a/ReactWindows/ReactNative/ReactRootView.cs b/ReactWindows/ReactNative/ReactRootView.cs index 579b1a1551b..9c4f9b66412 100644 --- a/ReactWindows/ReactNative/ReactRootView.cs +++ b/ReactWindows/ReactNative/ReactRootView.cs @@ -1,113 +1,116 @@ -using ReactNative.Modules.Core; -using ReactNative.Shell; +using ReactNative.Bridge; using ReactNative.UIManager; using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using Windows.UI.Xaml; +using Windows.Foundation; namespace ReactNative { + /// + /// Default root view for applicaitons. Provides the ability to listen for + /// size changes so that the UI manager can re-layout its elements. + /// + /// It is also responsible for handling touch events passed to any of it's + /// child views and sending those events to JavaScript via the + /// module. + /// public class ReactRootView : SizeMonitoringPanel, IRootView { private IReactInstanceManager _reactInstanceManager; private string _jsModuleName; - private int _rootTageNode; - private bool _isAttachedToWindow; - public void OnChildStartedNativeGesture(RoutedEventArgs ev) - { - throw new NotImplementedException("Native gesture event handling is not yet supported"); - } + private bool _wasMeasured; + private bool _attachScheduled; /// - /// Initializes and starts a instance. + /// Gets the JavaScript module name. /// - /// The Javascript Bundle location - /// The core Javascript module name - public void LiftAsync(string bundleAssetName, string jsModuleName) + internal string JavaScriptModuleName { - var defaultPackageList = new List() + get { - new MainReactPackage() - }; - - this.LiftAsync(bundleAssetName, jsModuleName, defaultPackageList); + return _jsModuleName; + } } /// - /// Initializes and starts a i nstance. + /// Schedule rendering of the react component rendered by the + /// JavaScript application from the given JavaScript module + /// using the provided + /// to attach to the JavaScript + /// context of that manager. /// - /// The Javascript Bundle location - /// The core Javascript module name - /// The list of react packges to initialize - public void LiftAsync(string bundleAssetName, string jsModuleName, List packages) + /// + /// The react instance manager. + /// + /// + /// The module name. + /// + public void StartReactApplication(IReactInstanceManager reactInstanceManager, string moduleName) { - var builder = new ReactInstanceManagerImpl.Builder() + DispatcherHelpers.AssertOnDispatcher(); + + if (_reactInstanceManager != null) { - InitialLifecycleState = LifecycleState.RESUMED, - JSMainModuleName = jsModuleName, - JSBundleFile = bundleAssetName + throw new InvalidOperationException("This root view has already been attached to an instance manager."); } - .AddPackages(packages) - .Build(); - this.StartReactApplication(builder, jsModuleName); - } + _reactInstanceManager = reactInstanceManager; + _jsModuleName = moduleName; - /// - /// Exposes the Javascript module name of the root view - /// - public string JSModuleName - { - get + if (!_reactInstanceManager.HasStartedCreatingInitialContext) { - return _jsModuleName; + _reactInstanceManager.CreateReactContextInBackground(); } - } - /// - /// Exposes the react tag id of the view - /// - public int TagId - { - get { return _rootTageNode; } + // We need to wait for the initial `Measure` call, if this view has + // not yet been measured, we set the `_attachScheduled` flag, which + // will enable deferred attachment of the root node. + if (_wasMeasured) + { + _reactInstanceManager.AttachMeasuredRootView(this); + } + else + { + _attachScheduled = true; + } } /// - /// Sets the react tag id to the view + /// Called when a child starts a native gesture. /// - /// - public void BindTagToView(int tagId) + /// The event. + public void OnChildStartedNativeGesture(RoutedEventArgs ev) { - _rootTageNode = tagId; + throw new NotImplementedException(); } /// - /// Schedule rendering of the react component rendered by the JS application from the given JS - /// module using provided + /// Hooks into the measurement event to potentially attach the react + /// root view. /// - /// The React Instance Manager - /// module to load - public async void StartReactApplication(IReactInstanceManager reactInstanceManager, string moduleName) + /// The available size. + /// The desired size. + protected override Size MeasureOverride(Size availableSize) { - _reactInstanceManager = reactInstanceManager; - _jsModuleName = moduleName; + DispatcherHelpers.AssertOnDispatcher(); - await _reactInstanceManager.RecreateReactContextInBackgroundFromBundleFileAsync(); + var result = base.MeasureOverride(availableSize); - // We need to wait for the initial onMeasure, if this view has not yet been measured, we set - // mAttachScheduled flag, which will make this view startReactApplication itself to instance - // manager once onMeasure is called. - if (!_isAttachedToWindow) + _wasMeasured = true; + + var reactInstanceManager = _reactInstanceManager; + if (_attachScheduled && reactInstanceManager != null) { - _reactInstanceManager.AttachMeasuredRootView(this); - _isAttachedToWindow = true; + _attachScheduled = false; + reactInstanceManager.AttachMeasuredRootView(this); } - } - private void OnDetachedFromWindow(object sender, RoutedEventArgs e) - { - _reactInstanceManager.DetachRootView(this); + return result; } } } diff --git a/ReactWindows/ReactNative/Shell/MainReactPackage.cs b/ReactWindows/ReactNative/Shell/MainReactPackage.cs index 7e1a7fa9edc..fec3f38b489 100644 --- a/ReactWindows/ReactNative/Shell/MainReactPackage.cs +++ b/ReactWindows/ReactNative/Shell/MainReactPackage.cs @@ -17,7 +17,7 @@ namespace ReactNative.Shell /// public class MainReactPackage : IReactPackage { - public IReadOnlyList CreateNativeModules(ReactApplicationContext reactContext) + public IReadOnlyList CreateNativeModules(ReactContext reactContext) { return new List { @@ -31,7 +31,7 @@ public IReadOnlyList CreateJavaScriptModulesConfig() } public IReadOnlyList CreateViewManagers( - ReactApplicationContext reactContext) + ReactContext reactContext) { return new List { diff --git a/ReactWindows/ReactNative/UIManager/Events/EventDispatcher.cs b/ReactWindows/ReactNative/UIManager/Events/EventDispatcher.cs index 5e3982a60ca..2732e639e29 100644 --- a/ReactWindows/ReactNative/UIManager/Events/EventDispatcher.cs +++ b/ReactWindows/ReactNative/UIManager/Events/EventDispatcher.cs @@ -79,7 +79,7 @@ public class EventDispatcher : ILifecycleEventListener private readonly IDictionary _eventCookieToLastEventIndex = new Dictionary(); private readonly IDictionary _eventNameToEventId = new Dictionary(); - private readonly ReactApplicationContext _reactContext; + private readonly ReactContext _reactContext; private RCTEventEmitter _rctEventEmitter; private EventDispatcherCallback _currentFrameCallback; @@ -88,7 +88,7 @@ public class EventDispatcher : ILifecycleEventListener /// Instantiates the . /// /// The context. - public EventDispatcher(ReactApplicationContext reactContext) + public EventDispatcher(ReactContext reactContext) { if (reactContext == null) throw new ArgumentNullException(nameof(reactContext)); @@ -146,7 +146,7 @@ public void OnResume() /// /// Called when the host is shutting down. /// - public void OnShutdown() + public void OnDestroy() { ClearCallback(); } diff --git a/ReactWindows/ReactNative/UIManager/IRootView.cs b/ReactWindows/ReactNative/UIManager/IRootView.cs index 4ac484bc098..b5db2b27f84 100644 --- a/ReactWindows/ReactNative/UIManager/IRootView.cs +++ b/ReactWindows/ReactNative/UIManager/IRootView.cs @@ -3,14 +3,14 @@ namespace ReactNative.UIManager { /// - /// Interface for the root native view of a React native application. + /// Interface for the root native view of a react native application. /// public interface IRootView { /// - /// Called when a child starts a native gesture (e.g. a scroll in a ScrollView). + /// Called when a child starts a native gesture. /// - /// - void OnChildStartedNativeGesture(RoutedEventArgs ev); + /// The event. + void OnChildStartedNativeGesture(RoutedEventArgs e); } } diff --git a/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs b/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs index 8f956549f6f..0faf2dcb3d5 100644 --- a/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs +++ b/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs @@ -304,6 +304,12 @@ public void RemoveRootView(int rootViewTag) } var rootView = _tagsToViews[rootViewTag]; + var sizeMonitoringPanel = rootView as SizeMonitoringPanel; + if (sizeMonitoringPanel != null) + { + sizeMonitoringPanel.RemoveSizeChanged(); + } + DropView(rootView); _rootTags.Remove(rootViewTag); } diff --git a/ReactWindows/ReactNative/UIManager/SizeMonitoringPanel.cs b/ReactWindows/ReactNative/UIManager/SizeMonitoringPanel.cs index 417938556ae..d6039a32e33 100644 --- a/ReactWindows/ReactNative/UIManager/SizeMonitoringPanel.cs +++ b/ReactWindows/ReactNative/UIManager/SizeMonitoringPanel.cs @@ -6,18 +6,27 @@ namespace ReactNative.UIManager /// /// allows registering for size change events. The main purpose for this class is to hide complexity of ReactRootView /// - public partial class SizeMonitoringPanel : Canvas + public class SizeMonitoringPanel : Canvas { - private ISizeChangedListener _onSizeChangedListener; + private SizeChangedEventHandler _sizeChangedEventHandler; /// - /// Sets and registers the size change listener for the XAML panel control + /// Sets and registers the event handler responsible for monitoring + /// size change events. /// - /// The listener - public void SetOnSizeChangedListener(ISizeChangedListener sizeChangedListener) + /// The event handler. + public void SetOnSizeChangedListener(SizeChangedEventHandler sizeChangedEventHandler) { - _onSizeChangedListener = sizeChangedListener; - this.SizeChanged += _onSizeChangedListener.OnSizeChanged; + _sizeChangedEventHandler = sizeChangedEventHandler; + SizeChanged += _sizeChangedEventHandler; + } + + /// + /// Unsets the size changed event handler. + /// + public void RemoveSizeChanged() + { + SizeChanged -= _sizeChangedEventHandler; } } } diff --git a/ReactWindows/ReactNative/UIManager/ThemedReactContext.cs b/ReactWindows/ReactNative/UIManager/ThemedReactContext.cs index 9c9b49334e6..f9b05b85408 100644 --- a/ReactWindows/ReactNative/UIManager/ThemedReactContext.cs +++ b/ReactWindows/ReactNative/UIManager/ThemedReactContext.cs @@ -5,21 +5,21 @@ namespace ReactNative.UIManager public class ThemedReactContext : ReactContext { - private readonly ReactApplicationContext mReactApplicationContext; + private readonly ReactContext mReactContext; - public ThemedReactContext(ReactApplicationContext reactApplicationContext) { + public ThemedReactContext(ReactContext reactApplicationContext) { InitializeWithInstance(reactApplicationContext.CatalystInstance); - mReactApplicationContext = reactApplicationContext; + mReactContext = reactApplicationContext; } public void addLifecycleEventListener(ILifecycleEventListener listener) { - mReactApplicationContext.AddLifecycleEventListener(listener); + mReactContext.AddLifecycleEventListener(listener); } public void removeLifecycleEventListener(ILifecycleEventListener listener) { - mReactApplicationContext.RemoveLifecycleEventListener(listener); + mReactContext.RemoveLifecycleEventListener(listener); } } diff --git a/ReactWindows/ReactNative/UIManager/UIImplementation.cs b/ReactWindows/ReactNative/UIManager/UIImplementation.cs index 8509811e0dc..3cd353dff46 100644 --- a/ReactWindows/ReactNative/UIManager/UIImplementation.cs +++ b/ReactWindows/ReactNative/UIManager/UIImplementation.cs @@ -34,12 +34,12 @@ public class UIImplementation /// /// The react context. /// The view managers. - public UIImplementation(ReactApplicationContext reactContext, IReadOnlyList viewManagers) + public UIImplementation(ReactContext reactContext, IReadOnlyList viewManagers) : this(reactContext, new ViewManagerRegistry(viewManagers)) { } - private UIImplementation(ReactApplicationContext reactContext, ViewManagerRegistry viewManagers) + private UIImplementation(ReactContext reactContext, ViewManagerRegistry viewManagers) : this( viewManagers, new UIViewOperationQueue(reactContext, new NativeViewHierarchyManager(viewManagers))) diff --git a/ReactWindows/ReactNative/UIManager/UIImplementationProvider.cs b/ReactWindows/ReactNative/UIManager/UIImplementationProvider.cs index d4db622f1e5..524384d1855 100644 --- a/ReactWindows/ReactNative/UIManager/UIImplementationProvider.cs +++ b/ReactWindows/ReactNative/UIManager/UIImplementationProvider.cs @@ -11,7 +11,7 @@ namespace ReactNative.UIManager public class UIImplementationProvider { public UIImplementation CreateUIImplementation( - ReactApplicationContext reactContext, + ReactContext reactContext, IReadOnlyList viewManagers) { return new UIImplementation(reactContext, viewManagers); diff --git a/ReactWindows/ReactNative/UIManager/UIManagerModule.cs b/ReactWindows/ReactNative/UIManager/UIManagerModule.cs index 6f08173e5bc..6e12ffedf67 100644 --- a/ReactWindows/ReactNative/UIManager/UIManagerModule.cs +++ b/ReactWindows/ReactNative/UIManager/UIManagerModule.cs @@ -35,7 +35,7 @@ public partial class UIManagerModule : ReactContextNativeModuleBase, ILifecycleE /// The view managers. /// The UI implementation. public UIManagerModule( - ReactApplicationContext reactContext, + ReactContext reactContext, IReadOnlyList viewManagers, UIImplementation uiImplementation) : base(reactContext) @@ -102,7 +102,8 @@ public int AddMeasuredRootView(SizeMonitoringPanel rootView) var context = new ThemedReactContext(Context); _uiImplementation.RegisterRootView(rootView, tag, width, height, context); - rootView.SetOnSizeChangedListener(new RootViewSizeChangedListener()); + // TODO: ensure this gets unset + rootView.SetOnSizeChangedListener(OnSizeChanged); return tag; } @@ -369,7 +370,7 @@ public void OnResume() /// /// Called when the host is shutting down. /// - public void OnShutdown() + public void OnDestroy() { _uiImplementation.OnShutdown(); } @@ -409,16 +410,16 @@ public override void OnCatalystInstanceDispose() #endregion - sealed class RootViewSizeChangedListener : ISizeChangedListener + #region SizeChangedEventHandler + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) { - public void OnSizeChanged(object sender, SizeChangedEventArgs e) - { - //TODO: Need to adjust the styling of the panel based on the new - //width and height(e.NewSize). The adjustment needs to run on the - //Native Modules thread off the react context(this.Context.RunOnNativeModulesThread) - throw new NotImplementedException("Size change behavior for root view still needs to be implemented"); - } + //TODO: Need to adjust the styling of the panel based on the new + //width and height(e.NewSize). The adjustment needs to run on the + //Native Modules thread off the react context(this.Context.RunOnNativeModulesThread) + throw new NotImplementedException("Size change behavior for root view still needs to be implemented"); } + #endregion } } diff --git a/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs b/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs index 77ec05e3fb4..b958b07c90a 100644 --- a/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs +++ b/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs @@ -23,7 +23,7 @@ public class UIViewOperationQueue private IList _operations = new List(); private readonly NativeViewHierarchyManager _nativeViewHierarchyManager; - private readonly ReactApplicationContext _reactContext; + private readonly ReactContext _reactContext; /// /// Instantiates the . @@ -32,7 +32,7 @@ public class UIViewOperationQueue /// /// The native view hierarchy manager. /// - public UIViewOperationQueue(ReactApplicationContext reactContext, NativeViewHierarchyManager nativeViewHierarchyManager) + public UIViewOperationQueue(ReactContext reactContext, NativeViewHierarchyManager nativeViewHierarchyManager) { _nativeViewHierarchyManager = nativeViewHierarchyManager; _reactContext = reactContext;