From 4d9cd35d9d412989f426e66fc948c09f614f5d81 Mon Sep 17 00:00:00 2001 From: James Crutchley Date: Sun, 26 Jan 2025 18:01:08 -0800 Subject: [PATCH 1/3] Support multiple windows and improve maintainability - Replace platform-specific directives with runtime checks in MediaElementMultipleWindowsPage.cs - Change LaunchMode to Multiple in MainActivity.cs - Update Info.plist to support multiple scenes - Simplify UIViewController logic in MauiMediaElement.macios.cs - Update methods to handle multi-window applications in MauiMediaElement.macios.cs - Add SceneDelegate classes for iOS and Mac Catalyst --- .../MediaElementMultipleWindowsPage.cs | 29 ++--- .../Platforms/Android/MainActivity.cs | 2 +- .../Platforms/MacCatalyst/Info.plist | 17 +++ .../Platforms/MacCatalyst/SceneDelegate.cs | 12 ++ .../Platforms/iOS/Info.plist | 17 +++ .../Platforms/iOS/SceneDelegate.cs | 12 ++ .../Views/MauiMediaElement.macios.cs | 119 ++++++++---------- 7 files changed, 126 insertions(+), 82 deletions(-) create mode 100644 samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/SceneDelegate.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/Platforms/iOS/SceneDelegate.cs diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs index 98301cb8f3..23c8323c53 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs @@ -1,23 +1,23 @@ +using CommunityToolkit.Maui.Markup; using CommunityToolkit.Maui.Sample.ViewModels.Views; -#if WINDOWS || MACCATALYST using CommunityToolkit.Maui.Views; -#else -using CommunityToolkit.Maui.Markup; -#endif namespace CommunityToolkit.Maui.Sample.Pages.Views; public partial class MediaElementMultipleWindowsPage : BasePage { const string buckBunnyMp4Url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; -#if WINDOWS || MACCATALYST const string elephantsDreamMp4Url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"; - readonly Window secondWindow; -#endif - + readonly Window? secondWindow; public MediaElementMultipleWindowsPage(MediaElementMultipleWindowsViewModel viewModel) : base(viewModel) { -#if WINDOWS || MACCATALYST + if(DeviceInfo.Current.Idiom == DeviceIdiom.Phone && DeviceInfo.Current.Platform == DevicePlatform.iOS) + { + Content = new Label() + .Text("This sample is only testable on MacCatalyst and Windows") + .TextCenter(); + return; + } secondWindow = new Window(new ContentPage { Content = new MediaElement @@ -32,18 +32,15 @@ public MediaElementMultipleWindowsPage(MediaElementMultipleWindowsViewModel view Source = buckBunnyMp4Url, ShouldAutoPlay = true }; -#else - Content = new Label() - .Text("This sample is only testable on MacCatalyst and Windows") - .TextCenter(); -#endif } protected override void OnAppearing() { base.OnAppearing(); -#if WINDOWS || MACCATALYST + if(secondWindow is null) + { + return; + } Application.Current?.OpenWindow(secondWindow); -#endif } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/Android/MainActivity.cs b/samples/CommunityToolkit.Maui.Sample/Platforms/Android/MainActivity.cs index 246d9e7a3f..d8547ded1c 100644 --- a/samples/CommunityToolkit.Maui.Sample/Platforms/Android/MainActivity.cs +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/Android/MainActivity.cs @@ -3,7 +3,7 @@ namespace CommunityToolkit.Maui.Sample; -[Activity(Theme = "@style/Maui.SplashTheme", ResizeableActivity = true, MainLauncher = true, LaunchMode = LaunchMode.SingleTask, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] +[Activity(Theme = "@style/Maui.SplashTheme", ResizeableActivity = true, MainLauncher = true, LaunchMode = LaunchMode.Multiple, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] public class MainActivity : MauiAppCompatActivity { diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/Info.plist b/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/Info.plist index c342a6fa06..a1aa61ef70 100644 --- a/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/Info.plist +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/Info.plist @@ -39,5 +39,22 @@ bluetooth-central audio + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + __MAUI_DEFAULT_SCENE_CONFIGURATION__ + UISceneDelegateClassName + SceneDelegate + + + + diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/SceneDelegate.cs b/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/SceneDelegate.cs new file mode 100644 index 0000000000..051eaf832e --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/MacCatalyst/SceneDelegate.cs @@ -0,0 +1,12 @@ +using System; +using Foundation; +using Microsoft.Maui; +using ObjCRuntime; +using UIKit; + +namespace CommunityToolkit.Maui.Sample.Platforms.MacCatalyst; + +[Register("SceneDelegate")] +public class SceneDelegate : MauiUISceneDelegate +{ +} diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/Info.plist b/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/Info.plist index d0245bf261..d55aee2cfe 100644 --- a/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/Info.plist +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/Info.plist @@ -40,5 +40,22 @@ audio + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + __MAUI_DEFAULT_SCENE_CONFIGURATION__ + UISceneDelegateClassName + SceneDelegate + + + + diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/SceneDelegate.cs b/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/SceneDelegate.cs new file mode 100644 index 0000000000..802eca8766 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/iOS/SceneDelegate.cs @@ -0,0 +1,12 @@ +using System; +using Foundation; +using Microsoft.Maui; +using ObjCRuntime; +using UIKit; + +namespace CommunityToolkit.Maui.Sample.Platforms.iOS; + +[Register("SceneDelegate")] +public class SceneDelegate : MauiUISceneDelegate +{ +} diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.macios.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.macios.cs index c3777325a0..ad83c396fd 100644 --- a/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.macios.cs +++ b/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.macios.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Reflection; using AVKit; using CommunityToolkit.Maui.Extensions; using CommunityToolkit.Maui.Views; @@ -28,7 +27,7 @@ public MauiMediaElement(AVPlayerViewController playerViewController, MediaElemen #if IOS16_0_OR_GREATER || MACCATALYST16_1_OR_GREATER // On iOS 16+ and macOS 13+ the AVPlayerViewController has to be added to a parent ViewController, otherwise the transport controls won't be displayed. - UIViewController? viewController; + UIViewController? viewController = null; // If any of the Parents in the VisualTree of MediaElement uses a UIViewController for their PlatformView, use it as the child ViewController // This enables support for UI controls like CommunityToolkit.Maui.Popup whose PlatformView is a UIViewController (e.g. `public class MauiPopup : UIViewController`) @@ -58,48 +57,17 @@ public MauiMediaElement(AVPlayerViewController playerViewController, MediaElemen } // look for an ItemsView (e.g. CarouselView or CollectionView) on page - if (TryGetItemsViewOnPage(currentPage, out var itemsView)) - { - var parentViewController = itemsView.Handler switch - { - CarouselViewHandler carouselViewHandler => carouselViewHandler.ViewController ?? GetInternalControllerForItemsView(carouselViewHandler), - CarouselViewHandler2 carouselViewHandler2 => carouselViewHandler2.ViewController ?? GetInternalControllerForItemsView2(carouselViewHandler2), - CollectionViewHandler collectionViewHandler => collectionViewHandler.ViewController ?? GetInternalControllerForItemsView(collectionViewHandler), - CollectionViewHandler2 collectionViewHandler2 => collectionViewHandler2.ViewController ?? GetInternalControllerForItemsView2(collectionViewHandler2), - null => throw new InvalidOperationException("Handler cannot be null"), - _ => throw new NotSupportedException($"{itemsView.Handler.GetType()} not yet supported") - }; - - viewController = parentViewController; - - // The Controller we need is a `protected internal` property called ItemsViewController in the ItemsViewHandler class: https://github.com/dotnet/maui/blob/cf002538cb73db4bf187a51e4786d7478a7025ee/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.iOS.cs#L39 - // In this method, we must use reflection to get the value of its backing field - static ItemsViewController GetInternalControllerForItemsView(ItemsViewHandler handler) where TItemsView : ItemsView - { - var nonPublicInstanceFields = typeof(ItemsViewHandler).GetFields(BindingFlags.NonPublic | BindingFlags.Instance); - - var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController)); - return (ItemsViewController)(controllerProperty.GetValue(handler) ?? throw new InvalidOperationException($"Unable to get the value for the Controller property on {handler.GetType()}")); - } - - // The Controller we need is a `protected internal` property called ItemsViewController in the ItemsViewHandler2 class: https://github.com/dotnet/maui/blob/70e8ddfd4bd494bc71aa7afb812cc09161cf0c72/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.iOS.cs#L64 - // In this method, we must use reflection to get the value of its backing field - static ItemsViewController GetInternalControllerForItemsView2(ItemsViewHandler2 handler) where TItemsView : ItemsView - { - var nonPublicInstanceFields = typeof(ItemsViewHandler2).GetFields(BindingFlags.NonPublic | BindingFlags.Instance); - - var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController2)); - return (ItemsViewController)(controllerProperty.GetValue(handler) ?? throw new InvalidOperationException($"Unable to get the value for the Controller property on {handler.GetType()}")); - } - } - // If we don't find an ItemsView, default to the current UIViewController - else - { - viewController = Platform.GetCurrentUIViewController(); - } + TryGetItemsViewOnPage(currentPage, out var itemsView); + + // Set the viewController to the first root view controller. + viewController = Platform.GetCurrentUIViewController(); + + // Check to see if there is a ItemsView in a collection view or CarouselView and replace Shell Renderer with the correct handler + viewController = itemsView?.Where(item => item.Handler is not null) + .Select(item => GetUIViewController(item.Handler)) + .FirstOrDefault(viewController => viewController is not null); } - if (viewController?.View is not null) { // Zero out the safe area insets of the AVPlayerViewController @@ -114,27 +82,40 @@ static ItemsViewController GetInternalControllerForItemsView2().ToList(); - switch (itemsViewsOnPage.Count) + return handler switch { - case > 1: - // We are unable to determine which ItemsView contains the MediaElement when multiple ItemsView are being used in the same page - // TODO: Add support for MediaElement in an ItemsView on a Page containing multiple ItemsViews - throw new NotSupportedException("MediaElement does not currently support pages containing multiple ItemsViews (eg multiple CarouselViews + CollectionViews)"); - case 1: - itemsView = itemsViewsOnPage[0]; - return true; - case <= 0: - itemsView = null; - return false; - } + CarouselViewHandler carouselViewHandler => carouselViewHandler.ViewController, + CarouselViewHandler2 carouselViewHandler2 => carouselViewHandler2.ViewController, + CollectionViewHandler collectionViewHandler => collectionViewHandler.ViewController, + CollectionViewHandler2 collectionViewHandler2 => collectionViewHandler2.ViewController, + null => throw new InvalidOperationException("Handler cannot be null"), + _ => throw new NotSupportedException($"{handler.GetType()} not yet supported") + }; + } + static void TryGetItemsViewOnPage(List currentPage, out List itemsView) + { + // We are looking for an ItemsView (e.g. CarouselView or CollectionView) on page. + // To retrieve its CarouselViewHandler / CollectionViewHandler, we must traverse all VisualElements on the current page/ + // We check if Handler.PlatformView is a View to ensure we are looking at a VisualElement. If not, we continue to the next VisualElement. + // We need to check both the page and ItemsView to ensure we are looking at the correct VisualElement. + // Checking both the page and ItemsView is necessary because the ItemsView may be nested inside another VisualElement. + // We may be using Multi-window support, so we need to traverse all Windows to find the current page in a multi-window application. + // We then check if the VisualElement is an ItemsView (e.g. CarouselView or CollectionView) and add it to the itemsView list/ + + itemsView = []; + List itemsViewsOnPage = []; + currentPage.Where(page => page.Handler?.PlatformView is View) + .SelectMany(page => ((IElementController)page).Descendants().OfType()) + .Where(item => item.Handler?.PlatformView is View) + .ToList() + .ForEach(item => itemsViewsOnPage.Add(item)); } - static bool TryGetCurrentPage([NotNullWhen(true)] out Page? currentPage) + static bool TryGetCurrentPage([NotNullWhen(true)] out List currentPage) { - currentPage = null; + currentPage = []; if (Application.Current?.Windows is null) { @@ -148,9 +129,17 @@ static bool TryGetCurrentPage([NotNullWhen(true)] out Page? currentPage) if (Application.Current.Windows.Count > 1) { - // We are unable to determine which Window contains the ItemsView that contains the MediaElement when multiple ItemsView are being used in the same page - // TODO: Add support for MediaElement in an ItemsView in a multi-window application - throw new NotSupportedException("MediaElement is not currently supported in multi-window applications"); + // We traverse all Windows to find the current page in a multi-window application + // We check if the Window contains a Page and add it to the currentPage list + // If the Page is null we continue to the next Window + // We then return the currentPage list + var pages = new List(); + var list = Application.Current.Windows.ToList(); + pages.AddRange(from item in list + where item.Page is not null + select item.Page); + currentPage = pages; + return true; } var window = Application.Current.Windows[0]; @@ -158,21 +147,21 @@ static bool TryGetCurrentPage([NotNullWhen(true)] out Page? currentPage) // If using Shell, return the current page if (window.Page is Shell { CurrentPage: not null } shell) { - currentPage = shell.CurrentPage; + currentPage.Add(shell.CurrentPage); return true; } // If not using Shell, use the ModelNavigationStack to check for any pages displayed modally if (TryGetModalPage(window, out var modalPage)) { - currentPage = modalPage; + currentPage.Add(modalPage); return true; } // If not using Shell or a Modal Page, return the visible page in the (non-modal) NavigationStack if (window.Navigation.NavigationStack[^1] is Page page) { - currentPage = page; + currentPage.Add(page); return true; } @@ -183,5 +172,5 @@ static bool TryGetModalPage(Window window, [NotNullWhen(true)] out Page? page) page = window.Navigation.ModalStack.LastOrDefault(); return page is not null; } - } + } } \ No newline at end of file From 0313e757bc36af44fb33c7480ba1a7ffb31fb512 Mon Sep 17 00:00:00 2001 From: James Crutchley Date: Wed, 9 Jul 2025 08:24:10 -0700 Subject: [PATCH 2/3] Refactor MediaElementMultipleWindowsPage for flexibility Added a using directive for CommunityToolkit.Maui.Sample.Constants. Removed the preprocessor directive for the secondWindow variable, replacing it with a nullable Window? type to allow for more flexible handling across platforms. --- .../Views/MediaElement/MediaElementMultipleWindowsPage.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs index 2a81fcf43f..82c39d58ae 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.Markup; +using CommunityToolkit.Maui.Sample.Constants; using CommunityToolkit.Maui.Sample.ViewModels.Views; using CommunityToolkit.Maui.Views; @@ -10,9 +11,7 @@ public partial class MediaElementMultipleWindowsPage : BasePage Date: Wed, 9 Jul 2025 08:40:22 -0700 Subject: [PATCH 3/3] Remove Device check from MediaElement Multiple Window Page --- .../Views/MediaElement/MediaElementMultipleWindowsPage.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs index 82c39d58ae..a13ebf73b0 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementMultipleWindowsPage.cs @@ -15,13 +15,6 @@ public partial class MediaElementMultipleWindowsPage : BasePage