dotnet / MobileBlazorBindings

Experimental Mobile Blazor Bindings - Build native and hybrid mobile apps with Blazor
MIT License
1.2k stars 152 forks source link

Enable gesture recognizers for controls that don't allow children #180

Open Eilon opened 4 years ago

Eilon commented 4 years ago

Today gesture recognizers are components that are used by nesting them as children of the component to which they apply. For example:

        <Label Text="Tap me or pan me"
               FontSize="40" BackgroundColor="Color.LightCoral">
            <TapGestureRecognizer NumberOfTapsRequired="1" OnTapped="@OnLabelTapped" />
            <PanGestureRecognizer OnPanUpdated="@OnLabelPanned" />
        </Label>

But some controls don't allow any children at all, such as <Image ... />. If you try to place a gesture recognizer control (or anything else) inside it, you get a Razor compilation error indicating that the control doesn't allow inner content. This is a fundamental design of Razor: Either the control allows children, or it doesn't.

Some ideas on how to support this:

  1. Have all controls all support children by changing the base class of all controls. Some controls will throw a runtime error if unsupported children are added. For example, you can't add a Button control as a child of an Image, but you can add gesture recognizers. This might cause conflicts for controls that today have a "natural" way of supporting child content and by having a global one-size-fits-all solution, this could make some common scenarios less friendly.

  2. Have all controls that should support some kind of children add support for them. For example, all Views support gesture recognizers, so have just all Views support children. This has the same problem as the previous suggestion, but it will affect fewer controls.

  3. In addition to what is currently supported, also support using @ref to associate gesture recgonizers with their targets. Something like this:

        <Label @ref="gestureEnabledLabel" Text="Tap me or pan me"
               FontSize="40" BackgroundColor="Color.LightCoral" />
        <TapGestureRecognizer Target="@gestureEnabledLabel" NumberOfTapsRequired="1" OnTapped="@OnLabelTapped" />
        <PanGestureRecognizer Target="@gestureEnabledLabel" OnPanUpdated="@OnLabelPanned" />
    
    @code
    {
        Microsoft.MobileBlazorBindings.Elements.Label gestureEnabledLabel;
    }

    This is quite verbose but should otherwise always work. Would need to figure out what happens if this is in a for() loop to see what happens in Blazor with @ref but presumably it can be made to work.

  4. Have a GestureRecognizers property on all supported controls that accepts a list of recognizers. This was supported in an early PR for this but was scrubbed in favor of the current approach. See here: https://github.com/xamarin/MobileBlazorBindings/pull/166#issuecomment-670783123 The main con is that gesture recognizers aren't easy to create programmatically so either we need to build APIs to make it easier to use, or get changes to the recognizers themselves.

  5. Don't add anything new. Use OnAfterRenderAsync or similar to get the .NativeControl and add the recognizers manually.

Dreamescaper commented 3 years ago

But some controls don't allow any children at all, such as <Image ... />. If you try to place a gesture recognizer control (or anything else) inside it, you get a Razor compilation error indicating that the control doesn't allow inner content. This is a fundamental design of Razor: Either the control allows children, or it doesn't.

That's not what I see. I have runtime exception, not compile time:

System.InvalidOperationException: 'Object of type 'Microsoft.MobileBlazorBindings.Elements.Image' does not have a property matching the name 'ChildContent'.'

So I don't think there's any downside to allowing children to all controls.

Another option would be to define GestureRecognizers property of RenderFragment type, so it would look something like that:

        <Label Text="Tap me or pan me"
               FontSize="40" BackgroundColor="Color.LightCoral">
            <GestureRecognizers>
                <TapGestureRecognizer NumberOfTapsRequired="1" OnTapped="@OnLabelTapped" />
                <PanGestureRecognizer OnPanUpdated="@OnLabelPanned" />
            </GestureRecognizers>
        </Label>

But that will require MBB core changes to allow to render separate RenderFragments, not sure how easy would that be. Besides, it's probably a little verbose, considering that in most cases only one recognizer is used.

Eilon commented 3 years ago

I think the problem of allowing child content for all controls is that it prevents controls from having custom inner contents. For example, a CollectionView control (which doesn't exist in MBB right now) might want an <ItemTemplate> inner property for templating, a <SeparatorTemplate>, a <HeaderTemplate>, etc. But if every MBB control automatically has ChildContent then that isn't possible.

Eilon commented 3 years ago

BTW I'm not saying we wouldn't do it, just that it isn't an easy decision to make because of the consequences.

Dreamescaper commented 3 years ago

But if every MBB control automatically has ChildContent then that isn't possible.

Why?

E.g.

Component ```csharp public class ComponentWithTemplates: ComponentBase { [Parameter] public RenderFragment HeaderTemplate { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } } ```
Usage ```razor ```
Eilon commented 3 years ago

Oh OK I didn't realize you could explicitly say <ChildContent> if there are other children... that does change things! Maybe this is far more promising than I had considered!