[UWP] Allow embedding Forms page in secondary window (#5658)
authorE.Z. Hart <hartez@users.noreply.github.com>
Mon, 22 Apr 2019 22:44:12 +0000 (16:44 -0600)
committerSamantha Houts <samhouts@users.noreply.github.com>
Mon, 22 Apr 2019 22:44:12 +0000 (15:44 -0700)
* Make secondary window work in UWP (fixes #2229)

* Update Xamarin.Forms.Core/Internals/Ticker.cs

Co-Authored-By: hartez <hartez@users.noreply.github.com>
Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs [new file with mode: 0644]
Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj
Xamarin.Forms.Controls/CoreGallery.cs
Xamarin.Forms.Controls/ISecondaryWindowService.cs [new file with mode: 0644]
Xamarin.Forms.Core/Internals/Ticker.cs
Xamarin.Forms.Platform.UAP/Forms.cs
Xamarin.Forms.Platform.UAP/Platform.cs
Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs
Xamarin.Forms.Platform.UAP/WindowsTicker.cs

diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs b/Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs
new file mode 100644 (file)
index 0000000..58e826a
--- /dev/null
@@ -0,0 +1,49 @@
+using System;
+using System.Threading.Tasks;
+using Windows.ApplicationModel.Core;
+using Windows.UI.Core;
+using Windows.UI.ViewManagement;
+using Windows.UI.Xaml;
+using Xamarin.Forms;
+using Xamarin.Forms.ControlGallery.WindowsUniversal;
+using Xamarin.Forms.Controls;
+using Xamarin.Forms.Platform.UWP;
+
+[assembly: Dependency(typeof(SecondaryWindowService))]
+namespace Xamarin.Forms.ControlGallery.WindowsUniversal
+{
+       class SecondaryWindowService : ISecondaryWindowService
+       {
+               public async Task OpenSecondaryWindow(Type pageType)
+               {
+                       CoreApplicationView newView = CoreApplication.CreateNewView();
+                       int newViewId = 0;
+                       await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
+                       {
+                               var frame = new Windows.UI.Xaml.Controls.Frame();
+                               frame.Navigate(pageType);
+                               Window.Current.Content = frame;
+                               Window.Current.Activate();
+
+                               newViewId = ApplicationView.GetForCurrentView().Id;
+                       });
+                       bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
+               }
+
+               public async Task OpenSecondaryWindow(ContentPage page)
+               {
+                       CoreApplicationView newView = CoreApplication.CreateNewView();
+                       int newViewId = 0;
+                       await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
+                       {
+                               var frame = new Windows.UI.Xaml.Controls.Frame();
+                               frame.Navigate(page);
+                               Window.Current.Content = frame;
+                               Window.Current.Activate();
+
+                               newViewId = ApplicationView.GetForCurrentView().Id;
+                       });
+                       bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
+               }
+       }
+}
index dfd055d..dccc968 100644 (file)
     <Compile Include="PlatformSpecificCoreGalleryFactory.cs" />
     <Compile Include="RegistrarValidationService.cs" />
     <Compile Include="SampleNativeControl.cs" />
+    <Compile Include="SecondaryWindowService.cs" />
     <Compile Include="_2489CustomRenderer.cs" />
     <Compile Include="_57114Renderer.cs" />
     <Compile Include="_58406EffectRenderer.cs" />
index 493e837..8859481 100644 (file)
@@ -10,6 +10,8 @@ using Xamarin.Forms.PlatformConfiguration;
 using Xamarin.Forms.PlatformConfiguration.AndroidSpecific;
 using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
 using Xamarin.Forms.Controls.GalleryPages.VisualStateManagerGalleries;
+using Xamarin.Forms.Controls.Issues;
+
 namespace Xamarin.Forms.Controls
 {
        [Preserve(AllMembers = true)]
@@ -562,6 +564,14 @@ namespace Xamarin.Forms.Controls
                                }
                        };
 
+                       var secondaryWindowService = DependencyService.Get<ISecondaryWindowService>();
+                       if (secondaryWindowService != null)
+                       {
+                               var openSecondWindowButton = new Button() { Text = "Open Secondary Window" };
+                               openSecondWindowButton.Clicked += (obj, args) => { secondaryWindowService.OpenSecondaryWindow(new Issue2482()); };
+                               stackLayout.Children.Add(openSecondWindowButton);
+                       }
+
                        this.SetAutomationPropertiesName("Gallery");
                        this.SetAutomationPropertiesHelpText("Lists all gallery pages");
 
diff --git a/Xamarin.Forms.Controls/ISecondaryWindowService.cs b/Xamarin.Forms.Controls/ISecondaryWindowService.cs
new file mode 100644 (file)
index 0000000..b39b55e
--- /dev/null
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Xamarin.Forms.Controls
+{
+       public interface ISecondaryWindowService
+       {
+               Task OpenSecondaryWindow(Type pageType);
+               Task OpenSecondaryWindow(ContentPage page);
+       }
+}
index fd18366..0f65993 100644 (file)
@@ -42,7 +42,22 @@ namespace Xamarin.Forms.Internals
                public static Ticker Default
                {
                        internal set { s_ticker = value; }
-                       get { return s_ticker ?? (s_ticker =  Device.PlatformServices.CreateTicker()); }
+                       get
+                       {
+                               if (s_ticker == null)
+                               {
+                                       s_ticker = Device.PlatformServices.CreateTicker();
+                               }
+
+                               return s_ticker.GetTickerInstance(); 
+                       }
+               }
+
+               protected virtual Ticker GetTickerInstance()
+               {
+                       // This method is provided so platforms can override it and return something other than
+                       // the normal Ticker singleton
+                       return s_ticker;
                }
 
                public virtual int Insert(Func<long, bool> timeout)
@@ -123,4 +138,4 @@ namespace Xamarin.Forms.Internals
                        EnableTimer();
                }
        }
-}
\ No newline at end of file
+}
index c87c998..1133bd0 100644 (file)
@@ -83,7 +83,7 @@ namespace Xamarin.Forms
                        return FlowDirection.MatchParent;
                }
 
-               static Windows.UI.Xaml.ResourceDictionary GetTabletResources()
+               internal static Windows.UI.Xaml.ResourceDictionary GetTabletResources()
                {
                        return new Windows.UI.Xaml.ResourceDictionary {
                                Source = new Uri("ms-appx:///Xamarin.Forms.Platform.UAP/Resources.xbf")
index 33ab159..5323e15 100644 (file)
@@ -67,9 +67,16 @@ namespace Xamarin.Forms.Platform.UWP
 
                        _page = page;
 
+                       var current = Windows.UI.Xaml.Application.Current;
+
+                       if (!current.Resources.ContainsKey("RootContainerStyle"))
+                       {
+                               Windows.UI.Xaml.Application.Current.Resources.MergedDictionaries.Add(Forms.GetTabletResources());
+                       }
+
                        _container = new Canvas
                        {
-                               Style = (Windows.UI.Xaml.Style)Windows.UI.Xaml.Application.Current.Resources["RootContainerStyle"]
+                               Style = (Windows.UI.Xaml.Style)current.Resources["RootContainerStyle"]
                        };
 
                        _page.Content = _container;
index 45df084..8ee13ef 100644 (file)
@@ -26,19 +26,24 @@ namespace Xamarin.Forms.Platform.UWP
 {
        internal abstract class WindowsBasePlatformServices : IPlatformServices
        {
+               const string WrongThreadError = "RPC_E_WRONG_THREAD";
                readonly CoreDispatcher _dispatcher;
 
                protected WindowsBasePlatformServices(CoreDispatcher dispatcher)
                {
-                       if (dispatcher == null)
-                               throw new ArgumentNullException(nameof(dispatcher));
-
-                       _dispatcher = dispatcher;
+                       _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
                }
 
-               public void BeginInvokeOnMainThread(Action action)
+               public async void BeginInvokeOnMainThread(Action action)
                {
-                       _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).WatchForError();
+                       if (CoreApplication.Views.Count == 1)
+                       {
+                               // This is the normal scenario - one window only
+                               _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).WatchForError();
+                               return;
+                       }
+
+                       await TryAllDispatchers(action);
                }
 
                public Ticker CreateTicker()
@@ -115,7 +120,23 @@ namespace Xamarin.Forms.Platform.UWP
                        return new WindowsIsolatedStorage(ApplicationData.Current.LocalFolder);
                }
 
-               public bool IsInvokeRequired => !_dispatcher.HasThreadAccess;
+               public bool IsInvokeRequired
+               {
+                       get
+                       {
+                               if (CoreApplication.Views.Count == 1)
+                               {
+                                       return !_dispatcher.HasThreadAccess;
+                               }
+
+                               if (Window.Current?.Dispatcher != null)
+                               {
+                                       return !Window.Current.Dispatcher.HasThreadAccess;
+                               }
+
+                               return true;
+                       }
+               }
 
                public string RuntimePlatform => Device.UWP;
 
@@ -152,5 +173,90 @@ namespace Xamarin.Forms.Platform.UWP
                {
                        return Platform.GetNativeSize(view, widthConstraint, heightConstraint);
                }
+
+               async Task TryAllDispatchers(Action action)
+               {
+                       // Our best bet is Window.Current; most of the time, that's the Dispatcher we need
+                       var currentWindow = Window.Current;
+
+                       if (currentWindow?.Dispatcher != null)
+                       {
+                               try
+                               {
+                                       await TryDispatch(currentWindow.Dispatcher, action);
+                                       return;
+                               }
+                               catch (Exception ex) when (ex.Message.Contains(WrongThreadError))
+                               {
+                                       // The current window is not the one we need 
+                               }
+                       }
+
+                       // Either Window.Current was the wrong Dispatcher, or Window.Current was null because we're on a 
+                       // non-UI thread (e.g., one from the thread pool). So now it's time to try all the available Dispatchers 
+
+                       var views = CoreApplication.Views;
+
+                       for (int n = 0; n < views.Count; n++)
+                       {
+                               var dispatcher = views[n].Dispatcher;
+
+                               if (dispatcher == null || dispatcher == currentWindow?.Dispatcher)
+                               {
+                                       // Obviously null Dispatchers are no good, and we already tried the one from currentWindow
+                                       continue;
+                               }
+
+                               // We need to ignore Deactivated/Never Activated windows, but it's possible we can't access their 
+                               // properties from this thread. So we'll check those using the Dispatcher
+                               bool activated = false;
+
+                               await TryDispatch(dispatcher, () => {
+                                       var mode = views[n].CoreWindow.ActivationMode;
+                                       activated = (mode == CoreWindowActivationMode.ActivatedInForeground
+                                               || mode == CoreWindowActivationMode.ActivatedNotForeground);
+                               });
+
+                               if (!activated)
+                               {
+                                       // This is a deactivated (or not yet activated) window; move on
+                                       continue;
+                               }
+
+                               try
+                               {
+                                       await TryDispatch(dispatcher, action);
+                                       return;
+                               }
+                               catch (Exception ex) when (ex.Message.Contains(WrongThreadError))
+                               {
+                                       // This was the incorrect dispatcher; move on to try another one
+                               }
+                       }
+               }
+
+               async Task<bool> TryDispatch(CoreDispatcher dispatcher, Action action)
+               {
+                       if (dispatcher == null)
+                       {
+                               throw new ArgumentNullException(nameof(dispatcher));
+                       }
+
+                       var taskCompletionSource = new TaskCompletionSource<bool>();
+
+                       await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
+                               try
+                               {
+                                       action();
+                                       taskCompletionSource.SetResult(true);
+                               }
+                               catch (Exception ex)
+                               {
+                                       taskCompletionSource.SetException(ex);
+                               }
+                       });
+
+                       return await taskCompletionSource.Task;
+               }
        }
 }
\ No newline at end of file
index f41e9ac..0614058 100644 (file)
@@ -1,10 +1,15 @@
-using Windows.UI.Xaml.Media;
+using Windows.ApplicationModel.Core;
+using System;
+using Windows.UI.Xaml.Media;
 using Xamarin.Forms.Internals;
 
 namespace Xamarin.Forms.Platform.UWP
 {
        internal class WindowsTicker : Ticker
        {
+               [ThreadStatic]
+               static Ticker s_ticker;
+
                protected override void DisableTimer()
                {
                        CompositionTarget.Rendering -= RenderingFrameEventHandler;
@@ -19,5 +24,22 @@ namespace Xamarin.Forms.Platform.UWP
                {
                        SendSignals();
                }
+
+               protected override Ticker GetTickerInstance()
+               {
+                       if (CoreApplication.Views.Count > 1)
+                       {
+                               // We've got multiple windows open, we'll need to use the local ThreadStatic Ticker instead of the 
+                               // singleton in the base class 
+                               if (s_ticker == null)
+                               {
+                                       s_ticker = new WindowsTicker();
+                               }
+
+                               return s_ticker;
+                       }
+
+                       return base.GetTickerInstance();
+               }
        }
 }
\ No newline at end of file