Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the timing of Measure method call when SwipeControl is included #2087

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

cat0363
Copy link
Contributor

@cat0363 cat0363 commented Aug 2, 2024

In this PR, we will fix it so that SwipeView will work if it is included under the Popup's Content on Windows.

Description of Change

The Measure method is called when measuring the Popup size, but if SwipeView is included, an AccessViolationException will occur on Windows. The .NET MAUI SwipeView control becomes SwipeControl on the platform side, but if the Measure method of SwipeControl is called when the IsLoaded property is false, an AccessViolationException will occur.

If the Popup's content includes SwipeControl, you need to modify it so that the Measure method is called when the SwipeControl's IsLoaded property is true.

This PR was created based on the idea of ​​.NET MAUI PR #6220.

Below are the changes.

The code below adds and removes the SwipeControl's Loaded event when the SwipeControl is included in the popup's content.

[src\CommunityToolkit.Maui.Core\Handlers\Popup\PopUpHandler.windows.cs]

	protected override void ConnectHandler(Popup platformView)
	{
		platformView.Closed += OnClosed;
		platformView.ConfigureControl(VirtualView, MauiContext);
		if (MauiContext is not null)
		{
			MauiContext.GetPlatformWindow().SizeChanged += OnSizeChanged;
		}
		AttachSwipeControlEvent(platformView);
		base.ConnectHandler(platformView);
	}

	protected override void DisconnectHandler(Popup platformView)
	{
		if (VirtualView.Parent is null)
		{
			return;
		}

		ArgumentNullException.ThrowIfNull(VirtualView.Handler?.MauiContext);
		var parent = VirtualView.Parent.ToPlatform(VirtualView.Handler.MauiContext);
		parent.IsHitTestVisible = true;
		platformView.IsOpen = false;
		platformView.Closed -= OnClosed;
		if (MauiContext is not null)
		{
			MauiContext.GetPlatformWindow().SizeChanged -= OnSizeChanged;
		}
		DetachSwipeControlEvent(platformView);
	}

	void AttachSwipeControlEvent(Popup platformView)
	{
		if (platformView.Child is FrameworkElement frameworkElement)
		{
			if (frameworkElement is SwipeControl)
			{
				SwipeControl swipeControl = (SwipeControl)frameworkElement;
				swipeControl.Loaded += OnLoadedSwipeControl;
			}
			else
			{
				var swipeControls = frameworkElement.GetChildren<SwipeControl>();
				foreach (SwipeControl? swipeControl in swipeControls)
				{
					if (swipeControl is not null)
					{
						swipeControl.Loaded += OnLoadedSwipeControl;
					}
				}
			}
		}
	}

	void DetachSwipeControlEvent(Popup platformView)
	{
		if (platformView.Child is FrameworkElement frameworkElement)
		{
			if (frameworkElement is SwipeControl)
			{
				SwipeControl swipeControl = (SwipeControl)frameworkElement;
				swipeControl.Loaded -= OnLoadedSwipeControl;
			}
			else
			{
				var swipeControls = frameworkElement.GetChildren<SwipeControl>();
				foreach (SwipeControl? swipeControl in swipeControls)
				{
					if (swipeControl is not null)
					{
						swipeControl.Loaded -= OnLoadedSwipeControl;
					}
				}
			}
		}
	}

	void OnLoadedSwipeControl(object? sender, RoutedEventArgs e)
	{
		if (VirtualView is not null)
		{
			PopupExtensions.SetSize(PlatformView, VirtualView, MauiContext);
			PopupExtensions.SetLayout(PlatformView, VirtualView, MauiContext);
		}
	}

The code below prevents the Measure method from being called if the Popup's Content includes SwipeControl and the SwipeControl's IsLoaded property is false. If the Popup content does not include SwipeControl, call the Measure method.

[src\CommunityToolkit.Maui.Core\Views\Popup\PopupExtensions.windows.cs]

	static bool CanMeasureContent(Popup mauiPopup)
	{
		if (mauiPopup.Child is FrameworkElement frameworkElement)
		{
			if (frameworkElement is SwipeControl)
			{
				SwipeControl swipeControl = (SwipeControl)frameworkElement;
				if (!swipeControl.IsLoaded)
				{
					return false;
				}
			}
			else
			{
				var swipeControls = frameworkElement.GetChildren<SwipeControl>();
				foreach (SwipeControl? swipeControl in swipeControls)
				{
					if (swipeControl is not null && !swipeControl.IsLoaded)
					{
						return false;
					}
				}
			}
		}
		return true;
	}

	public static void SetSize(this Popup mauiPopup, IPopup popup, IMauiContext? mauiContext)
	{
		ArgumentNullException.ThrowIfNull(mauiContext);
		ArgumentNullException.ThrowIfNull(popup.Content);

		const double defaultBorderThickness = 0;
		const double defaultSize = 600;

		var popupParent = mauiContext.GetPlatformWindow();
		var currentSize = new Size { Width = defaultSize, Height = defaultSize / 2 };

		if (popup.Size.IsZero)
		{
			if (double.IsNaN(popup.Content.Width) || (double.IsNaN(popup.Content.Height)))
			{
				if (CanMeasureContent(mauiPopup))
				{
					currentSize = popup.Content.Measure(double.IsNaN(popup.Content.Width) ? popupParent.Bounds.Width : popup.Content.Width, double.IsNaN(popup.Content.Height) ? popupParent.Bounds.Height : popup.Content.Height);
				}
				else
				{
					return;
				}

				if (double.IsNaN(popup.Content.Width))
				{
					currentSize.Width = popup.HorizontalOptions == LayoutAlignment.Fill ? popupParent.Bounds.Width : currentSize.Width;
				}
				if (double.IsNaN(popup.Content.Height))
				{
					currentSize.Height = popup.VerticalOptions == LayoutAlignment.Fill ? popupParent.Bounds.Height : currentSize.Height;
				}
			}
			else
			{
				currentSize.Width = popup.Content.Width;
				currentSize.Height = popup.Content.Height;
			}
		}
		else
		{
			currentSize.Width = popup.Size.Width;
			currentSize.Height = popup.Size.Height;
		}

                omit ... 
	}

	public static void SetLayout(this Popup mauiPopup, IPopup popup, IMauiContext? mauiContext)
	{
		ArgumentNullException.ThrowIfNull(mauiContext);
		ArgumentNullException.ThrowIfNull(popup.Content);

		var popupParent = mauiContext.GetPlatformWindow();

		if (CanMeasureContent(mauiPopup))
		{
			popup.Content.Measure(double.PositiveInfinity, double.PositiveInfinity);
		}
		else
		{
			return;
		}
 
                omit ...
	}

Linked Issues

PR Checklist

  • Has a linked Issue, and the Issue has been approved(bug) or Championed (feature/proposal)
  • Has tests (if omitted, state reason in description)
  • Has samples (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Changes adhere to coding standard
  • Documentation created or updated: https://github.com/MicrosoftDocs/CommunityToolkit/pulls

Additional information

Below are the verification results.

I used the following layout in the case where the Popup's content includes SwipeControl.

<?xml version="1.0" encoding="utf-8" ?>
<toolkit:Popup xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Class="swipeview_cv_crash.PopupTest">
    <VerticalStackLayout>
        <SwipeView Grid.Row="1">
            <SwipeView.LeftItems>
                <SwipeItems>
                    <SwipeItem Text="Delete" IconImageSource="dotnet_bot" BackgroundColor="LightPink"/>
                </SwipeItems>
            </SwipeView.LeftItems>
            <SwipeView.RightItems>
                <SwipeItems>
                    <SwipeItem Text="Favorite"  IconImageSource="dotnet_bot" BackgroundColor="LightGreen"/>
                    <SwipeItem Text="Share" IconImageSource="dotnet_bot" BackgroundColor="LightYellow"/>
                </SwipeItems>
            </SwipeView.RightItems>
            <Grid HeightRequest="60" WidthRequest="300" BackgroundColor="LightGray">
                <Label Text="Swipe" HorizontalOptions="Center" VerticalOptions="Center" />
            </Grid>
        </SwipeView>
    </VerticalStackLayout>
</toolkit:Popup>

The execution result is shown below.

2024-08-02.12-38-23.mp4

You can see that SwipeView is working. AccessViolationException is not occurring when displaying the Popup.
On Windows, SwipeView does not work with mouse interaction. It works with touch movements only.

dotnet/maui#6152 (comment)

Below are the verification results using the Community Toolkit sample app.
This is a case where the Popup content does not include SwipeControl.

Toolkit.Features.2024-08-02.12-57-06.mp4

You can see that the Popup is displayed at the intended size.

@dotnet-policy-service dotnet-policy-service bot added stale The author has not responded in over 30 days help wanted This proposal has been approved and is ready to be implemented labels Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted This proposal has been approved and is ready to be implemented stale The author has not responded in over 30 days
Projects
None yet
Development

Successfully merging this pull request may close these issues.

SwipeView inside Popup crashes on Windows with System.AccessViolationException
1 participant