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

iOS PageRenderer compatibility not rendering page content #7174

Open
agendamatic opened this issue May 13, 2022 · 18 comments
Open

iOS PageRenderer compatibility not rendering page content #7174

agendamatic opened this issue May 13, 2022 · 18 comments
Assignees
Labels
area-compatibility Issues related to features in the Compatibility Nuget migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/iOS 🍎 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working

Comments

@agendamatic
Copy link

Description

In my MAUI iOS app, I need access to the UIViewController to access the NavigationItem. I ported my Xamarin Forms PageRenderer to my app and added it to the MauiAppBuilder using the AddCompatibilityRenderer() method.

At runtime, the renderer is instantiated and the ViewWillAppear overload executes, there is no page content rendered.

Steps to Reproduce

Create a new MAUI app. In App.xaml.cs modify as follows:

MainPage = new NavigationPage(new MainPage());

Modify MauiProgram.cs as follows:

builder.ConfigureMauiHandlers(handlers => {
#if __IOS__
        handlers.AddCompatibilityRenderer<Page, Microsoft.Maui.Controls.Compatibility.Platform.iOS.PageRenderer>();
#endif
});

Run it on an iOS simulator. There is no content rendered.

Comment out the AddCompatibilityRenderer statement and the content is rendered.

Version with bug

Release Candidate 3 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 154

Did you find any workaround?

No. I cannot access the NavigationController from the PageHandler either, so I'm pretty much dead in the water.

Relevant log output

No response

@agendamatic agendamatic added s/needs-verification Indicates that this issue needs initial verification before further triage will happen t/bug Something isn't working labels May 13, 2022
@PureWeen
Copy link
Member

@agendamatic can you try porting your code to a PageHandler?

The PageHandler has a ViewController property you can access.

And if you want to supply your own custom view you can do so via
PageHandler.PlatformViewFactory

Just return a view that inherits from the expected type

@DancesWithDingo
Copy link

I spent some hours trying to work with PageHandler, but I was unable to access the current NavigationItem. I decided to wait for documentation/examples.

In the meantime I tried to use the old renderer approach. That resulted in the discovery that PageRenderer is not working as expected. Thus I filed a bug report to point out that issue. Compatibility measures should work correctly.

@PureWeen
Copy link
Member

@agendamatic @DancesWithDingo

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        this.Loaded += MainPage_Loaded;
    }

    private void MainPage_Loaded(object sender, EventArgs e)
    {
#if IOS
        if (Handler is IPlatformViewHandler platformViewHandler)
        {
            var cv = platformViewHandler.ViewController;
            var navItem = cv.NavigationItem;
            System.Diagnostics.Debug.WriteLine($"{navItem}");
        }
#endif
    }
}

@jfversluis jfversluis added platform/iOS 🍎 area/migration 🚚 and removed s/needs-verification Indicates that this issue needs initial verification before further triage will happen labels May 16, 2022
@jfversluis
Copy link
Member

Please let us know if that helps anything!

@jfversluis jfversluis added the s/needs-info Issue needs more info from the author label May 16, 2022
@ghost
Copy link

ghost commented May 16, 2022

Hi @agendamatic. We have added the "s/needs-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

@agendamatic
Copy link
Author

agendamatic commented May 16, 2022

First, there no longer seems to be a Loaded event on ContentPage. I wired up the HandlerChanged event, and placed the following code in it:

    if ( Handler is IPlatformViewHandler platformViewHandler ) {
        var vc = platformViewHandler.ViewController;
        var navItem = vc.NavigationController?.NavigationItem;

        if ( navItem is null )
            navItem = vc.NavigationItem;

        if ( navItem is null ) {
            System.Diagnostics.Debug.WriteLine($"navItem -> null");
        } else {
            int buttonCount = navItem.RightBarButtonItems?.Length ?? 0;
            System.Diagnostics.Debug.WriteLine($"navItem -> Title:[{navItem.Title}], buttons: {buttonCount}");
        }
    }

Place a ToolbarButton in the ContentPage.ToolbarButtons collection:

<ContentPage.ToolbarItems>
    <ToolbarItem Text="Go!"/>
</ContentPage.ToolbarItems>

In App.xaml.cs, ensure you have this:

MainPage = new NavigationPage(new MainPage()) { Title = "Testing 1-2-3" };

At runtime, the ViewControlller's NavigationController is null, and the NavigationItem contains null for the title. The NavigationItem RightBarButtonItem property is null.

This is why I tried to fall back on the old PageRenderer technique (I had concluded that I did not yet understand PageHandler well enough). Within the PageRenderer I can access the NavigationItem as expected, However there appears to be a but in PageRenderer that prevents page content from being rendered.

Thus this issue was filed. For the PageRenderer issue.

@ghost ghost added s/needs-attention Issue has more information and needs another look and removed s/needs-info Issue needs more info from the author labels May 16, 2022
@agendamatic
Copy link
Author

I just realized that I had placed the title on the NavigationPage instead of the ContentPage. With that change, the NavigationItem correctly contains the title, however the RightBarButtonItem (and corresponding array) aren't populated.

@cwaldron
Copy link

cwaldron commented May 25, 2022

I'm using a Xamarin library that inherits the iOS PageRenderer and overrides ViewDidLoad/ViewDidUnload to perform some work. The new rendering scheme for Maui doesn't inherit UIViewController. How can code like this be migrated to Maui?

In the mean time I'm using the obsolete PageRenderer to port this code over.

@JohnHDev
Copy link

I would like to see an answer to @cwaldron 's question too pls, we have an iOS renderer that overrides ViewWillAppear to set some toolbar items to the left instead of right, but I don't see how that might work with handlers.

In the example from @PureWeen above, the navItem RightBarButtonItems and LeftBarButtonItems are empty, so either that Loaded event is too early for what we need and they haven't been populated yet, or we are now looking in the wrong place.

@agendamatic
Copy link
Author

The key is that you need to cast the PageHandler as IPlatformViewHandler to access the ViewController, as @PureWeen said. To get to the UINavigationItem, you need to look at the ParentViewController at just the right moment in the page's lifecycle. Unfortunately that moment exists somewhere between the page Appearing event and the Loaded event.

I have been able to approximate the functionality of overriding ViewWillAppear by using the following handler. In my apps I use a base class for each page called CorePage:

public abstract class CorePage : ContentPage { }

public class CorePageHandler : Microsoft.Maui.Handlers.PageHandler
{
    protected override void ConnectHandler(Microsoft.Maui.Platform.ContentView nativeView) {
        base.ConnectHandler(nativeView);
        CorePage.Loaded += OnLoaded;
    }

    protected override void DisconnectHandler(Microsoft.Maui.Platform.ContentView nativeView) {
        CorePage.Loaded -= OnLoaded;
        base.DisconnectHandler(nativeView);
    }

    CorePage CorePage => VirtualView as CorePage;

    void OnLoaded(object sender, EventArgs e) => ManageBarButtons();

    void ManageBarButtons() {
        if (this is IPlatformViewHandler handler
                && handler.ViewController?.ParentViewController?.NavigationItem is UINavigationItem navItem ) {
            List<UIBarButtonItem> rightBarButtons = new();
            List<UIBarButtonItem> leftBarButtons = new(navItem.LeftBarButtonItems ?? Array.Empty<UIBarButtonItem>());

            // manipulate buttons here

            navItem.RightBarButtonItems = rightBarButtons.ToArray();
            navItem.LeftBarButtonItems = leftBarButtons.ToArray();
        }
    }
}

Note that I say approximate, because there can be visual artifacts when manipulating the buttons during the ContentPage.Loaded event. My implementation replaces generated UIBarButtonItem objects with new ones using native UIBarButtonSystemItem and SF Symbols (this is iOS after all). I overcame the timing issue with a hack: I save/remove the ToolbarItem objects in ConnectHandler and add manually created UIBarButtonItem objects during the Loaded event. This approach works, but I'm not a fan.

For the PageRenderer functionality to ultimately be obsoleted, MAUI needs to implement access to the UIViewController during ViewWillAppear in a manner that not only we Xamarin Forms developers are accustomed to, but that is also easily discoverable by new programmers coming to .NET MAUI.

@JohnHDev
Copy link

JohnHDev commented Sep 12, 2022

@agendamatic ah thank you! The key I was missing is the reference to the ParentViewController, that gave me access to the right bar button items and then my XF renderer code could be moved across. I have this on a single page for now, will move it to a registered page handler later today.

EDIT: Haven't seen any artefacts yet but will keep my eyes peeled, thank you again!

@eli191
Copy link

eli191 commented Nov 28, 2022

Hi, I need to override some methods of the UIViewController, which I am not able to achieve.
How can we define a custom UIViewController with MAUI please ?
I did similar as @agendamatic , settings the ViewController in ConnectHandler but my handler code is not called. It is registered within ConfigureMauiHandlers, so it does exist.

@samhouts samhouts added the partner/cat 😻 this is an issue that impacts one of our partners or a customer our advisory team is engaged with label Mar 27, 2023
@samhouts samhouts added the p/1 Work that is important, and has been scheduled for release in this or an upcoming sprint label Apr 4, 2023
@PureWeen PureWeen self-assigned this Apr 12, 2023
@PureWeen PureWeen removed p/1 Work that is important, and has been scheduled for release in this or an upcoming sprint partner/cat 😻 this is an issue that impacts one of our partners or a customer our advisory team is engaged with s/needs-attention Issue has more information and needs another look labels Apr 12, 2023
@PureWeen
Copy link
Member

Hi, I need to override some methods of the UIViewController, which I am not able to achieve. How can we define a custom UIViewController with MAUI please ? I did similar as @agendamatic , settings the ViewController in ConnectHandler but my handler code is not called. It is registered within ConfigureMauiHandlers, so it does exist.

So what you'd do here is override CreatePlatformView and copy our code.
We should really have an override or something to make this easier.

This should work for you

	internal class WorkaroundPageHandler : PageHandler
	{
		class MyPageViewController : PageViewController
		{
			public MyPageViewController(IView page, IMauiContext mauiContext) : base(page, mauiContext)
			{
			}
		}

		protected override Microsoft.Maui.Platform.ContentView CreatePlatformView()
		{
			_ = VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} must be set to create a LayoutView");
			_ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} cannot be null");

			if (ViewController == null)
				ViewController = new MyPageViewController(VirtualView, MauiContext);

			if (ViewController is PageViewController pc && pc.CurrentPlatformView is Microsoft.Maui.Platform.ContentView pv)
				return pv;

			if (ViewController.View is Microsoft.Maui.Platform.ContentView cv)
				return cv;

			throw new InvalidOperationException($"PageViewController.View must be a {nameof(Microsoft.Maui.Platform.ContentView)}");
		}
	}

@PureWeen PureWeen removed their assignment Apr 12, 2023
@agendamatic
Copy link
Author

That was exactly what I've been looking for! Thanks!

@amittapelly11
Copy link

amittapelly11 commented Aug 2, 2023

I want to override the navigation bar back button behaviour using below code

public class BaseContentPage : ContentPage
{

    public void OnSoftBackButtonPressed()
    {
        Navigation.PopAsync();
    }
}

builder.ConfigureMauiHandlers( handlers => { handlers.AddHandler(typeof(BaseContentPage), typeof(BasePageHandler)); });

public class BasePageHandler : PageHandler
{
protected override void ConnectHandler(Microsoft.Maui.Platform.ContentView nativeView)
{
base.ConnectHandler(nativeView);
Basepage.Loaded += Loaded;
}

    private void Loaded(object sender, EventArgs e) => ChangeSoftwareBackButtonBehavior();

    private void ChangeSoftwareBackButtonBehavior()
    {
        if (this is IPlatformViewHandler handler)
        {
            var cv = handler.ViewController;
            var navigationController = cv.NavigationController;

            UIButton btn = new UIButton();
            btn.Frame = new CGRect(0, 0, 50, 40);
            btn.BackgroundColor = UIColor.Clear;

            btn.TouchDown += (sender, e) =>
            {
                // Whatever your custom back button click handling
                Basepage.OnSoftBackButtonPressed();
            };
          
            //var views = NavigationController?.NavigationBar.Subviews;
            navigationController?.NavigationBar.AddSubview(btn);
        }
    }

    protected override void DisconnectHandler(Microsoft.Maui.Platform.ContentView nativeView)
    {
        Basepage.Loaded -= Loaded;
        base.DisconnectHandler(nativeView);
    }


    BaseContentPage Basepage => VirtualView as BaseContentPage;

}

with the above code the page back button behavior is working as expected. But when I have clicked on the back of the navigation bar of previous page(Content Page), getting Unhandled Exception:
System.InvalidOperationException: VirtualView cannot be null here

Please help me to resolve this issue.

@samhouts samhouts added migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert and removed area/migration 🚚 labels Aug 28, 2023
@Zhanglirong-Winnie Zhanglirong-Winnie added the s/triaged Issue has been reviewed label Sep 14, 2023
@tjiakwokyung28
Copy link

in android after tab not rendering too

@PureWeen PureWeen added this to the Backlog milestone Dec 16, 2023
@ghost
Copy link

ghost commented Dec 16, 2023

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

@mnidhk
Copy link

mnidhk commented Apr 25, 2024

I changed my custom render from XF to a Handler for Maui. To put a searchbar in a navigation bar on iOS.
Custom Searchbar iOS.zip
Scherm­afbeelding 2024-04-25 om 12 09 39

Looked at to the code above. After a lot of error and trial i come up with this:

public class ModernContentPage : ContentPage
    {

        public static readonly BindableProperty SearchTextProperty = BindableProperty.Create(nameof(SearchText), typeof(string), typeof(ModernContentPage), string.Empty, BindingMode.TwoWay);
        public static readonly BindableProperty SearchCommandProperty = BindableProperty.Create(nameof(SearchCommand), typeof(ICommand), typeof(ModernContentPage), default(ICommand), BindingMode.OneWay);
        public static readonly BindableProperty SearchCommandParameterProperty = BindableProperty.Create(nameof(SearchCommand), typeof(object), typeof(ModernContentPage), null, BindingMode.OneWay);
        public static readonly BindableProperty SearchCancelledCommandProperty = BindableProperty.Create(nameof(SearchCancelledCommand), typeof(ICommand), typeof(ModernContentPage), default(ICommand), BindingMode.OneWay);
        public static readonly BindableProperty SearchPlaceholderProperty = BindableProperty.Create(nameof(SearchPlaceholder), typeof(string), typeof(ModernContentPage), string.Empty, BindingMode.OneWay);
        public static readonly BindableProperty IsSearchActiveProperty = BindableProperty.Create(nameof(IsSearchActive), typeof(bool), typeof(ModernContentPage), false, BindingMode.OneWayToSource);
        public static readonly BindableProperty IsSearchFocusedProperty = BindableProperty.Create(nameof(IsSearchFocused), typeof(bool), typeof(ModernContentPage), false, BindingMode.OneWayToSource);
        public static readonly BindableProperty ActionImageProperty = BindableProperty.Create(nameof(ActionImage), typeof(ImageSource), typeof(ModernContentPage), null, BindingMode.OneWay);
        public static readonly BindableProperty ActionCommandProperty = BindableProperty.Create(nameof(ActionCommand), typeof(ICommand), typeof(ModernContentPage), null, BindingMode.OneWay);

        

        public string SearchText 
        {
            get => (string)this.GetValue(SearchTextProperty);
            set => this.SetValue(SearchTextProperty, value);
        }

        public ICommand SearchCommand
        {
            get => (ICommand)this.GetValue(SearchCommandProperty);
            set => this.SetValue(SearchCommandProperty, value);
        }

        public object SearchCommandParameter
        {
            get => this.GetValue(SearchCommandParameterProperty);
            set => this.SetValue(SearchCommandParameterProperty, value);
        }

        public ICommand SearchCancelledCommand
        {
            get => (ICommand)this.GetValue(SearchCancelledCommandProperty);
            set => this.SetValue(SearchCancelledCommandProperty, value);
        }

        public string SearchPlaceholder
        {
            get => (string)this.GetValue(SearchPlaceholderProperty);
            set => this.SetValue(SearchPlaceholderProperty, value);
        }

        public bool IsSearchActive
        {
            get => (bool)this.GetValue(IsSearchActiveProperty);
            set => this.SetValue(IsSearchActiveProperty, value);
        }

        public bool IsSearchFocused
        {
            get => (bool)this.GetValue(IsSearchFocusedProperty);
            set => this.SetValue(IsSearchFocusedProperty, value);
        }

        public ImageSource ActionImage
        {
            get => (ImageSource)this.GetValue(ActionImageProperty);
            set => this.SetValue(ActionImageProperty, value);
        }

        public ICommand ActionCommand
        {
            get => (ICommand)this.GetValue(ActionCommandProperty);
            set => this.SetValue(ActionCommandProperty, value);
        }

        protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            if (propertyName == SearchTextProperty.PropertyName || propertyName == IsSearchFocusedProperty.PropertyName)
            {
                if (this.IsSearchFocused)
                {
                  this.IsSearchActive = true;
                }
                else if (!string.IsNullOrWhiteSpace(this.SearchText))
                {
                    this.IsSearchActive = true;
                }
                else
                {
                    this.IsSearchActive = false;
                }
            }
        }
    }

And the Handler :

public partial class ModernContentPageRender: PageHandler
   {

       static UISearchController searchController = new UISearchController(searchResultsController: null);


       public static IPropertyMapper<ModernContentPage, ModernContentPageRender> PropertyMapper = new PropertyMapper<ModernContentPage, ModernContentPageRender>(PageHandler.Mapper)
       {
           [nameof(ModernContentPage.SearchTextProperty)] = SearchTextProperty,
           [nameof(ModernContentPage.SearchPlaceholderProperty)] = SearchPlaceholderProperty,



       };

       public ModernContentPageRender() : base(PropertyMapper)
       {

       }

       static void SearchTextProperty(PageHandler handler, ModernContentPage ModernContentPage)
       {
           searchController.SearchBar.Text = ModernContentPage.SearchText;

       }

       static void SearchPlaceholderProperty(PageHandler handler, ModernContentPage ModernContentPage)
       {
           searchController.SearchBar.Placeholder = ModernContentPage.SearchPlaceholder;


           //************************************************************
           //************  Delegate actions searchbar   ******************
           //************************************************************

           ModernContentPage.PropertyChanged += (sender, e) =>
           {
               if (e.PropertyName== "SearchText")
                   searchController.SearchBar.Text = ModernContentPage.SearchText;
           };


           searchController.SearchBar.SearchButtonClicked += (sender, e) =>
           {
               if (ModernContentPage.SearchCommand != null && ModernContentPage.SearchCommand.CanExecute(searchController.SearchBar.Text))
               {
                   ModernContentPage.SearchCommand.Execute(searchController.SearchBar.Text);
               }
           };

           searchController.SearchBar.TextChanged += (sender, e) =>
           {
               ModernContentPage.SearchText = searchController.SearchBar.Text;
               if (string.IsNullOrEmpty(searchController.SearchBar.Text))
               {
                   if (ModernContentPage.SearchCancelledCommand != null && ModernContentPage.SearchCancelledCommand.CanExecute(null))
                   {
                       ModernContentPage.SearchCancelledCommand.Execute(null);
                   }
               }
           };

           searchController.SearchBar.CancelButtonClicked += (sender, e) =>
           {
               ModernContentPage.SearchText = searchController.SearchBar.Text= string.Empty;

               if (ModernContentPage.SearchCancelledCommand != null && ModernContentPage.SearchCancelledCommand.CanExecute(null))
               {
                   ModernContentPage.SearchCancelledCommand.Execute(null);
               }

           };

           searchController.SearchBar.OnEditingStarted += (sender, e) =>
           {
               ModernContentPage.IsSearchFocused = true;

           };

           searchController.SearchBar.OnEditingStopped += (sender, e) =>
           {
               ModernContentPage.IsSearchFocused = false;

           };

       }

      

       protected override void ConnectHandler(Microsoft.Maui.Platform.ContentView nativeView)
       {
           base.ConnectHandler(nativeView);
           ContentPage.Loaded += OnLoaded;
       }

       protected override void DisconnectHandler(Microsoft.Maui.Platform.ContentView nativeView)
       {
           ContentPage.Loaded -= OnLoaded;
           base.DisconnectHandler(nativeView);
       }

       ContentPage ContentPage => VirtualView as ContentPage;

       void OnLoaded(object sender, EventArgs e) => ConfigureInteractive();

       void ConfigureInteractive()
       {
           if (this is IPlatformViewHandler handler && handler.ViewController?.ParentViewController is UIKit.UIViewController navControler)
           {
               searchController.HidesNavigationBarDuringPresentation = true;
               searchController.ObscuresBackgroundDuringPresentation = false;
               searchController.DefinesPresentationContext = true;
                    
               searchController.SearchBar.SearchBarStyle = UISearchBarStyle.Default;
               searchController.SearchBar.Translucent = true;

               navControler.NavigationItem.SearchController = ModernContentPageRender.searchController;
               navControler.NavigationItem.HidesSearchBarWhenScrolling = false;
           } 
       }

       
   }

In several pages i use this handler
<local:ModernContentPage....

And in Mauiprogram.cs:
handlers.AddHandler(typeof(ModernContentPage), typeof(Pink_Template.ModernContentPageRender));

The searchbar is showing in the navigation bar of iOS. (ContentPage A) Everything is working. I can set the placeholder text, or a default text.
I can put in text and press Search on keyboard.
The right trigger is working

 searchController.SearchBar.SearchButtonClicked += (sender, e) =>
            {
                if (ModernContentPage.SearchCommand != null && ModernContentPage.SearchCommand.CanExecute(searchController.SearchBar.Text))
                {
                    ModernContentPage.SearchCommand.Execute(searchController.SearchBar.Text);
                }
            };

and my functions/commands initialized in my contentpage code are fired

SearchPlaceholder = App.Terms.TermString(TermID.Zoeken);
 SearchCancelledCommand = new Command(() => SearchBar_TextChanged(null, SearchText));
 SearchCommand = new Command(() => SearchBar_SearchButtonPressed(null, SearchText));

The problem is:

  • When i go to another page (ContentPage B), through my menu, with also this handler everything is showing.
  • But when i do the same, input text and click search then my function on ContentPage A is fired AND function on ContentPage B is fired.
  • Go to another page C en also A en B is fired . So the handler is not a standalone handler only for that page?

What did i wrong? How can i fix this?

@PureWeen Any idea how to create a handler specific working for each page?

@PureWeen PureWeen added the area-compatibility Issues related to features in the Compatibility Nuget label May 31, 2024
@PureWeen PureWeen self-assigned this May 31, 2024
@PureWeen PureWeen modified the milestones: Backlog, .NET 9 Planning May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-compatibility Issues related to features in the Compatibility Nuget migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/iOS 🍎 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests