diff --git a/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj index a43b5ee7db9..231ee65064b 100644 --- a/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj +++ b/ReactWindows/ReactNative.Tests/ReactNative.Tests.csproj @@ -125,7 +125,9 @@ + + diff --git a/ReactWindows/ReactNative.Tests/UIManager/FrameworkElementExtensionsTests.cs b/ReactWindows/ReactNative.Tests/UIManager/FrameworkElementExtensionsTests.cs new file mode 100644 index 00000000000..b26c5984b51 --- /dev/null +++ b/ReactWindows/ReactNative.Tests/UIManager/FrameworkElementExtensionsTests.cs @@ -0,0 +1,57 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using ReactNative.Bridge; +using ReactNative.UIManager; +using System; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.Tests.UIManager +{ + [TestClass] + public class FrameworkElementExtensionsTests + { + [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] + public void FrameworkElementExtensions_ArgumentChecks() + { + var element = new Button(); + + AssertEx.Throws( + () => FrameworkElementExtensions.SetTag(null, 0), + ex => Assert.AreEqual("view", ex.ParamName)); + + AssertEx.Throws( + () => FrameworkElementExtensions.SetReactContext(null, null), + ex => Assert.AreEqual("view", ex.ParamName)); + + AssertEx.Throws( + () => FrameworkElementExtensions.GetTag(null), + ex => Assert.AreEqual("view", ex.ParamName)); + + AssertEx.Throws( + () => FrameworkElementExtensions.GetReactContext(null), + ex => Assert.AreEqual("view", ex.ParamName)); + } + + [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] + public void FrameworkElementExtensions_ExistingTag() + { + var button = new Button(); + button.Tag = new object(); + + AssertEx.Throws(() => button.SetTag(1)); + AssertEx.Throws(() => button.SetReactContext(null)); + } + + [Microsoft.VisualStudio.TestPlatform.UnitTestFramework.AppContainer.UITestMethod] + public void FrameworkElementExtensions_Get_Set() + { + var button = new Button(); + + button.SetTag(42); + Assert.AreEqual(42, button.GetTag()); + + button.SetReactContext(null); + Assert.IsNull(button.GetReactContext()); + } + + } +} diff --git a/ReactWindows/ReactNative.Tests/UIManager/PropertySetterTests.cs b/ReactWindows/ReactNative.Tests/UIManager/PropertySetterTests.cs index 96ec8514ddb..758fad270ba 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/PropertySetterTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/PropertySetterTests.cs @@ -350,6 +350,21 @@ public ReactShadowNode CreateShadowNodeInstance() throw new NotImplementedException(); } + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMap properties) { throw new NotImplementedException(); diff --git a/ReactWindows/ReactNative.Tests/UIManager/RootViewHelperTests.cs b/ReactWindows/ReactNative.Tests/UIManager/RootViewHelperTests.cs new file mode 100644 index 00000000000..0a06609d87d --- /dev/null +++ b/ReactWindows/ReactNative.Tests/UIManager/RootViewHelperTests.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using ReactNative.UIManager; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.Tests.UIManager +{ + [TestClass] + public class RootViewHelperTests + { + [TestMethod] + public void RootViewHelper_Null() + { + Assert.IsNull(RootViewHelper.GetRootView(null)); + } + + class TestRootView : Panel, IRootView + { + public void OnChildStartedNativeGesture(RoutedEventArgs ev) + { + } + } + } +} diff --git a/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs b/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs index 0d11ffe7d5d..16f4fe17851 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/UIManagerModuleTests.cs @@ -1,4 +1,5 @@ using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Tests.Constants; using ReactNative.UIManager; @@ -132,6 +133,21 @@ public ReactShadowNode CreateShadowNodeInstance() return null; } + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + public void UpdateExtraData(FrameworkElement viewToUpdate, object extraData) { throw new NotImplementedException(); diff --git a/ReactWindows/ReactNative.Tests/UIManager/ViewAtIndexTests.cs b/ReactWindows/ReactNative.Tests/UIManager/ViewAtIndexTests.cs index 23bb7d67db4..b642ac6ba8d 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/ViewAtIndexTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/ViewAtIndexTests.cs @@ -12,9 +12,9 @@ public void ViewAtIndex_Comparator() var v1 = new ViewAtIndex(17, 6); var v2 = new ViewAtIndex(42, 17); - Assert.IsTrue(ViewAtIndex.Comparer.Compare(v1, v2) < 0); - Assert.IsTrue(ViewAtIndex.Comparer.Compare(v2, v1) > 0); - Assert.AreEqual(0, ViewAtIndex.Comparer.Compare(v1, v1)); + Assert.IsTrue(ViewAtIndex.IndexComparer.Compare(v1, v2) < 0); + Assert.IsTrue(ViewAtIndex.IndexComparer.Compare(v2, v1) > 0); + Assert.AreEqual(0, ViewAtIndex.IndexComparer.Compare(v1, v1)); } [TestMethod] diff --git a/ReactWindows/ReactNative.Tests/UIManager/ViewManagerRegistryTests.cs b/ReactWindows/ReactNative.Tests/UIManager/ViewManagerRegistryTests.cs index 82047426b84..f36596f8d58 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/ViewManagerRegistryTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/ViewManagerRegistryTests.cs @@ -1,4 +1,5 @@ using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; +using Newtonsoft.Json.Linq; using ReactNative.UIManager; using System; using System.Collections.Generic; @@ -90,6 +91,21 @@ public ReactShadowNode CreateShadowNodeInstance() throw new NotImplementedException(); } + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + public void UpdateExtraData(FrameworkElement viewToUpdate, object extraData) { throw new NotImplementedException(); diff --git a/ReactWindows/ReactNative.Tests/UIManager/ViewManagersPropertyCacheTests.cs b/ReactWindows/ReactNative.Tests/UIManager/ViewManagersPropertyCacheTests.cs index 88b14748884..cb18801d3e9 100644 --- a/ReactWindows/ReactNative.Tests/UIManager/ViewManagersPropertyCacheTests.cs +++ b/ReactWindows/ReactNative.Tests/UIManager/ViewManagersPropertyCacheTests.cs @@ -190,7 +190,9 @@ public void ViewManagersPropertyCache_Defaults() class EmptyTest : IViewManager { - public IReadOnlyDictionary CommandsMap + #region IViewManager + + public string Name { get { @@ -198,7 +200,7 @@ public IReadOnlyDictionary CommandsMap } } - public IReadOnlyDictionary ExportedCustomBubblingEventTypeConstants + public IReadOnlyDictionary CommandsMap { get { @@ -206,7 +208,7 @@ public IReadOnlyDictionary ExportedCustomBubblingEventTypeConsta } } - public IReadOnlyDictionary ExportedCustomDirectEventTypeConstants + public IReadOnlyDictionary ExportedCustomBubblingEventTypeConstants { get { @@ -214,7 +216,7 @@ public IReadOnlyDictionary ExportedCustomDirectEventTypeConstant } } - public IReadOnlyDictionary ExportedViewConstants + public IReadOnlyDictionary ExportedCustomDirectEventTypeConstants { get { @@ -222,7 +224,7 @@ public IReadOnlyDictionary ExportedViewConstants } } - public string Name + public IReadOnlyDictionary ExportedViewConstants { get { @@ -243,7 +245,17 @@ public ReactShadowNode CreateShadowNodeInstance() throw new NotImplementedException(); } - public void UpdateExtraData(FrameworkElement viewToUpdate, object extraData) + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) { throw new NotImplementedException(); } @@ -252,6 +264,13 @@ public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMa { throw new NotImplementedException(); } + + public void UpdateExtraData(FrameworkElement viewToUpdate, object extraData) + { + throw new NotImplementedException(); + } + + #endregion } class ViewManagerValueTest : IViewManager @@ -272,7 +291,7 @@ public void Bar(FrameworkElement element, int index, string value) BarValues[index] = value; } - #region IViewManager Implementation + #region IViewManager public string Name { @@ -327,6 +346,21 @@ public ReactShadowNode CreateShadowNodeInstance() throw new NotImplementedException(); } + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMap properties) { throw new NotImplementedException(); @@ -540,6 +574,21 @@ public ReactShadowNode CreateShadowNodeInstance() throw new NotImplementedException(); } + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMap properties) { throw new NotImplementedException(); diff --git a/ReactWindows/ReactNative/IReactInstanceManager.cs b/ReactWindows/ReactNative/IReactInstanceManager.cs index a1852ffdd4f..f005bd435fb 100644 --- a/ReactWindows/ReactNative/IReactInstanceManager.cs +++ b/ReactWindows/ReactNative/IReactInstanceManager.cs @@ -47,8 +47,6 @@ public interface IReactInstanceManager : IDisposable //public abstract void onResume(DefaultHardwareBackBtnHandler defaultBackButtonImpl); - void Dispose(); - /// /// Attach given {@param rootView} to a catalyst instance manager and start JS application /// diff --git a/ReactWindows/ReactNative/ReactNative.csproj b/ReactWindows/ReactNative/ReactNative.csproj index b5ba7f63f96..b5b76a2783a 100644 --- a/ReactWindows/ReactNative/ReactNative.csproj +++ b/ReactWindows/ReactNative/ReactNative.csproj @@ -191,6 +191,7 @@ + @@ -207,6 +208,9 @@ + + + diff --git a/ReactWindows/ReactNative/UIManager/Animation/AnimationRegistry.cs b/ReactWindows/ReactNative/UIManager/Animation/AnimationRegistry.cs new file mode 100644 index 00000000000..4dd83a4f4b5 --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/Animation/AnimationRegistry.cs @@ -0,0 +1,9 @@ +namespace ReactNative.UIManager.Animation +{ + public class AnimationRegistry + { + public AnimationRegistry() + { + } + } +} \ No newline at end of file diff --git a/ReactWindows/ReactNative/UIManager/FrameworkElementExtensions.cs b/ReactWindows/ReactNative/UIManager/FrameworkElementExtensions.cs new file mode 100644 index 00000000000..43e7f2931ce --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/FrameworkElementExtensions.cs @@ -0,0 +1,91 @@ +using System; +using Windows.UI.Xaml; + +namespace ReactNative.UIManager +{ + static class FrameworkElementExtensions + { + public static void SetTag(this FrameworkElement view, int tag) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + var existingData = view.Tag; + var elementData = default(FrameworkElementData); + if (existingData == null) + { + elementData = new FrameworkElementData(); + view.Tag = elementData; + } + else + { + elementData = existingData as FrameworkElementData; + if (elementData == null) + { + throw new InvalidOperationException("Tag for FrameworkElement has already been set."); + } + } + + elementData.Tag = tag; + } + + public static int GetTag(this FrameworkElement view) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + var elementData = view.Tag as FrameworkElementData; + if (elementData == null || elementData.Tag == null) + { + throw new InvalidOperationException("Could not get tag for view."); + } + + return elementData.Tag.Value; + } + + public static void SetReactContext(this FrameworkElement view, ThemedReactContext context) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + var existingData = view.Tag; + var elementData = default(FrameworkElementData); + if (existingData == null) + { + elementData = new FrameworkElementData(); + view.Tag = elementData; + } + else + { + elementData = existingData as FrameworkElementData; + if (elementData == null) + { + throw new InvalidOperationException("Tag for FrameworkElement has already been set."); + } + } + + elementData.Context = context; + } + + public static ThemedReactContext GetReactContext(this FrameworkElement view) + { + if (view == null) + throw new ArgumentNullException(nameof(view)); + + var elementData = view.Tag as FrameworkElementData; + if (elementData == null) + { + throw new InvalidOperationException("Could not get context for view."); + } + + return elementData.Context; + } + + class FrameworkElementData + { + public ThemedReactContext Context { get; set; } + + public int? Tag { get; set; } + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/IViewManager.cs b/ReactWindows/ReactNative/UIManager/IViewManager.cs index 3862c207c67..34dd9d0e040 100644 --- a/ReactWindows/ReactNative/UIManager/IViewManager.cs +++ b/ReactWindows/ReactNative/UIManager/IViewManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; using Windows.UI.Xaml; namespace ReactNative.UIManager @@ -44,6 +45,34 @@ public interface IViewManager /// The shadow node instance. ReactShadowNode CreateShadowNodeInstance(); + /// + /// Creates a view and installs event emitters on it. + /// + /// The context. + /// The responder handler. + /// The view. + FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler); + + /// + /// Called when view is detached from view hierarchy and allows for + /// additional cleanup by the + /// subclass. + /// + /// The react context. + /// The view. + void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view); + + /// + /// Implement this method to receive events/commands directly from + /// JavaScript through the . + /// + /// + /// The view instance that should receive the command. + /// + /// Identifer for the command. + /// Optional arguments for the command. + void ReceiveCommand(FrameworkElement view, int commandId, JArray args); + /// /// Update the properties of the given view. /// diff --git a/ReactWindows/ReactNative/UIManager/JavaScriptResponderHandler.cs b/ReactWindows/ReactNative/UIManager/JavaScriptResponderHandler.cs index df1a574911e..7ebb44534b8 100644 --- a/ReactWindows/ReactNative/UIManager/JavaScriptResponderHandler.cs +++ b/ReactWindows/ReactNative/UIManager/JavaScriptResponderHandler.cs @@ -1,6 +1,17 @@ -namespace ReactNative.UIManager +using System; + +namespace ReactNative.UIManager { public class JavaScriptResponderHandler { + internal void ClearJavaScriptResponder() + { + throw new NotImplementedException(); + } + + internal void SetJavaScriptResponder(int initialReactTag, object p) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs b/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs index 0d1dc402133..453037b88e7 100644 --- a/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs +++ b/ReactWindows/ReactNative/UIManager/NativeViewHierarchyManager.cs @@ -1,10 +1,13 @@ using Newtonsoft.Json.Linq; using ReactNative.Bridge; using ReactNative.Tracing; +using ReactNative.UIManager.Animation; using System; using System.Collections.Generic; using System.Globalization; +using Windows.UI.Popups; using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; namespace ReactNative.UIManager { @@ -37,6 +40,9 @@ namespace ReactNative.UIManager /// /// TODO: /// 1) AnimationRegistry + /// 2) UpdateLayout + /// 3) Measure + /// 4) ShowPopupMenu /// public class NativeViewHierarchyManager { @@ -46,7 +52,12 @@ public class NativeViewHierarchyManager private readonly ViewManagerRegistry _viewManagers; private readonly JavaScriptResponderHandler _jsResponderHandler; private readonly RootViewManager _rootViewManager; + private readonly AnimationRegistry _animationRegistry; + /// + /// Instantiates the . + /// + /// The view manager registry. public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) { _viewManagers = viewManagers; @@ -55,9 +66,35 @@ public NativeViewHierarchyManager(ViewManagerRegistry viewManagers) _rootTags = new Dictionary(); _jsResponderHandler = new JavaScriptResponderHandler(); _rootViewManager = new RootViewManager(); + _animationRegistry = new AnimationRegistry(); } - public void UpdateProperties(int tag, string className, CatalystStylesDiffMap properties) + /// + /// The animation registry. + /// + public AnimationRegistry Animations + { + get + { + return _animationRegistry; + } + } + + /// + /// Signals if layout animation is enabled. + /// + public bool LayoutAnimationEnabled + { + private get; + set; + } + + /// + /// Updates the properties of the view with the given tag. + /// + /// The view tag. + /// The properties. + public void UpdateProperties(int tag, CatalystStylesDiffMap properties) { DispatcherHelpers.AssertOnDispatcher(); var viewManager = ResolveViewManager(tag); @@ -65,14 +102,28 @@ public void UpdateProperties(int tag, string className, CatalystStylesDiffMap pr viewManager.UpdateProperties(viewToUpdate, properties); } - public void UpdateViewExtraData(int tag, object data) + /// + /// Updates the extra data for the view with the given tag. + /// + /// The view tag. + /// The extra data. + public void UpdateViewExtraData(int tag, object extraData) { DispatcherHelpers.AssertOnDispatcher(); var viewManager = ResolveViewManager(tag); var viewToUpdate = ResolveView(tag); - viewManager.UpdateExtraData(viewToUpdate, data); + viewManager.UpdateExtraData(viewToUpdate, extraData); } + /// + /// Updates the layout of a view. + /// + /// The parent view tag. + /// The view tag. + /// The left coordinate. + /// The right coordinate. + /// The layout width. + /// The layout height. public void UpdateLayout(int parentTag, int tag, int x, int y, int width, int height) { DispatcherHelpers.AssertOnDispatcher(); @@ -82,83 +133,384 @@ public void UpdateLayout(int parentTag, int tag, int x, int y, int width, int he { var viewToUpdate = ResolveView(tag); // TODO: call viewToUpdate.Measure() + throw new NotImplementedException(); + } + } + + /// + /// Creates a view with the given tag and class name. + /// + /// The context. + /// The tag. + /// The class name. + /// The properties. + public void CreateView(ThemedReactContext themedContext, int tag, string className, CatalystStylesDiffMap initialProperties) + { + DispatcherHelpers.AssertOnDispatcher(); + using (Tracer.Trace(Tracer.TRACE_TAG_REACT_VIEW, "NativeViewHierarcyManager.CreateView") + .With("tag", tag) + .With("className", className)) + { + var viewManager = _viewManagers.Get(className); + var view = viewManager.CreateView(themedContext, _jsResponderHandler); + _tagsToViews.Add(tag, view); + _tagsToViewManagers.Add(tag, viewManager); + // Uses an extension method and a conditional weak table to + // store the tag of the view. The conditional weak table does + // not prevent the view from being garbage collected. + view.SetTag(tag); + + if (initialProperties != null) + { + viewManager.UpdateProperties(view, initialProperties); + } } } - private FrameworkElement ResolveView(int tag) + /// + /// Manages the children of a react view. + /// + /// The tag of the view to manager. + /// Child indices to remove. + /// Views to add. + /// Tags to delete. + public void ManageChildren(int tag, int[] indicesToRemove, ViewAtIndex[] viewsToAdd, int[] tagsToDelete) { - var view = default(FrameworkElement); - if (!_tagsToViews.TryGetValue(tag, out view)) + var viewManager = default(IViewManager); + if (!_tagsToViewManagers.TryGetValue(tag, out viewManager)) { throw new InvalidOperationException( string.Format( CultureInfo.InvariantCulture, - "Trying to resolve view with tag '{0}' which doesn't exist.", + "Trying to manage children with tag '{0}' which doesn't exist.", tag)); } - return view; + var viewGroupManager = (ViewGroupManager)viewManager; + var viewToManage = (Panel)_tagsToViews[tag]; + + var lastIndexToRemove = viewGroupManager.GetChildCount(viewToManage); + if (indicesToRemove != null) + { + for (var i = indicesToRemove.Length - 1; i >= 0; --i) + { + var indexToRemove = indicesToRemove[i]; + if (indexToRemove < 0) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to remove a negative index '{0}' on view tag '{1}'.", + indexToRemove, + tag)); + } + + if (indexToRemove >= viewGroupManager.GetChildCount(viewToManage)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to remove a view index '{0}' greater than the child could for view tag '{1}'.", + indexToRemove, + tag)); + } + + if (indexToRemove >= lastIndexToRemove) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to remove an out of order index '{0}' (last index was '{1}') for view tag '{2}'.", + indexToRemove, + lastIndexToRemove, + tag)); + } + + viewGroupManager.RemoveChildAt(viewToManage, indexToRemove); + lastIndexToRemove = indexToRemove; + } + } + + if (viewsToAdd != null) + { + for (var i = 0; i < viewsToAdd.Length; ++i) + { + var viewAtIndex = viewsToAdd[i]; + var viewToAdd = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(viewAtIndex.Tag, out viewToAdd)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to add unknown view tag '{0}'.", + viewAtIndex.Tag)); + } + + viewGroupManager.AddView(viewToManage, viewToAdd, viewAtIndex.Index); + } + } + + if (tagsToDelete != null) + { + for (var i = 0; i < tagsToDelete.Length; ++i) + { + var tagToDelete = tagsToDelete[i]; + var viewToDestroy = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(tagToDelete, out viewToDestroy)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to destroy unknown view tag '{0}'.", + tagToDelete)); + } + + DropView(viewToDestroy); + } + } } - private IViewManager ResolveViewManager(int tag) + /// + /// Remove the root view with the given tag. + /// + /// The root view tag. + public void RemoveRootView(int rootViewTag) { - var viewManager = default(IViewManager); - if (!_tagsToViewManagers.TryGetValue(tag, out viewManager)) + DispatcherHelpers.AssertOnDispatcher(); + if (!_rootTags.ContainsKey(rootViewTag)) { throw new InvalidOperationException( string.Format( CultureInfo.InvariantCulture, - "ViewManager for tag '{0}' could not be found.", - tag)); + "View with tag '{0}' is not registered as a root view.", + rootViewTag)); } - return viewManager; + var rootView = _tagsToViews[rootViewTag]; + DropView(rootView); + _rootTags.Remove(rootViewTag); } - internal void RemoveRootView(int rootViewTag) + /// + /// Measures a view and sets the output buffer to (x, y, width, height). + /// + /// The view tag. + /// The output buffer. + public void Measure(int tag, int[] outputBuffer) { + DispatcherHelpers.AssertOnDispatcher(); + var v = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(tag, out v)) + { + throw new ArgumentOutOfRangeException(nameof(tag)); + } + + var rootView = (FrameworkElement)RootViewHelper.GetRootView(v); + if (rootView == null) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Native view '{0}' is no longer on screen.", + tag)); + } + + //TODO: implement get position, etc. throw new NotImplementedException(); } - internal void CreateView(ThemedReactContext themedContext, int tag, string className, CatalystStylesDiffMap initialProps) + /// + /// Adds a root view with the given tag. + /// + /// The tag. + /// The root view. + /// The themed context. + public void AddRootView(int tag, SizeMonitoringFrameLayout view, ThemedReactContext themedContext) { - throw new NotImplementedException(); + AddRootViewGroup(tag, view, themedContext); } - internal void ManageChildren(int tag, int[] indicesToRemove, ViewAtIndex[] viewsToAdd, int[] tagsToDelete) + /// + /// Find the view target for touch coordinates. + /// + /// The view tag. + /// The x-coordinate of the touch event. + /// The y-coordinate of the touch event. + /// The view target. + public int FindTargetForTouch(int reactTag, double touchX, double touchY) { - throw new NotImplementedException(); + var view = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(reactTag, out view)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Could not find view with tag '{0}'.", + reactTag)); + } + + return TouchTargetHelper.FindTargetTagForTouch(touchX, touchY, (Panel)view); } - internal void AddRootView(int tag, SizeMonitoringFrameLayout rootView, ThemedReactContext themedRootContext) + /// + /// Sets the JavaScript responder handler for a view. + /// + /// The view tag. + /// The initial tag. + /// + /// Flag to block the native responder. + /// + public void SetJavaScriptResponder(int reactTag, int initialReactTag, bool blockNativeResponder) { + if (!blockNativeResponder) + { + _jsResponderHandler.SetJavaScriptResponder(initialReactTag, null); + return; + } + throw new NotImplementedException(); } - internal void SetJavaScriptResponder(int tag, int initialTag, bool blockNativeResponder) + /// + /// Clears the JavaScript responder. + /// + public void ClearJavaScriptResponder() { - throw new NotImplementedException(); + _jsResponderHandler.ClearJavaScriptResponder(); } - internal void Measure(int reactTag, int[] _measureBuffer) + /// + /// Dispatches a command to a view. + /// + /// The view tag. + /// The command identifier. + /// The command arguments. + public void DispatchCommand(int reactTag, int commandId, JArray args) { - throw new NotImplementedException(); + DispatcherHelpers.AssertOnDispatcher(); + var view = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(reactTag, out view)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to send command to a non-existent view with tag '{0}.", + reactTag)); + } + + var viewManager = ResolveViewManager(reactTag); + viewManager.ReceiveCommand(view, commandId, args); } - internal void ClearJavaScriptResponder() + /// + /// Shows a . + /// + /// + /// The tag of the anchor view (the is + /// displayed next to this view); this needs to be the tag of a native + /// view (shadow views cannot be anchors). + /// + /// The menu items as an array of strings. + /// + /// A callback used with the position of the selected item as the first + /// argument, or no arguments if the menu is dismissed. + /// + public void ShowPopupMenu(int tag, string[] items, ICallback success) { + DispatcherHelpers.AssertOnDispatcher(); + var view = ResolveView(tag); + + var menu = new PopupMenu(); + for (var i = 0; i < items.Length; ++i) + { + menu.Commands.Add(new UICommand( + items[i], + cmd => + { + success.Invoke(cmd.Id); + }, + i)); + } + + // TODO: figure out where to popup the menu + // TODO: add continuation that calls the callback with empty args throw new NotImplementedException(); } - internal void DispatchCommand(int reactTag, int commandId, JArray commandArgs) + private FrameworkElement ResolveView(int tag) { - throw new NotImplementedException(); + var view = default(FrameworkElement); + if (!_tagsToViews.TryGetValue(tag, out view)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Trying to resolve view with tag '{0}' which doesn't exist.", + tag)); + } + + return view; } - internal void ShowPopupMenu(object tag, JArray items, ICallback success) + private IViewManager ResolveViewManager(int tag) { - throw new NotImplementedException(); + var viewManager = default(IViewManager); + if (!_tagsToViewManagers.TryGetValue(tag, out viewManager)) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "ViewManager for tag '{0}' could not be found.", + tag)); + } + + return viewManager; + } + + private void AddRootViewGroup(int tag, Panel view, ThemedReactContext themedContext) + { + DispatcherHelpers.AssertOnDispatcher(); + _tagsToViews.Add(tag, view); + _tagsToViewManagers.Add(tag, _rootViewManager); + _rootTags.Add(tag, true); + view.SetTag(tag); + } + + private void DropView(FrameworkElement view) + { + DispatcherHelpers.AssertOnDispatcher(); + var tag = view.GetTag(); + if (!_rootTags.ContainsKey(tag)) + { + // For non-root views, we notify the view manager with `OnDropViewInstance` + var mgr = ResolveViewManager(tag); + mgr.OnDropViewInstance(view.GetReactContext(), view); + } + + var viewManager = default(IViewManager); + if (_tagsToViewManagers.TryGetValue(tag, out viewManager)) + { + var viewGroup = view as Panel; + var viewGroupManager = viewManager as ViewGroupManager; + if (viewGroup != null && viewGroupManager != null) + { + for (var i = viewGroupManager.GetChildCount(viewGroup) - 1; i >= 0; --i) + { + var child = viewGroupManager.GetChildAt(viewGroup, i); + var managedChild = default(FrameworkElement); + if (_tagsToViews.TryGetValue(child.GetTag(), out managedChild)) + { + DropView(managedChild); + } + } + } + + viewGroupManager.RemoveAllChildren(viewGroup); + } + + _tagsToViews.Remove(tag); + _tagsToViewManagers.Remove(tag); } } } diff --git a/ReactWindows/ReactNative/UIManager/RootViewHelper.cs b/ReactWindows/ReactNative/UIManager/RootViewHelper.cs new file mode 100644 index 00000000000..e4f6b391ea8 --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/RootViewHelper.cs @@ -0,0 +1,35 @@ +using Windows.UI.Xaml; + +namespace ReactNative.UIManager +{ + /// + /// Helper methods for root view management. + /// + public static class RootViewHelper + { + /// + /// Returns the root view of a givenview in a react application. + /// + /// The view instance. + /// The root view instance. + public static IRootView GetRootView(FrameworkElement view) + { + var current = view; + while (true) + { + if (current == null) + { + return null; + } + + var rootView = current as IRootView; + if (rootView != null) + { + return rootView; + } + + current = (FrameworkElement)current.Parent; + } + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/RootViewManager.cs b/ReactWindows/ReactNative/UIManager/RootViewManager.cs index b4d3d56d064..b06c0de7e45 100644 --- a/ReactWindows/ReactNative/UIManager/RootViewManager.cs +++ b/ReactWindows/ReactNative/UIManager/RootViewManager.cs @@ -1,6 +1,88 @@ -namespace ReactNative.UIManager +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using Windows.UI.Xaml; + +namespace ReactNative.UIManager { - internal class RootViewManager + internal class RootViewManager : IViewManager { + public IReadOnlyDictionary CommandsMap + { + get + { + throw new NotImplementedException(); + } + } + + public IReadOnlyDictionary ExportedCustomBubblingEventTypeConstants + { + get + { + throw new NotImplementedException(); + } + } + + public IReadOnlyDictionary ExportedCustomDirectEventTypeConstants + { + get + { + throw new NotImplementedException(); + } + } + + public IReadOnlyDictionary ExportedViewConstants + { + get + { + throw new NotImplementedException(); + } + } + + public string Name + { + get + { + throw new NotImplementedException(); + } + } + + public IReadOnlyDictionary NativeProperties + { + get + { + throw new NotImplementedException(); + } + } + + public ReactShadowNode CreateShadowNodeInstance() + { + throw new NotImplementedException(); + } + + public FrameworkElement CreateView(ThemedReactContext themedContext, JavaScriptResponderHandler jsResponderHandler) + { + throw new NotImplementedException(); + } + + public void OnDropViewInstance(ThemedReactContext themedReactContext, FrameworkElement view) + { + throw new NotImplementedException(); + } + + public void ReceiveCommand(FrameworkElement view, int commandId, JArray args) + { + throw new NotImplementedException(); + } + + public void UpdateExtraData(FrameworkElement viewToUpdate, object extraData) + { + throw new NotImplementedException(); + } + + public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMap properties) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs b/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs index 2cfb13e99d3..47932d91761 100644 --- a/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs +++ b/ReactWindows/ReactNative/UIManager/SizeMonitoringFrameLayout.cs @@ -1,11 +1,12 @@ using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; namespace ReactNative.UIManager { /// /// allows registering for size change events. The main purpose for this class is to hide complexity of ReactRootView /// - public class SizeMonitoringFrameLayout + public class SizeMonitoringFrameLayout : Panel { public interface OnSizeChangedListener { diff --git a/ReactWindows/ReactNative/UIManager/TouchTargetHelper.cs b/ReactWindows/ReactNative/UIManager/TouchTargetHelper.cs new file mode 100644 index 00000000000..b7c09f530ac --- /dev/null +++ b/ReactWindows/ReactNative/UIManager/TouchTargetHelper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml.Controls; + +namespace ReactNative.UIManager +{ + class TouchTargetHelper + { + internal static int FindTargetTagForTouch(double touchX, double touchY, Panel view) + { + throw new NotImplementedException(); + } + } +} diff --git a/ReactWindows/ReactNative/UIManager/UIImplementation.cs b/ReactWindows/ReactNative/UIManager/UIImplementation.cs index b0b39408252..b7c34857181 100644 --- a/ReactWindows/ReactNative/UIManager/UIImplementation.cs +++ b/ReactWindows/ReactNative/UIManager/UIImplementation.cs @@ -280,7 +280,7 @@ public void ManageChildren( // them. Like the view removal, iteration direction is important // to preserve the correct index. - Array.Sort(viewsToAdd, ViewAtIndex.Comparer); + Array.Sort(viewsToAdd, ViewAtIndex.IndexComparer); Array.Sort(indicesToRemove); // Apply changes to the ReactShadowNode hierarchy. @@ -493,7 +493,7 @@ public void DispatchViewManagerCommand(int reactTag, int commandId, JArray comma /// Callback used with the position of the selected item as the first /// argument, or no arguments if the menu is dismissed. /// - public void ShowPopupMenu(int reactTag, JArray items, ICallback error, ICallback success) + public void ShowPopupMenu(int reactTag, string[] items, ICallback error, ICallback success) { AssertViewExists(reactTag); _operationsQueue.EnqueueShowPopupMenu(reactTag, items, error, success); diff --git a/ReactWindows/ReactNative/UIManager/UIManagerModule.cs b/ReactWindows/ReactNative/UIManager/UIManagerModule.cs index d1d49e216f7..dea8fc6c66c 100644 --- a/ReactWindows/ReactNative/UIManager/UIManagerModule.cs +++ b/ReactWindows/ReactNative/UIManager/UIManagerModule.cs @@ -313,7 +313,7 @@ public void dispatchViewManagerCommand(int reactTag, int commandId, JArray comma /// argument, or no arguments if the menu is dismissed. /// [ReactMethod] - public void showPopupMenu(int reactTag, JArray items, ICallback error, ICallback success) + public void showPopupMenu(int reactTag, string[] items, ICallback error, ICallback success) { _uiImplementation.ShowPopupMenu(reactTag, items, error, success); } diff --git a/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs b/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs index 5ead5e8cfc5..2dead432af9 100644 --- a/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs +++ b/ReactWindows/ReactNative/UIManager/UIViewOperationQueue.cs @@ -96,7 +96,7 @@ public void EnqueueDispatchCommand(int tag, int command, JArray args) EnqueueOperation(() => _nativeViewHierarchyManager.DispatchCommand(tag, command, args)); } - public void EnqueueShowPopupMenu(int tag, JArray items, ICallback error, ICallback success) + public void EnqueueShowPopupMenu(int tag, string[] items, ICallback error, ICallback success) { EnqueueOperation(() => _nativeViewHierarchyManager.ShowPopupMenu(tag, items, success)); } @@ -134,7 +134,7 @@ public void EnqueueCreateView( public void EnqueueUpdateProperties(int tag, string className, CatalystStylesDiffMap properties) { EnqueueOperation(() => - _nativeViewHierarchyManager.UpdateProperties(tag, className, properties)); + _nativeViewHierarchyManager.UpdateProperties(tag, properties)); } internal void EnqueueManageChildren( diff --git a/ReactWindows/ReactNative/UIManager/ViewAtIndex.cs b/ReactWindows/ReactNative/UIManager/ViewAtIndex.cs index ff2cd19d91a..17519156b0d 100644 --- a/ReactWindows/ReactNative/UIManager/ViewAtIndex.cs +++ b/ReactWindows/ReactNative/UIManager/ViewAtIndex.cs @@ -2,19 +2,36 @@ namespace ReactNative.UIManager { - class ViewAtIndex + /// + /// A data structure for holding tags and indices. + /// + public class ViewAtIndex { + /// + /// Instantiates the . + /// + /// The tag. + /// The index. public ViewAtIndex(int tag, int index) { Tag = tag; Index = index; } - public static IComparer Comparer { get; } = + /// + /// A comparer for instances to sort by index. + /// + public static IComparer IndexComparer { get; } = Comparer.Create((x, y) => x.Index - y.Index); + /// + /// The index of the view. + /// public int Index { get; } + /// + /// The tag of the view. + /// public int Tag { get; } } } diff --git a/ReactWindows/ReactNative/UIManager/ViewGroupManager.cs b/ReactWindows/ReactNative/UIManager/ViewGroupManager.cs index 38d774d56f0..605f1804077 100644 --- a/ReactWindows/ReactNative/UIManager/ViewGroupManager.cs +++ b/ReactWindows/ReactNative/UIManager/ViewGroupManager.cs @@ -82,9 +82,9 @@ public int GetChildCount(Panel parent) /// The parent view. /// The index. /// The child view. - public UIElement GetChildAt(Panel parent, int index) + public FrameworkElement GetChildAt(Panel parent, int index) { - return parent.Children[index]; + return (FrameworkElement)parent.Children[index]; } /// diff --git a/ReactWindows/ReactNative/UIManager/ViewManager.cs b/ReactWindows/ReactNative/UIManager/ViewManager.cs index c5e18045934..552cbf03b66 100644 --- a/ReactWindows/ReactNative/UIManager/ViewManager.cs +++ b/ReactWindows/ReactNative/UIManager/ViewManager.cs @@ -95,7 +95,7 @@ public void UpdateProperties(FrameworkElement viewToUpdate, CatalystStylesDiffMa /// The context. /// The responder handler. /// The view. - public TFrameworkElement CreateView( + public FrameworkElement CreateView( ThemedReactContext reactContext, JavaScriptResponderHandler jsResponderHandler) { @@ -120,7 +120,7 @@ public TFrameworkElement CreateView( /// /// Derived classes do not need to call this base method. /// - public virtual void OnDropViewInstance(ThemedReactContext reactContext, TFrameworkElement view) + public virtual void OnDropViewInstance(ThemedReactContext reactContext, FrameworkElement view) { } @@ -154,7 +154,7 @@ public virtual void OnDropViewInstance(ThemedReactContext reactContext, TFramewo /// /// Identifer for the command. /// Optional arguments for the command. - public abstract void ReceiveCommand(TFrameworkElement root, int commandId, JArray args); + public abstract void ReceiveCommand(FrameworkElement root, int commandId, JArray args); /// /// Creates a new view instance of type .