Dropbox support for Xamarin.Forms

The new Dropbox v2 API makes it quite straightforward to add support for Dropbox into an app running on a phone or tablet. When I decided to add Dropbox support to Mobile DMX it quickly became obvious, looking at the “SimpleTest” sample app, that the awkward part is logging in, but uploading and downloading small files would then be very easy.

Given that, I decided to write a class to manage the login, and make it portable between different OSs, which means having some way of calling out to OS specific code to manage the login UI. Initially, I was going to use an event to do this, but for Xamarin.Forms it needs to be async, and it’s not easy to await on an async event, so I made it a callback instead, with sync and async versions. (They could instead be virtual methods, and require the class to be overridden).

The UI needs to display a web page, the Dropbox login page, and supply a redirect url. After the user has logged in, Dropbox will redirect the page to the redirect url, adding an Access Token parameter to it, which the app can then store and use instead of having to log in again. It’s obviously designed to work from a web app – the redirect url would normally be used to return to a page within the calling website. In an app, you need to display a web browser control, then trap when it returns to the redirect url, and grab the Access Token from it.

To the app, where the redirect url goes is irrelevant and it is just used to detect that the login routine has finished. To a web app, it obviously matters, which is why it has to be registered with Dropbox. If it is non-local, it has to be https, but for an app this is irrelevant and just any old uri can be used.

The idea behind my DropboxManager object is that it is given the information it needs to identify the app to Dropbox, the App Name (a string that gets displayed by the Dropbox login page), the App Key (a string supplied by Dropbox when the app is registered) and any existing Access Token (from a previous login). It then logs in, if not given an access token and, if successful returns an Access Key and a DropboxClient object that can then be used to carry out operations.

Click to see the code of the DropboxManager.

To use it, you create a DropboxManager, passing in the Access Token if you already have it, set up either a Login or LoginAsync callback to manage the login form, and call SetupClientAsync, which will return true if it succeeds in creating a client, or false if the login fails.

Login would be used where the form management can be synchronous, as in WinForms, and LoginAsync where it is asynchronous, as in Xamarin.Forms. Either way, the callback needs to create some kind of UI window or page with a web browser control on it (e.g. a WebView in Xamarin.Forms), and call OnBrowserNavigating within the browser control’s Navigating event. If this returns true the login procedure has finished or failed, and the window or page should be closed.

As I am most familiar with WinForms, I thought I would start by writing it to work with that. This turned out to be a mistake, as I still haven’t managed to get this to work. The Login callback is simply

var accessToken = manager.Login += (uri, oAuth2State) => {
    using (var f = new FormLogin(manager, oAuth2State, uri)) {
        f.ShowDialog();
        return f.AccessToken;
    }
};

and FormLogin is a form with a WebBrowser control and the code

internal partial class FormLogin : Form {
    private DropboxManager _manager;
    private string _oAuthState;
    private Uri _uri;
    public string AccessToken { get; private set; }

    public FormLogin(DropboxManager manager, string oAuthState, Uri uri) {
        InitializeComponent();

        _manager = manager;
        _oAuthState = oAuthState;
        _uri = uri;
    }

    protected override void OnLoad(EventArgs e) {
        base.OnLoad(e);

        webBrowser1.Url = _uri;
    }

    private void webBrowser1_Navigating(object sender, WebBrowserNavigatingEventArgs e) {
        string token;
        if (_manager.OnBrowserNavigating(e.Url, _oAuthState, out token)) {
            AccessToken = token;
            e.Cancel = true;
            Close();
        }
    }
}

Unfortunately, this doesn’t quite work. The Dropbox login page is displayed, and the login does happen, but there’s a problem with getting the redirect in the Navigating event. My guess is that IE (or Edge, it’s similar with both) is doing some processing, probably to do with security, before raising the Navigating event, realising there is something up with the localhost url, and displaying an error page instead. Closing that makes it appear the login hasn’t worked. However, if you then try again, instead of displaying the Dropbox login page, it goes to the cache and asks you to Refresh. If you do this, it does try to navigate to the redirect url, and the login routine succeeds.

I spent ages trying to work out what was happening with this, before I decided to try creating a Xamarin.Forms version, which worked first time!

For Xamarin.Forms LoginAsync is used. There are a number of approaches that could be used to handle the synchronisation, but I used this one:

manager.LoginAsync = async (uri, oAuth2State) => {
    var logPage = new DropboxLoginPage(manager, oAuth2State, uri);
    await Navigation.PushAsync(logPage);
    var completion = new TaskCompletionSource<string>();
    logPage.Popped += (sender, ea) => {
        completion.TrySetResult(logPage.AccessToken);
    };
    return await completion.Task;
};

The Popped event is something I have added to pages using an interface that is called by the Main Navigation page when its Popped event is called, so that I can pick up the event from the page that is actually being Popped. I don’t understand why that isn’t a standard part of Xamarin.Forms. Without that, the easiest alternative would be to pass the TaskCompletionSource to the DropboxLoginPage.

The source of the page is just:

internal class DropboxLoginPage : TrackedContentPage
{
    private DropboxManager _manager;
    private string _oAuthState;
    public string AccessToken { get; private set; }

    public DropboxLoginPage (DropboxManager manager, string oAuthState, Uri uri)
    {
        _manager = manager;
        _oAuthState = oAuthState;

        Title = "Sign in to Dropbox";

        var web = new WebView {
            Source = uri.ToString(),
            HorizontalOptions = LayoutOptions.FillAndExpand,
            VerticalOptions = LayoutOptions.FillAndExpand
        };
        web.Navigating += async (s, e) => {
            string token;
            if (_manager.OnBrowserNavigating(new Uri(e.Url), _oAuthState, out token)) {
                AccessToken = token;
                e.Cancel = true;
                await Navigation.PopAsync();
            }
        };

        Content = new StackLayout {
            Children = { web }
        };
    }
}

TrackedContentPage implements the interface I mentioned above.

5 thoughts on “Dropbox support for Xamarin.Forms”

  1. Thank you for this great approach.

    Could you show how you’ve implemented the Popped event?
    I still don’t understand it.

    That would be nice.

  2. The reason I created the Tracked Pages is because the only way of knowing that a page has been popped is by responding to an event on the NavigationPage, whereas I find it convenient to have the events raised on the Page object that is being popped, and also to be able to have protected methods like OnPopped in the Page.

    The way I implemented it was to create an ITrackedPage interface and have my navigation page check for it and call methods on it that raise the events defined on it. There are other things in the real interface, but this shows the basic implementation for Popped.

    public interface ITrackedPage {
        void BeingPopped();
        event EventHandler Popped;
    }
    

    Then my navigation page contains

    Popped += (object sender, NavigationEventArgs e) => {
        var trackedPage = e.Page as ITrackedPage;
        if (trackedPage != null) trackedPage.BeingPopped();
    }
    

    If a Page has an ITracked interface its BeingPopped method will be called whenever it is popped and it can then handle the event in the normal way. As it happens, I was only using ContentPages, so I created a base content page class that all my other pages derived from:

    public class TrackedContentPage : ContentPage, ITrackedPage {
        public event EventHandler Popped;
        protected virtual void OnPopped() {
            Popped?.Invoke(this, EventArgs.Empty);
        }
        public void BeingPopped() {
            OnPopped();
        }
    }
    

    This lets me write code like:

    var p = new MatrixPage();
    p.Popped += (s, e) => {
        matrix.Reload(p.GetSomeInfo());
    };
    await Navigation.PushAsync(p);
    

    which is more like displaying a modal dialogue in WinForms, and is much cleaner than responding directly on the navigation page.

  3. Do you have a complete example somewhere as to how you used the class? It’s a little hard to piece together based upon the article.

Leave a Reply

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.