From e50775037ad45e42a9407088db7a8a949482efef Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Mon, 22 Apr 2019 16:44:12 -0600 Subject: [PATCH] [UWP] Allow embedding Forms page in secondary window (#5658) * Make secondary window work in UWP (fixes #2229) * Update Xamarin.Forms.Core/Internals/Ticker.cs Co-Authored-By: hartez --- .../SecondaryWindowService.cs | 49 +++++++ ...rms.ControlGallery.WindowsUniversal.csproj | 1 + Xamarin.Forms.Controls/CoreGallery.cs | 10 ++ .../ISecondaryWindowService.cs | 13 ++ Xamarin.Forms.Core/Internals/Ticker.cs | 19 ++- Xamarin.Forms.Platform.UAP/Forms.cs | 2 +- Xamarin.Forms.Platform.UAP/Platform.cs | 9 +- .../WindowsBasePlatformServices.cs | 120 +++++++++++++++++- Xamarin.Forms.Platform.UAP/WindowsTicker.cs | 24 +++- 9 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs create mode 100644 Xamarin.Forms.Controls/ISecondaryWindowService.cs diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs b/Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs new file mode 100644 index 000000000..58e826ac7 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.WindowsUniversal/SecondaryWindowService.cs @@ -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); + } + } +} diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj index dfd055d7c..dccc968b8 100644 --- a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj +++ b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj @@ -124,6 +124,7 @@ + diff --git a/Xamarin.Forms.Controls/CoreGallery.cs b/Xamarin.Forms.Controls/CoreGallery.cs index 493e8372a..885948166 100644 --- a/Xamarin.Forms.Controls/CoreGallery.cs +++ b/Xamarin.Forms.Controls/CoreGallery.cs @@ -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(); + 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 index 000000000..b39b55e9c --- /dev/null +++ b/Xamarin.Forms.Controls/ISecondaryWindowService.cs @@ -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); + } +} diff --git a/Xamarin.Forms.Core/Internals/Ticker.cs b/Xamarin.Forms.Core/Internals/Ticker.cs index fd183668c..0f65993fc 100644 --- a/Xamarin.Forms.Core/Internals/Ticker.cs +++ b/Xamarin.Forms.Core/Internals/Ticker.cs @@ -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 timeout) @@ -123,4 +138,4 @@ namespace Xamarin.Forms.Internals EnableTimer(); } } -} \ No newline at end of file +} diff --git a/Xamarin.Forms.Platform.UAP/Forms.cs b/Xamarin.Forms.Platform.UAP/Forms.cs index c87c9980d..1133bd071 100644 --- a/Xamarin.Forms.Platform.UAP/Forms.cs +++ b/Xamarin.Forms.Platform.UAP/Forms.cs @@ -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") diff --git a/Xamarin.Forms.Platform.UAP/Platform.cs b/Xamarin.Forms.Platform.UAP/Platform.cs index 33ab1593e..5323e153c 100644 --- a/Xamarin.Forms.Platform.UAP/Platform.cs +++ b/Xamarin.Forms.Platform.UAP/Platform.cs @@ -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; diff --git a/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs b/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs index 45df084f2..8ee13eff4 100644 --- a/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs +++ b/Xamarin.Forms.Platform.UAP/WindowsBasePlatformServices.cs @@ -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 TryDispatch(CoreDispatcher dispatcher, Action action) + { + if (dispatcher == null) + { + throw new ArgumentNullException(nameof(dispatcher)); + } + + var taskCompletionSource = new TaskCompletionSource(); + + 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 diff --git a/Xamarin.Forms.Platform.UAP/WindowsTicker.cs b/Xamarin.Forms.Platform.UAP/WindowsTicker.cs index f41e9acf9..0614058f4 100644 --- a/Xamarin.Forms.Platform.UAP/WindowsTicker.cs +++ b/Xamarin.Forms.Platform.UAP/WindowsTicker.cs @@ -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