Reputation: 5276
I am implementing native Android Facebook authentication in Xamarin Forms and am struggling with the Facebook SDK breaking my await/async chain. The SDK requires passing implementations of IFacebookCallback
and GraphRequest.IGraphJSONObjectCallback
which define callbacks to process results. I would like to do the following in code that requests a login:
public async Task LogInFacebookAsync()
{
var loginResult = await _facebookManager.Login();
// do stuff with result
}
However, here's what I'm currently doing.
Code that requests a login:
public void LogInFacebookAsync()
{
_facebookManager.Login(onFacebookLoginComplete);
}
private async Task<string> onFacebookLoginComplete(FacebookUser fbUser, string errorMessage)
{
// do stuff with result
}
Here is my FacebookManager class which implements the necessary interfaces. Notice that results can come from multiple callback methods: OnCancel()
, OnError()
, OnCompleted()
, each of which invokes _onLoginComplete as passed in by the code above.
public class FacebookManager : Java.Lang.Object, IFacebookManager, IFacebookCallback, GraphRequest.IGraphJSONObjectCallback
{
public Func<FacebookUser, string, Task> _onLoginComplete;
public ICallbackManager _callbackManager;
private Activity _context;
public FacebookManager()
{
this._context = CrossCurrentActivity.Current.Activity;
_callbackManager = CallbackManagerFactory.Create();
LoginManager.Instance.RegisterCallback(_callbackManager, this);
}
public async Task Login(Func<FacebookUser, string, Task> onLoginComplete)
{
_onLoginComplete = onLoginComplete;
LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
await Task.CompletedTask;
}
#region IFacebookCallback
public void OnCancel()
{
_onLoginComplete?.Invoke(null, "Canceled!");
}
public void OnError(FacebookException error)
{
_onLoginComplete?.Invoke(null, error.Message);
}
public void OnSuccess(Java.Lang.Object result)
{
var n = result as LoginResult;
if (n != null)
{
var request = GraphRequest.NewMeRequest(n.AccessToken, this);
var bundle = new Android.OS.Bundle();
bundle.PutString("fields", "id, first_name, email, last_name, picture.width(500).height(500)");
request.Parameters = bundle;
request.ExecuteAsync();
}
}
#endregion
#region GraphRequest.IGraphJSONObjectCallback
public void OnCompleted(JSONObject p0, GraphResponse p1)
{
// extract user
_onLoginComplete?.Invoke(fbUser, string.Empty);
}
#endregion
}
Is there any way I can wrap these operations so that I am "async all the way down"?
Thank you Paulo for the great answer. I was already thinking along those lines and you really crystallized the solution. My only concern with the answer was about it being thread-safe. What if a button is double-clicked or two API calls are made at the same time that require Facebook login? The following is my attempt to make the LoginAsync()
method safer. However, I'm no expert at thread safety and I welcome all criticism.
private TaskCompletionSource<FacebookUser> _loginTcs;
public async Task<FacebookUser> LoginAsync()
{
Task<FacebookUser> loginTask = _loginTcs?.Task;
// Don't want to create a new login request to the Facebook API if one is already being made.
if (loginTask != null && (loginTask.Status == TaskStatus.Running || loginTask.Status == TaskStatus.WaitingForActivation
|| loginTask.Status == TaskStatus.WaitingToRun || loginTask.Status == TaskStatus.WaitingForChildrenToComplete))
{
return await loginTask.ConfigureAwait(false);
}
this._loginTcs = new TaskCompletionSource<FacebookUser>();
LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
var user = await this._loginTcs.Task.ConfigureAwait(false);
return user;
}
Upvotes: 0
Views: 291
Reputation: 14856
Use a TaskCompletionSource. No need for anything else.
I don't know a lot about that API (and I'm surprised Xamarin does not provide na idiomatic .NET version of it) but, roughly, what you need to do is:
Add a task completion source to your class instead of the _onLoginComplete
delegate:
// public Func<FacebookUser, string, Task> _onLoginComplete;
private TaskCompletionSource<FacebookUser> completion;
Change your OnCancel
method to cancel the task:
public void OnCancel()
{
this.completion.TrySetCanceled();
}
Change your OnError
method to complete the task with error:
public void OnError(FacebookException error)
{
this.completion.TrySetException(error);
}
Change your OnCompleted
to set the task result:
public void OnCompleted(JSONObject p0, GraphResponse p1)
{
// extract user
this.completion.TrySetResult(fbUser);
}
Change your login method to create the task completion source and return the task completion source's task:
public async Task<FacebookUser> LoginAsync()
{
this.completion = new TaskCompletionSource<FacebookUser>();
LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
await this.completion.Task;
}
Upvotes: 3