adospace / reactorui-maui

MauiReactor is a MVU UI framework built on top of .NET MAUI
MIT License
599 stars 50 forks source link

Question / Help: Scaffold custom dialog (The49.Maui.BottomSheet) #253

Closed mario-zelger closed 1 month ago

mario-zelger commented 1 month ago

Hi @adospace

This might be out of scope as it relates to a 3rd party library.

I'm currently trying to integrate a BottomSheet with the help of The49.Maui.BottomSheet. To achieve this, I used your input from the documentation via How to deal with custom dialogs/popups?.

The code I've implemented for the scaffolding / wrapping part looks like the following.

Bottom Sheet Scaffold / Wrapper

```csharp [Scaffold(typeof(The49.Maui.BottomSheet.BottomSheet))] public partial class BottomSheet { protected override void OnAddChild(VisualNode widget, MauiControls.BindableObject childNativeControl) { if (childNativeControl is MauiControls.View content) { NativeControl.EnsureNotNull(); NativeControl.Content = content; } base.OnAddChild(widget, childNativeControl); } protected override void OnRemoveChild(VisualNode widget, MauiControls.BindableObject childNativeControl) { NativeControl.EnsureNotNull(); if (childNativeControl is MauiControls.View content && NativeControl.Content == content) { NativeControl.Content = null; } base.OnRemoveChild(widget, childNativeControl); } } public class BottomSheetHost : Component { private bool _isShown; private Action? _onDismissedAction; private The49.Maui.BottomSheet.BottomSheet? _bottomSheet; private readonly Action? _nativePopupCreateAction; public BottomSheetHost(Action? nativePopupCreateAction = null) { _nativePopupCreateAction = nativePopupCreateAction; } public BottomSheetHost IsShown(bool isShown) { _isShown = isShown; return this; } public BottomSheetHost OnDismissed(Action action) { _onDismissedAction = action; return this; } protected override void OnMounted() { InitializeBottomSheet(); base.OnMounted(); } protected override void OnPropsChanged() { InitializeBottomSheet(); base.OnPropsChanged(); } private void InitializeBottomSheet() { if (!_isShown || MauiControls.Application.Current == null) { return; } MauiControls.Application.Current.Dispatcher.DispatchAsync(async () => { if (ContainerPage == null || _bottomSheet is null) { return; } await _bottomSheet.ShowAsync(); }); } public override VisualNode Render() { if (!_isShown) { return null!; } var children = Children(); return new BottomSheet(r => { _bottomSheet = r; _nativePopupCreateAction?.Invoke(r); }) { children[0] } .Detents([ new MediumDetent() ]) .SelectedDetent(new MediumDetent()) .OnDismissed((_, origin) => _onDismissedAction?.Invoke(origin)); } } ```

Additionaly, here are parts of the app code I've used.

App Code

**MauiProgram.cs** ```csharp public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiReactorApp() .UseBottomSheet() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); return builder.Build(); } } ``` **MainPage.cs** ```csharp public class MainPage : Component { public override VisualNode Render() => NavigationPage(new BottomSheetPage()); } ``` **BottomSheetPage.cs** ```csharp public class BottomSheetPage : Component { private The49.Maui.BottomSheet.BottomSheet? _bottomSheet; public override VisualNode Render() { return new ContentPage { new Grid { new Button("Show Bottom Sheet") .HCenter() .VCenter() .OnClicked(() => ShowBottomSheet()), new BottomSheetHost(r => _bottomSheet = r) { new VStack(spacing: 10) { new Label("Hello from Bottom Sheet!"), new Button("Close", async () => await _bottomSheet!.DismissAsync()) .HCenter() .VCenter() } } .IsShown(State.IsShown) .OnDismissed(result => { Console.WriteLine(result); SetState(s => s.IsShown = false); }) } }; } private void ShowBottomSheet() { if (State.IsShown) { return; } SetState(s => s.IsShown = true); } } ```

I now run into the problem that the .NET MAUI handlers from the BottomSheet implementation are being called during the Render() phase. At this stage certain properties are still null which the handler expects to be non-null. For example in this method the Controller is null because ShowAsync has not yet been called.

public static void MapBackground(BottomSheetHandler handler, BottomSheet view)
{
    // The Controller property should not be null.
    view.Controller.UpdateBackground();
}

This leads to NullReferenceExceptions which can prevent the rendering of the content inside the BottomSheet.

In a classic MVVM application the samples show that a custom BottomSheet is instantiated in a Command and then directly displayed via ShowAsync.

var page = new SimplePage(); // SimplePage inherits from BottomSheet / is a BottomSheet
page.ShowAsync(Window);

This way the mentioned Controller property is never null as the controller is created inside the ShowAsyncmethod which happens before the component is acually rendered to the page (see here).

Is there a way to achieve the same behavior with MAUI Reactor?

Thanks a lot for your help!

adospace commented 1 month ago

Hi, nothing wrong with your approach that works most of the time but the BottomSheet is a "special" control that works best using an imperative code.

This is why I've created a repo for that (that repo contains implementations for a variety of 3rd party controls): https://github.com/adospace/mauireactor-integration/blob/main/The49/Pages/BottomSheetManager.cs

I have used BottomSheet in some of my applications. this is one of them: https://github.com/adospace/mauireactor-samples/tree/main/TaskApp

https://www.youtube.com/watch?v=q-oM2PO0ZtU&ab_channel=AdolfoMarinucci

mario-zelger commented 1 month ago

You're amazing! Thank you very much!