diff --git a/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs b/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs index b531bb86226..57e043c1541 100644 --- a/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs +++ b/ReactWindows/ReactNative.Shared/ReactInstanceManager.cs @@ -422,7 +422,7 @@ public async Task AttachMeasuredRootViewAsync(ReactRootView rootView) rootView.Children.Clear(); ViewExtensions.ClearData(rootView); - await DispatcherHelpers.CallOnDispatcher(() => + await DispatcherHelpers.CallOnDispatcher(async () => { _attachedRootViews.Add(rootView); @@ -433,11 +433,11 @@ public async Task AttachMeasuredRootViewAsync(ReactRootView rootView) var currentReactContext = _currentReactContext; if (currentReactContext != null) { - AttachMeasuredRootViewToInstance(rootView, currentReactContext.ReactInstance); + await AttachMeasuredRootViewToInstanceAsync(rootView, currentReactContext.ReactInstance); } return true; - }, true); // inlining allowed + }, true).Unwrap(); // inlining allowed } /// @@ -591,7 +591,7 @@ private Task CreateReactContextFromCachedPackagerBundleAsync(Cance try { var reactContext = await CreateReactContextCoreAsync(jsExecutorFactory, jsBundleLoader, token); - SetupReactContext(reactContext); + await SetupReactContextAsync(reactContext); return reactContext; } catch (OperationCanceledException) @@ -607,7 +607,7 @@ private Task CreateReactContextFromCachedPackagerBundleAsync(Cance return null; } - private void SetupReactContext(ReactContext reactContext) + private async Task SetupReactContextAsync(ReactContext reactContext) { DispatcherHelpers.AssertOnDispatcher(); if (_currentReactContext != null) @@ -624,7 +624,7 @@ private void SetupReactContext(ReactContext reactContext) foreach (var rootView in _attachedRootViews) { - AttachMeasuredRootViewToInstance(rootView, reactInstance); + await AttachMeasuredRootViewToInstanceAsync(rootView, reactInstance); } } @@ -634,13 +634,13 @@ private void InvokeDefaultOnBackPressed() _defaultBackButtonHandler?.Invoke(); } - private void AttachMeasuredRootViewToInstance( + private async Task AttachMeasuredRootViewToInstanceAsync( ReactRootView rootView, IReactInstance reactInstance) { DispatcherHelpers.AssertOnDispatcher(); - var rootTag = reactInstance.GetNativeModule() - .AddMeasuredRootView(rootView); + var rootTag = await reactInstance.GetNativeModule() + .AddMeasuredRootViewAsync(rootView); var jsAppModuleName = rootView.JavaScriptModuleName; var appParameters = new Dictionary diff --git a/ReactWindows/ReactNative.Shared/ReactRootView.cs b/ReactWindows/ReactNative.Shared/ReactRootView.cs index 63d9406d8a2..27c4eafc392 100644 --- a/ReactWindows/ReactNative.Shared/ReactRootView.cs +++ b/ReactWindows/ReactNative.Shared/ReactRootView.cs @@ -134,7 +134,9 @@ private async Task StartReactApplicationAsync(ReactInstanceManager reactInstance var getReactContextTaskTask = DispatcherHelpers.CallOnDispatcher(async () => await _reactInstanceManager.GetOrCreateReactContextAsync(CancellationToken.None), true); - + + await getReactContextTaskTask.Unwrap(); + // 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. @@ -146,8 +148,6 @@ private async Task StartReactApplicationAsync(ReactInstanceManager reactInstance { _attachScheduled = true; } - - await getReactContextTaskTask.Unwrap(); } /// @@ -167,6 +167,7 @@ public void StopReactApplication() /// /// Has to be called under the dispatcher associated with the view. /// + /// Awaitable task. public async Task StopReactApplicationAsync() { DispatcherHelpers.AssertOnDispatcher(this); @@ -176,6 +177,8 @@ public async Task StopReactApplicationAsync() { await reactInstanceManager.DetachRootViewAsync(this); } + + _attachScheduled = false; } /// @@ -241,9 +244,9 @@ private async Task MeasureOverrideHelperAsync() var reactInstanceManager = _reactInstanceManager; if (_attachScheduled && reactInstanceManager != null) { - _attachScheduled = false; - await reactInstanceManager.AttachMeasuredRootViewAsync(this); + + _attachScheduled = false; } } diff --git a/ReactWindows/ReactNative.Shared/UIManager/NativeViewHierarchyManager.cs b/ReactWindows/ReactNative.Shared/UIManager/NativeViewHierarchyManager.cs index 4cd485fa036..a869216212a 100644 --- a/ReactWindows/ReactNative.Shared/UIManager/NativeViewHierarchyManager.cs +++ b/ReactWindows/ReactNative.Shared/UIManager/NativeViewHierarchyManager.cs @@ -663,6 +663,7 @@ private void AddRootViewParent(int tag, FrameworkElement view, ThemedReactContex _tagsToViewManagers.Add(tag, _rootViewManager); _rootTags.Add(tag, true); + // Keeping here for symmetry, tag on root views is set early, in UIManagerModule.AddMeasuredRootViewAsync ViewExtensions.SetTag(view, tag); ViewExtensions.SetReactContext(view, themedContext); #if WINDOWS_UWP diff --git a/ReactWindows/ReactNative.Shared/UIManager/UIManagerModule.cs b/ReactWindows/ReactNative.Shared/UIManager/UIManagerModule.cs index 062ea1410df..2fb1851ed92 100644 --- a/ReactWindows/ReactNative.Shared/UIManager/UIManagerModule.cs +++ b/ReactWindows/ReactNative.Shared/UIManager/UIManagerModule.cs @@ -125,7 +125,7 @@ public UIImplementation UIImplementation /// JavaScript can use the returned tag with to add or remove children /// to this view through . /// - public int AddMeasuredRootView(ReactRootView rootView) + public async Task AddMeasuredRootViewAsync(ReactRootView rootView) { // Called on main dispatcher thread DispatcherHelpers.AssertOnDispatcher(); @@ -133,9 +133,12 @@ public int AddMeasuredRootView(ReactRootView rootView) var tag = _nextRootTag; _nextRootTag += RootViewTagIncrement; + // Set tag early in case of concurrent DetachRootViewAsync + rootView.SetTag(tag); + var context = new ThemedReactContext(Context); - DispatcherHelpers.RunOnDispatcher(rootView.Dispatcher, () => + await DispatcherHelpers.CallOnDispatcher(rootView.Dispatcher, () => { var width = rootView.ActualWidth; var height = rootView.ActualHeight; @@ -167,6 +170,7 @@ public int AddMeasuredRootView(ReactRootView rootView) // Register view in DeviceInfoModule for tracking its dimensions Context.GetNativeModule().RegisterRootView(rootView, tag); #endif + return true; }, true); // Allow inlining return tag; diff --git a/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs index 1c0b3f6cc7e..d84ab5a6e3e 100644 --- a/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs +++ b/ReactWindows/ReactNative.Tests/Internal/DispatcherHelpers.cs @@ -9,16 +9,26 @@ namespace ReactNative.Tests { static class DispatcherHelpers { - public static async Task RunOnDispatcherAsync(Action action) + public static Task RunOnDispatcherAsync(Action action) { - await App.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(action)).AsTask().ConfigureAwait(false); + return RunOnDispatcherAsync(App.Dispatcher, action); } - public static async Task CallOnDispatcherAsync(Func func) + public static async Task RunOnDispatcherAsync(CoreDispatcher dispatcher, Action action) + { + await dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(action)).AsTask().ConfigureAwait(false); + } + + public static Task CallOnDispatcherAsync(Func func) + { + return CallOnDispatcherAsync(App.Dispatcher, func); + } + + public static async Task CallOnDispatcherAsync(CoreDispatcher dispatcher, Func func) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await RunOnDispatcherAsync(() => + await RunOnDispatcherAsync(dispatcher, () => { try { @@ -34,11 +44,16 @@ public static async Task CallOnDispatcherAsync(Func func) return await tcs.Task.ConfigureAwait(false); } - public static async Task CallOnDispatcherAsync(Func asyncFunc) + public static Task CallOnDispatcherAsync(Func asyncFunc) + { + return CallOnDispatcherAsync(App.Dispatcher, asyncFunc); + } + + public static async Task CallOnDispatcherAsync(CoreDispatcher dispatcher, Func asyncFunc) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await RunOnDispatcherAsync(async () => + await RunOnDispatcherAsync(dispatcher, async () => { try { @@ -54,11 +69,16 @@ public static async Task CallOnDispatcherAsync(Func asyncFunc) await tcs.Task.ConfigureAwait(false); } - public static async Task CallOnDispatcherAsync(Func> asyncFunc) + public static Task CallOnDispatcherAsync(Func> asyncFunc) + { + return CallOnDispatcherAsync(App.Dispatcher, asyncFunc); + } + + public static async Task CallOnDispatcherAsync(CoreDispatcher dispatcher, Func> asyncFunc) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await RunOnDispatcherAsync(async () => + await RunOnDispatcherAsync(dispatcher, async () => { try { diff --git a/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj index d0914537c88..0798d1babe4 100644 --- a/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj +++ b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj @@ -18,8 +18,15 @@ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} ReactNative.Tests_TemporaryKey.pfx 14.0 + Debug F0FB82837F45EE9E0EFC3B38576F3C1523EAC45A + + Debug + + + Release + true bin\x86\Debug\ @@ -111,6 +118,7 @@ + @@ -142,6 +150,7 @@ + Always @@ -158,6 +167,13 @@ ReactNative + + + %(Filename)%(Extension) + PreserveNewest + False + + diff --git a/ReactWindows/ReactNative.Tests/ReactRootViewTests.cs b/ReactWindows/ReactNative.Tests/ReactRootViewTests.cs new file mode 100644 index 00000000000..1df13b49f3e --- /dev/null +++ b/ReactWindows/ReactNative.Tests/ReactRootViewTests.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using ReactNative.Bridge; +using ReactNative.Common; +using ReactNative.Modules.Core; +using ReactNative.UIManager; +using System; +using System.Threading; +using System.Threading.Tasks; +using Windows.ApplicationModel.Core; +using Windows.UI.Core; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.Tests +{ + [TestClass] + public class ReactRootViewTests + { + [TestMethod] + public async Task ReactRootView_SecondaryWindow() + { + var jsBundleFile = "ms-appx:///Resources/mwtest.js"; + ReactInstanceManager manager = null; + await DispatcherHelpers.CallOnDispatcherAsync(() => manager = CreateReactInstanceManager(jsBundleFile)); + + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); + + var dispatcher = await CreateView(() => + { + var rv = new ReactRootView(); + rv.StartReactApplication( + manager, + "alt_window", + null); + return rv; + }); + + await CloseView(manager, dispatcher); + + await DispatcherHelpers.CallOnDispatcherAsync(async () => await DisposeInstanceManager(manager)); + } + + [TestMethod] + public async Task ReactRootView_SecondaryWindowStress() + { + var jsBundleFile = "ms-appx:///Resources/mwtest.js"; + ReactInstanceManager manager = null; + await DispatcherHelpers.CallOnDispatcherAsync(() => manager = CreateReactInstanceManager(jsBundleFile)); + + var reactContext = await DispatcherHelpers.CallOnDispatcherAsync( + () => manager.CreateReactContextAsync(CancellationToken.None)); + + int currentDelay = 2000; + + for (int i = 0; i < 30; i++) + { + currentDelay /= 2; + + // Create a window + var dispatcher = await CreateView(() => + { + var rv = new ReactRootView(); + rv.StartReactApplication( + manager, + "alt_window", + null); + return rv; + }); + + await Task.Delay(currentDelay); + + await CloseView(manager, dispatcher); + } + + await DispatcherHelpers.CallOnDispatcherAsync(async () => await DisposeInstanceManager(manager)); + } + + private static ReactInstanceManager CreateReactInstanceManager(string jsBundleFile, LifecycleState initialLifecycleState = LifecycleState.Foreground) + { + ReactNative.Bridge.DispatcherHelpers.Initialize(); + ReactChoreographer.Initialize(); + + return new ReactInstanceManagerBuilder + { + InitialLifecycleState = initialLifecycleState, + JavaScriptBundleFile = jsBundleFile, + }.Build(); + } + + private static async Task DisposeInstanceManager(ReactInstanceManager manager) + { + await manager.DisposeAsync(); + + // Go back to the initial state as set by the host test app + ReactNative.Bridge.DispatcherHelpers.Initialize(); + } + + private static async Task CreateView(Func rootViewCreateAction) + { + var dispatcherTask = DispatcherHelpers.CallOnDispatcherAsync(async () => + { + CoreApplicationView newView = CoreApplication.CreateNewView(); + int newViewId = 0; + var dispatcher = newView.Dispatcher; + + await DispatcherHelpers.CallOnDispatcherAsync(dispatcher, () => + { + Frame frame = new Frame(); + + Window.Current.Content = frame; + + frame.Content = new Page + { + Content = rootViewCreateAction(), + }; + + // You have to activate the window in order to show it later. + Window.Current.Activate(); + + newViewId = ApplicationView.GetForCurrentView().Id; + + return true; + }); + + bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId); + + return dispatcher; + }); + + return await dispatcherTask; + } + + private static async Task CloseView(ReactInstanceManager manager, CoreDispatcher dispatcher) + { + await DispatcherHelpers.CallOnDispatcherAsync(dispatcher, async () => + { + var window = Window.Current; + if (window != null) + { + var frame = window.Content as Frame; + if (frame != null) + { + var page = frame.Content as Page; + if (page != null) + { + var rootView = page.Content as ReactRootView; + if (rootView != null) + { + await rootView.StopReactApplicationAsync(); + + Window.Current.Close(); + page.Content = null; + } + } + } + } + }); + } + } +} diff --git a/ReactWindows/ReactNative.Tests/Resources/mwtest.js b/ReactWindows/ReactNative.Tests/Resources/mwtest.js new file mode 100644 index 00000000000..bc0d4d6b5d4 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/Resources/mwtest.js @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +function require(name) { + return this[name]; +} + +// Extract the remoteRootView method from config +var modconfig = __fbBatchedBridgeConfig.remoteModuleConfig; +var uiManagerModuleId = modconfig.findIndex(descr => descr[0] === 'UIManager'); +var uiManagerConfig = modconfig[uiManagerModuleId]; +var removeRootMethodId = uiManagerConfig[2].findIndex(name => name === 'removeRootView'); + +var FunctionCalls = new Array(); +var CallbackCalls = new Array(); +var BatchedBridge = + { + 'callFunctionReturnFlushedQueue': function (moduleId, methodId, args) { + FunctionCalls.push([moduleId, methodId, args]); + + if (moduleId === 'AppRegistry' && + methodId === 'unmountApplicationComponentAtRootTag') { + return [[uiManagerModuleId], [removeRootMethodId], [[args[0]]]]; + } else { + return [[], [], []]; + } + }, + 'invokeCallbackAndReturnFlushedQueue': function (callbackId, args) { + CallbackCalls.push([callbackId, args]); + return [[], [], []]; + }, + 'flushedQueue': function (args) { + return [[], [], []]; + } + }; + +var __fbBatchedBridge = BatchedBridge; + +