Open agendamatic opened 2 years ago
@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
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.
@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
}
}
Please let us know if that helps anything!
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.
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.
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.
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.
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.
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.
@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!
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.
Hi, I need to override some methods of the
UIViewController
, which I am not able to achieve. How can we define a customUIViewController
with MAUI please ? I did similar as @agendamatic , settings theViewController
inConnectHandler
but my handler code is not called. It is registered withinConfigureMauiHandlers
, 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)}");
}
}
That was exactly what I've been looking for! Thanks!
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.
in android after tab not rendering too
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.
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
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:
What did i wrong? How can i fix this?
@PureWeen Any idea how to create a handler specific working for each page?
So, since 2022 no one has fixed this?
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:
Modify MauiProgram.cs as follows:
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