xamarin / Xamarin.Forms

Xamarin.Forms is no longer supported. Migrate your apps to .NET MAUI.
https://aka.ms/xamarin-upgrade
Other
5.62k stars 1.87k forks source link

[Enhancement] Popup Control #1778

Closed hartez closed 4 years ago

hartez commented 6 years ago

Rationale

Forms users would like to have the option of defining and displaying Popup controls. These are controls which are modal, can host arbitrary content, allow for complex user interaction, can return a response from the user, and are expressed (by default) using the appropriate controls on each platform:

Requirements

Popup display must be asynchronous.

Popups must be effectively modal; i.e., only one may be displayed at any given time.

Popups must be light-dismissable where the platform allows for it (e.g., on iPad, they should be light-dismissable; on iPhone this is not an option on all versions).

Users must be able to define their own custom Popups with arbitrary return value types.

Users must be able to host the following in a Popup:

This will effectively mean that Popups must provide for hosting VisualElement; for VisualElement types which are not View, any type other than ContentPage or NavigationPage will throw a NotSupportedException.

Popups must allow for (optionally) specifying an "anchor" View; behavior regarding the anchor will be platform-specific:

Popups must allow for a Size specification. If no Size is specified, the Popup will size to its content.

Forms should define a convenience Popup subclass for returning a 1-bit response (i.e., a yes/no, true/false, ok/cancel). In addition to providing convenience for users dealing with this common requirement, it will also serve as an example for users who need to implement other custom scenarios.

Forms should define a convenience Popup subclass for returning (effectively) a void value (in order to easily support users wanting to display simple content with no user response).

API Changes

The INavigation interface will add the following method:

Task<T> ShowPopupAsync<T>(Popup<T> popup);

The following interface will be added to Core:

public interface IPopup<out T>
{
    VisualElement Element { get; }

    void SetDismissDelegate(Action<T> dismissDelegate);
}

The Dismiss delegate is invoked when the Popup is dismissed.

The following classes will be added to Core:

public abstract class Popup<T>

This will be the base class from which all Popups will be built and will provide the core properties and functionality.

public struct PopupDismissed{}
public class Popup : Popup<PopupDismissed>

This is a convenience implementation for users who simply need to display content without receiving a user response.

public class BinaryResultPopup : Popup<BinaryResult>
public enum BinaryResult
{
    Negative,
    Affirmative
}

This is a convenience implementation for users who simply need a yes/no type of response.

Other Notes

A proof-of-concept implementation of an API very similar to this one (iOS-only) exists at https://github.com/xamarin/Xamarin.Forms/tree/popover

Difficulty: High

The basic implementation on iOS should be relatively easy; most of it can be taken from the PoC branch. UWP should also be fairly simple, as Flyout already provides most of what's required. Android implementation may be a bit trickier because of the requirement to position the Popup near the anchor control.

The most difficult aspect is the requirement to display a NavigationPage as content; the current Navigation model may need to be adjusted to accommodate this.

rogihee commented 6 years ago

Some inspiration perhaps: https://github.com/rotorgames/Rg.Plugins.Popup

andreinitescu commented 6 years ago

@hartez Any particular issue or roadblock with the Popup implementation started on that branch? Or it wasn't just enough time to completed yet?

hartez commented 6 years ago

@andreinitescu Just never enough time/demand to finish it. I think the branch is missing NavigationPage support, but otherwise is pretty close to the final spec (the big difference is that the branch calls them Popovers instead of Popups).

The branch has a bunch of demo code in the Control Gallery.

boguslawblonski commented 6 years ago

I aslo used Rg.Plugins.Popup for that

velocitysystems commented 6 years ago

@hartez We have implemented this as a DependencyService already on all three platforms (iOS, Android and UWP). Happy to look at contributing a PR.

samhouts commented 6 years ago

@mattrichnz We'd love to see it! Thanks!

jcapellman commented 6 years ago

Please include Mac in this as well, if it would help I can detail out how that would work.

mattregul commented 6 years ago

I'd love to see this added!

SkyeHoefling commented 5 years ago

I have taken this spec and implemented in my current project for Android and UWP. I am planning an iOS implementation but haven't finished it yet. While working on the spec I have a few items I would like to change that will make the Popup implementation easier to use than documented.

My Branch:

I am presenting here to get community feedback and see if these changes would be considered if I submit a Pull Request

Reasoning

The original proposal is really flexbile and is built with sub-classing in-mind. However, I found the original proposal missing details that made the UWP implementation very difficult. The original Popup workflow diverges from the standard ContentPage and ContentView implementations which made it difficult to use.

For my project I decided to alter the spec which greatly simplified my usage. I prefer to use MVVM frameworks when developing Xamarin.Forms apps and the current spec will not work with the new Prism Dialog Spec which is what I wanted to use this in. My altered proposal will work because of the addition of event handling. This also means any other MVVM framework that implements a Dialog or Popup abstraction will be able to work with the new spec.

New Proposal

The new proposal below is code pulled from my working fork that includes some detailed XML comments

INavigation

New Navigation methods

interface INavigation
{
    Task ShowPopupAsync(Popup popup);
    Task<T> ShowPopupAsync<T>(Popup popup); 
}

Popup

New popup class which utilizes object instead of generic types, this allows us to sub-class just like ContentPage and ContentView

public class Popup : View
{
    /// <summary>
    /// Gets or sets the <see cref="Content"/> to render in the Popup.
    /// </summary>
    /// <remarks>
    /// The View can be or type: <see cref="Content"/>, <see cref="ContentPage"/> or <see cref="NavigationPage"/>
    /// </remarks>
    public virtual View Content { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="Color"/> of the Popup.
    /// </summary>
    /// <remarks>
    /// This color sets the native background color of the <see cref="Popup"/>, which is
    /// independent of any background color configured in the actual View.
    /// </remarks>
    public Color Color { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="Color"/> of the Popup Border.
    /// </summary>
    /// <remarks>
    /// This color sets the native border color of the <see cref="Popup"/>, which is
    /// independent of any border color configured in the actual view.
    /// </remarks>
    public Color BorderColor { get; set; } // UWP ONLY - wasn't originally in spec

    /// <summary>
    /// Gets or sets the <see cref="Content"/> anchor.
    /// </summary>
    /// <remarks>
    /// The Anchor is where the Popup will render closest to. When an Anchor is configured
    /// the popup will appear centered over that control or as close as possible.
    /// </remarks>
    public View Anchor { get; set; }

    /// <summary>
    /// Gets or sets the <see cref="Size"/> of the Popup Display. 
    /// </summary>
    /// <remarks>
    /// The Popup will always try to constrain the actual size of the <see cref="Popup" />
    /// to the <see cref="Popup" /> of the View unless a <see cref="Size"/> is specified.
    /// If the <see cref="Popup" /> contiains <see cref="LayoutOptions"/> a <see cref="Size"/>
    /// will be required. This will allow the View to have a concept of <see cref="Size"/>
    /// that varies from the actual <see cref="Size"/> of the <see cref="Popup" />
    /// </remarks>
    public Size Size { get; set; }

    /// <summary>
    /// Gets or sets if the popup can be dismissed by tapping in
    /// the background mask.
    /// </summary>
    /// <remarks>
    /// Typically this is a black or grey mask on the outside of the
    /// popup display depending on the platform.
    /// </remarks>
    public bool IsLightDismissEnabled { get; set; }

    /// <summary>
    /// Dismissed event fired when the popup is dismissed. 
    /// </summary>
    /// <remarks>
    /// Check the <see cref="PopupDismissedEventArgs"/> which
    /// includes if the popup was dismissed by the light background
    /// mask.
    /// </remarks>
    public event EventHandler<PopupDismissedEventArgs> Dismissed;

    /// <summary>
    /// Manually dismiss the popup with a result
    /// </summary>
    public void Dismiss(object result);
}

PopupDismissedEventArgs

There is now an event that is being used and I needed to create a custom EventArgs to track the following properties:

public class PopupDismissedEventArgs : EventArgs
{
    /// <summary>
    /// Gets or sets the popup dismissed result
    /// </summary>
    public object Result { get; set; }

    /// <summary>
    /// Gets or sets if the event was fired from light dismissed.
    /// </summary>
    public bool IsLightDismissed { get; set; }
}

Usage

With the new API defined an implementation can use existing Xamarin.Forms techniques to create the Popup just like ContentPage and ContentView.

<Popup xmlns="http://xamarin.com/schemas/2014/forms" 
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:d="http://xamarin.com/schemas/2014/forms/design"
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       mc:Ignorable="d"
       x:Class="DemoApp.Popups.MyPopup">

    <Popup.Content>
        <Label 
            Text="Hello from Popup in Xamarin.Forms"
            VerticalOptions="CenterAndExpand"
            HorizontalOptions="CenterAndExpand" />
    </Popup.Content>

</Popup>

Consider the following code behind for a ContentPage that uses the Popup we just defined.

Simple Usage (ignore result)

When creating a simple alert style Popup where the result doesn't matter at all.

public class MainPage : ContentPage
{
    public async void PopupAwaitIgnoreResult()
    {
        var popup = new MyPopup();
        await Navigation.ShowPopupAsync(popup);
    }
}

Task Awaitable Result

When creating a popup that you await for the result and then continue execution of the method.

public class MainPage : ContentPage
{   
    public async void PopupWithAwaitResult()
    {
        var popup = new MyPopup();
        var isOkay = await Navigation.ShowPopupAsync<bool>(popup);

        if (isOkay)
        {
            // invoke okay logic
        }
        else
        {
            // invoke not okay logic
        }
    }
}

Event Result

When creating a popup and you want to listen for the Dismissed event. This allows the code to invoke the popup and have the callback executed in a different part of their code.

public class MainPage : ContentPage
{   
    public void PopupWithEventCallback()
    {
        var popup = new MyPopup();
        popup.Dismissed += OnDismissed;
        Navigation.ShowPopupAsync(popup);

        void OnDismissed(object sender, PopupDismissedEventArgs e)
        {
            var isOkay = (bool)e.Result;
            if (isOkay)
            {
                // invoke okay logic
            }
            else
            {
                // invoke not okay logic
            }
        }
    }
}

Screenshots

I have this working in the following platforms at the moment:

I am still working on an iOS implementation but once that is completed I can submit a pull request.

Below are some gifs of what this looks like on the different platforms

Android

popup_example_layoutoptions

UWP

popup_example_uwp

pranshu-aggarwal commented 5 years ago

Amazing work @ahoefling!! I implemented android version on my project and it is working fine. Can we also extend to consider RelativePosition like in Syncfusion popup control: https://help.syncfusion.com/xamarin/sfpopuplayout/popup-positioning

pranshu-aggarwal commented 5 years ago

@ahoefling I think some work need to done on popup size (at least in Android). I am not able to create popup that can extend horizontally.

pranshu-aggarwal commented 5 years ago

I think we may also need to think about LifeCycle event so that user can add their own animation.

PureWeen commented 5 years ago

Related https://github.com/xamarin/Xamarin.Forms/issues/3937

SkyeHoefling commented 5 years ago

I've been thinking a lot about this as I haven't posted an update in a long time. I am hoping to get back to my work on this to submit my PR in the beginning of next year. I have a project that is going to require me to finish the iOS and UWP portions

charlesroddie commented 4 years ago

@hartez Users must be able to host the following in a Popup: View ContentPage NavigationPage

This will effectively mean that Popups must provide for hosting VisualElement; for VisualElement types which are not View, any type other than ContentPage or NavigationPage will throw a NotSupportedException. The most difficult aspect is the requirement to display a NavigationPage as content; the current Navigation model may need to be adjusted to accommodate this.

Better to simplify so only a View is allowed as doing it for only some subtypes of VisualElement is difficult and breaks the type system as you describe.

SkyeHoefling commented 4 years ago

@samhouts I see some new tags, is anyone from Xamarin starting to look at this? I updated my branch the other day and am hoping to make a PR soon that will give us all something concrete to look at.

samhouts commented 4 years ago

@ahoefling No, we're just doing some backlog grooming at the moment. We'd love to receive your PR! Thanks!

SkyeHoefling commented 4 years ago

Thanks for clarifying that

charlesroddie commented 4 years ago

We are happy with Rg.Plugins.Popup, especially now that it supports UWP/Android/iOS/Mac.

This issue needs to define a significant improvement over Rg.Plugins.Popup, or else it's needless duplication of effort.

ChaseMarsh commented 4 years ago

In response to dotnet/maui#76

Showing a view arbitrarily above the view hierarchy has always been one of my personal "most wanted". As a XF user since its release and someone trying to create richer user experience in an environment where third-party code is frowned upon, I want to voice my support!

To help...I'm not familiar with all of the popup implementations out there, but would like to describe our implementation to see if we've done anything NEW. Would love some feedback!

P.S. Can't post the code ATM, but if this implementation is of interest, I can go through the appropriate channels.

IeuanWalker commented 4 years ago

@charlesroddie if the creator of Rg.Popup is happy then the work he has done could be used to implement it into XF.

I believe that's how a lot of the Xamarin.essentials features where created (could be wrong).

Could be used as an opportunity to update the code and add more features.

SkyeHoefling commented 4 years ago

Today I spoke with @PureWeen @jsuarezruiz and @jfversluis about this and agreed that my PR #9616 is best suited for the Xamarin Community Toolkit. I have created a new specification that will ultimately be a port of the Xamarin.Forms PR that was submitted in Feb 2020.

I believe this issue should be closed out since we have a new one to track work in the community toolkit