Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.Sample.ViewModels.Views;
#if WINDOWS || MACCATALYST
using CommunityToolkit.Maui.Markup;
using CommunityToolkit.Maui.Sample.Constants;
using CommunityToolkit.Maui.Sample.ViewModels.Views;
using CommunityToolkit.Maui.Views;
#else
using CommunityToolkit.Maui.Markup;
#endif

namespace CommunityToolkit.Maui.Sample.Pages.Views;

public partial class MediaElementMultipleWindowsPage : BasePage<MediaElementMultipleWindowsViewModel>
{
#if WINDOWS || MACCATALYST
readonly Window secondWindow;
#endif
const string buckBunnyMp4Url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
const string elephantsDreamMp4Url = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4";
readonly Window? secondWindow;
Copy link
Preview

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secondWindow is declared but never initialized in the constructor, so OnAppearing will always return early. Reintroduce its assignment using your new URL constants.

Copilot uses AI. Check for mistakes.



public MediaElementMultipleWindowsPage(MediaElementMultipleWindowsViewModel viewModel) : base(viewModel)
{
#if WINDOWS || MACCATALYST
secondWindow = new Window(new ContentPage
{
Content = new MediaElement
Expand All @@ -34,18 +31,15 @@ public MediaElementMultipleWindowsPage(MediaElementMultipleWindowsViewModel view
Source = StreamingVideoUrls.BuckBunny,
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,22 @@
<string>bluetooth-central</string>
<string>audio</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>__MAUI_DEFAULT_SCENE_CONFIGURATION__</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -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
{
}
17 changes: 17 additions & 0 deletions samples/CommunityToolkit.Maui.Sample/Platforms/iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,22 @@
<array>
<string>audio</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>__MAUI_DEFAULT_SCENE_CONFIGURATION__</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -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
{
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using AVKit;
using CommunityToolkit.Maui.Extensions;
using CommunityToolkit.Maui.Views;
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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<TItemsView> GetInternalControllerForItemsView<TItemsView>(ItemsViewHandler<TItemsView> handler) where TItemsView : ItemsView
{
var nonPublicInstanceFields = typeof(ItemsViewHandler<TItemsView>).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController<TItemsView>));
return (ItemsViewController<TItemsView>)(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<TItemsView> GetInternalControllerForItemsView2<TItemsView>(ItemsViewHandler2<TItemsView> handler) where TItemsView : ItemsView
{
var nonPublicInstanceFields = typeof(ItemsViewHandler2<TItemsView>).GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

var controllerProperty = nonPublicInstanceFields.Single(x => x.FieldType == typeof(ItemsViewController2<TItemsView>));
return (ItemsViewController<TItemsView>)(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);
Comment on lines +66 to +68
Copy link
Preview

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unconditionally overriding viewController here can set it to null if no matching item is found, losing the fallback to the root view controller. Wrap this assignment in a check (e.g. only override if the query returns a non-null controller).

Suggested change
viewController = itemsView?.Where(item => item.Handler is not null)
.Select(item => GetUIViewController(item.Handler))
.FirstOrDefault(viewController => viewController is not null);
var potentialViewController = itemsView?.Where(item => item.Handler is not null)
.Select(item => GetUIViewController(item.Handler))
.FirstOrDefault(viewController => viewController is not null);
if (potentialViewController is not null)
{
viewController = potentialViewController;
}

Copilot uses AI. Check for mistakes.

}


if (viewController?.View is not null)
{
// Zero out the safe area insets of the AVPlayerViewController
Expand All @@ -114,27 +82,40 @@ static ItemsViewController<TItemsView> GetInternalControllerForItemsView2<TItems
AddSubview(playerViewController.View);
}

static bool TryGetItemsViewOnPage(Page currentPage, [NotNullWhen(true)] out ItemsView? itemsView)
static UIViewController? GetUIViewController(IViewHandler? handler)
{
var itemsViewsOnPage = ((IElementController)currentPage).Descendants().OfType<ItemsView>().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<Page> currentPage, out List<ItemsView> 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 = [];
Copy link
Preview

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The out parameter itemsView is initialized but never populated with the discovered items. You should assign the local itemsViewsOnPage list to itemsView (e.g. itemsView = itemsViewsOnPage;).

Copilot uses AI. Check for mistakes.

List<ItemsView> itemsViewsOnPage = [];
currentPage.Where(page => page.Handler?.PlatformView is View)
.SelectMany(page => ((IElementController)page).Descendants().OfType<ItemsView>())
.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<Page> currentPage)
{
currentPage = null;
currentPage = [];

if (Application.Current?.Windows is null)
{
Expand All @@ -148,31 +129,39 @@ 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<Page>();
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];

// 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.LastOrDefault() is Page page)
{
currentPage = page;
currentPage.Add(page);
return true;
}

Expand All @@ -183,5 +172,5 @@ static bool TryGetModalPage(Window window, [NotNullWhen(true)] out Page? page)
page = window.Navigation.ModalStack.LastOrDefault();
return page is not null;
}
}
}
}
Loading