{"id":288,"date":"2016-05-05T11:38:44","date_gmt":"2016-05-05T10:38:44","guid":{"rendered":"http:\/\/babbacom.com\/?p=288"},"modified":"2016-05-05T11:38:44","modified_gmt":"2016-05-05T10:38:44","slug":"dropbox-support-for-xamarin-forms","status":"publish","type":"post","link":"https:\/\/babbacom.com\/?p=288","title":{"rendered":"Dropbox support for Xamarin.Forms"},"content":{"rendered":"<p>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 <a href=\"http:\/\/mobiledmx.com\">Mobile DMX<\/a> it quickly became obvious, looking at the &#8220;SimpleTest&#8221; sample app, that the awkward part is logging in, but uploading and downloading small files would then be very easy.<!--more--><\/p>\n<p>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&#8217;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).<\/p>\n<p>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&#8217;s obviously designed to work from a web app &#8211; 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.<\/p>\n<p>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 <strong>has<\/strong> to be https, but for an app this is irrelevant and just any old uri can be used.<\/p>\n<p>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.<\/p>\n<p><a href=\"https:\/\/gist.github.com\/trevorprinn\/f4b6b6b57ee5b23e4ba68307f513ed16\" target=\"_blank\">Click to see the code of the DropboxManager.<\/a><\/p>\n<p>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.<\/p>\n<p>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&#8217;s Navigating event. If this returns true the login procedure has finished or failed, and the window or page should be closed.<\/p>\n<p>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&#8217;t managed to get this to work. The Login callback is simply<\/p>\n<pre>var accessToken = manager.Login += (uri, oAuth2State) =&gt; {\r\n    using (var f = new FormLogin(manager, oAuth2State, uri)) {\r\n        f.ShowDialog();\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 return f.AccessToken;\r\n    }\r\n};<\/pre>\n<p>and FormLogin is a form with a WebBrowser control and the code<\/p>\n<pre>internal partial class FormLogin : Form {\r\n\u00a0\u00a0 \u00a0private DropboxManager _manager;\r\n\u00a0\u00a0 \u00a0private string _oAuthState;\r\n\u00a0\u00a0 \u00a0private Uri _uri;\r\n\u00a0\u00a0 \u00a0public string AccessToken { get; private set; }\r\n\r\n\u00a0\u00a0 \u00a0public FormLogin(DropboxManager manager, string oAuthState, Uri uri) {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0InitializeComponent();\r\n\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0_manager = manager;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0_oAuthState = oAuthState;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0_uri = uri;\r\n\u00a0\u00a0 \u00a0}\r\n\r\n\u00a0\u00a0 \u00a0protected override void OnLoad(EventArgs e) {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0base.OnLoad(e);\r\n\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0webBrowser1.Url = _uri;\r\n\u00a0\u00a0 \u00a0}\r\n\r\n\u00a0\u00a0 \u00a0private void webBrowser1_Navigating(object sender, WebBrowserNavigatingEventArgs e) {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0string token;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0if (_manager.OnBrowserNavigating(e.Url, _oAuthState, out token)) {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0AccessToken = token;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0e.Cancel = true;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0Close();\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0}\r\n\u00a0\u00a0 \u00a0}\r\n}<\/pre>\n<p>Unfortunately, this doesn&#8217;t quite work. The Dropbox login page is displayed, and the login does happen, but there&#8217;s a problem with getting the redirect in the Navigating event. My guess is that IE (or Edge, it&#8217;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&#8217;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.<\/p>\n<p>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!<\/p>\n<p>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:<\/p>\n<pre>manager.LoginAsync = async (uri, oAuth2State) =&gt; {\r\n\u00a0\u00a0 \u00a0var logPage = new DropboxLoginPage(manager, oAuth2State, uri);\r\n\u00a0\u00a0 \u00a0await Navigation.PushAsync(logPage);\r\n\u00a0\u00a0 \u00a0var completion = new TaskCompletionSource&lt;string&gt;();\r\n\u00a0\u00a0 \u00a0logPage.Popped += (sender, ea) =&gt; {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0completion.TrySetResult(logPage.AccessToken);\r\n\u00a0\u00a0 \u00a0};\r\n\u00a0\u00a0 \u00a0return await completion.Task;\r\n};<\/pre>\n<p>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&#8217;t understand why that isn&#8217;t a standard part of Xamarin.Forms. Without that, the easiest alternative would be to pass the TaskCompletionSource to the DropboxLoginPage.<\/p>\n<p>The source of the page is just:<\/p>\n<pre>internal class DropboxLoginPage : TrackedContentPage\r\n{\r\n\u00a0\u00a0 \u00a0private DropboxManager _manager;\r\n\u00a0\u00a0 \u00a0private string _oAuthState;\r\n\u00a0\u00a0 \u00a0public string AccessToken { get; private set; }\r\n\r\n\u00a0\u00a0 \u00a0public DropboxLoginPage (DropboxManager manager, string oAuthState, Uri uri)\r\n\u00a0\u00a0 \u00a0{\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0_manager = manager;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0_oAuthState = oAuthState;\r\n\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0Title = \"Sign in to Dropbox\";\r\n\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0var web = new WebView {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0Source = uri.ToString(),\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0HorizontalOptions = LayoutOptions.FillAndExpand,\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0VerticalOptions = LayoutOptions.FillAndExpand\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0};\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0web.Navigating += async (s, e) =&gt; {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0string token;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0if (_manager.OnBrowserNavigating(new Uri(e.Url), _oAuthState, out token)) {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0AccessToken = token;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0e.Cancel = true;\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0await Navigation.PopAsync();\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0}\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0};\r\n\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0Content = new StackLayout {\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0Children = { web }\r\n\u00a0\u00a0 \u00a0\u00a0\u00a0 \u00a0};\r\n\u00a0\u00a0 \u00a0}\r\n}<\/pre>\n<p>TrackedContentPage implements the interface I mentioned above.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 &#8220;SimpleTest&#8221; sample app, that the awkward part is logging in, but uploading and downloading small &hellip; <a href=\"https:\/\/babbacom.com\/?p=288\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Dropbox support for Xamarin.Forms&#8221;<\/span><\/a><\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,7,11],"tags":[31,42,41],"class_list":["post-288","post","type-post","status-publish","format-standard","hentry","category-android","category-programming","category-windows","tag-c","tag-dropbox","tag-xamarin"],"_links":{"self":[{"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/posts\/288","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/babbacom.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=288"}],"version-history":[{"count":10,"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/posts\/288\/revisions"}],"predecessor-version":[{"id":299,"href":"https:\/\/babbacom.com\/index.php?rest_route=\/wp\/v2\/posts\/288\/revisions\/299"}],"wp:attachment":[{"href":"https:\/\/babbacom.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=288"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/babbacom.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=288"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/babbacom.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=288"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}