Download Code (1.14 MB)
I was thinking about how to write code that will work on both Windows Phone 7 and Windows 8 (Metro). In theory some of the techniques that could be used are well known. But I wanted to try them out. So I decided to try some things out in a piece of throw away code. This gives me some liberty to experiement without worrying about the long term consequences of having not chosen the "best" way. For my throw away application I decided to make an application that would grab some one's Facebook friend list and display it on the screen. So this code will necessarily need to authenticate the user against Facebook. If you've not worked with the Facebook APIs before don't worry about it too much. You should still be able to follow along since this isn't digging deeply into what's available.
I've managed to use some of the techniques to make a Windows Metro version of RestSharp. I'll talk about that in another post.
Register your Application
Before you begin doing any coding you'll want to register your new Facebook application in the Facebook developer portal. This also means that you need to have a facebook account. Because I didn't want to bother my associates with test items in my feed I also created a secondaty facebook account for testing (It's for my cat, feel free to friend if if you are enthusiastic about cat pictures). The portal is available at developer.facebook.com. Clicking on menu item "Apps" at the top of the screen will take you to a page that shows all of your registered applications (if any) and will let you register more. If this is your first time using the portal you'll have no applications. I'll walk you through registering an application. I'll only draw your attention to options as necessary. Options that I don't mention can be left in their default state (which will most likely be blank).
Click on the "Create New App" button. You'll get prompted with a dialog in which you will need to enter the name of the application that you are creating. The dialog will automatically validate the name that you've entered once you've stopped typing and will let you know if a chane needs to be made. Once you have a name that the dialog considers valid click on the "Continue" button.
The next dialog is a Captcha. I really dislike Captchas. While I understand that these dialogs exists to make sure that a bot is not registering the new application the problem that I have with them is that more times than not I'm going to get the entry wrong a few times (ex: is that third character a lowercase 'L', upper case 'L', or an uppercase 'I'?).
Once you pass the Captcha you'll get an opportunity to specify the other details for your application and a chance to change the application's icon and category. It may not be obvious which option to select for "Select how your app integrates with Facebook." None of the available options has a description that you would automatically assocate with a Windows Phone or Metro application. Select "Website". During development it doesn't matter if the website that you've specified exists or not. But you will need to enter an address to a page that would in theory process the login information. Once you've entered this information select "Save Changes." There are three pieces of information that are on this page that you'll be copying into your application later on: App ID, App Secret, and the Site URL. Keep this page open so that you can copy the information when I refer to it later.
Now to make your mobile application. At the time of this writing you'll need to use Visual 2010 for Windows Phone applications and Visual Studio 11 for Windows Metro applications. Most of what will be done will be similar enough such that I can talk about the procedure to be followind in parallel and note the occasional differences as they arise.
Creating the New Application Projects
I created both a Silverlight Application for Windows Phone and a Windows 8 Metro project. There are going to be significant blocks of code that will be identical between these two projects. When this occurs I'll only create one version of the file but will have it referenced in both projects. Linked files can easily be recognized by the arrow that shows in the lower-left corner of their icon. If you've never linked files before you can find more information on it here.
How linked files appear in Visual Studio.
Conditionally Compiled Code
There still may be sections in thee files that are specific to Windows Phone or Windows 8. These sections will be conditionally commented out depending on the platform on which the code is being compiled. For code that is specific to Windows 8 Metro applications the code is wrapped in #if NETFX_CORE
/#endif
blocks. For Windows 7 code I've wrapped the code in #if SILVERLIGHT
/#endif
blocks. I could have also used #if WINDIWS_PHONE
/#endif
blocks
If there is a large block of code that is specific to one platform and not the other instead of using the conditional compilation directives one could choose to just include the file containing the code in one file and not in the other. You'll see this done with most of the UI code (the XAML, while similar, is not shared between the two projects).
Creating the Authentication Page
Before I can call any method in Facebook I need to authenticate the user. Facebook uses a form of open authentication. So we will need to use a web browser in the application. Add a new page to your project caled "AuthenticationPage.xaml
." The only element that is necessary in this page will be the web browser. Many of the differences that you will encounter between Metro and Windows Phone 7 are in UI related code. There's a slight difference in the element that needs to be used for representing a browser element.
<WebView
x:Name="AuthenticationWindow"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
LoadCompleted="AuthenticationWindow_LoadCompleted"
/>
|
|
<phone:WebBrowser
Name="AuthenticationWindow"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Navigated="AuthenticationWindow_Navigated"
/>
|
Browser element for Windows Metro
|
|
Browser element for Windows Phone 7
|
The Application Constants
There are a few elements of data to which we will need. They are all in the entries for the app that you registered with Facebook. I'll need to use these in more than one place. The logic needed for authentication is simple and I left it in the UI code. I separated the logic that will be shared in the Metro and WP7 versions of this application into a partial class definition. That
static class ApplicationConstants
{
const string REDIRECT_URL = "https://mysite.com/SomePostAuthenticationPage.html";
const string APP_ID = "_YOUR_APP_ID_";
const string APP_SECRET = "_YOUR_APP_SECRET_";
const string PERMISSIONS = "read_friendlists";
}
You'll need to substitute the values for your own application in these constants (except for the PERMISSIONS
constant).
Creating the Authentication Page
An extremely high level overview of what occurs in the code-behind for the authentication page is that it will load the Facebook Open Authentication page and then monitor the path to which the user navigates. If the user has authenticated the browser will be directed to the site that had been registered for the application earlier. When this occurs there will be an authentication code appended to the URL. That code will be parsed out and exchanged for a token. There's not much the UI is really doing. So the codebehind looks simple.
public partial class AuthenticationPage : PhoneApplicationPage
{
public AuthenticationPage()
{
InitializeComponent();
ViewModel = (App.Current as J2i.Net.FacebookAuthenticationTest.App).ViewModel;
ViewModel.AuthenticationAttempted = true;
}
private void AuthenticationWindow_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
PrepareForAuthentication();
}
private void AuthenticationWindow_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
}
private void AuthenticationWindow_Navigating(object sender, NavigatingEventArgs e)
{
ProcessWebPageLoad(e.Uri.ToString());
}
}
Code-behind for the Windows Phone version of the application
Handling Code Differences with Partial Classes
Where there is a slight difference in the code behind for the two pages the rest of the logic is the same. Instead of duplicating it I put the common logic in a partial class definition. The partial class has the same name as the class for the page. So at compile time the code from the code-behind and the partial class will be compiled into a single class. The common code is in AuthenticationPage.xaml.shared.cs
. The first method that the code-behind references from the shared file is PrepareForAuthentication()
. This method will build the authentication URL which contains parameters to allow the application to identify itself, what permissions are needed, and a random string that should be passed for security reasone. If you want to see more information on these parameters they can be found in the Facebook authentication documentation.
void PrepareForAuthentication()
{
_unique = Guid.NewGuid().ToString();
string targetUrl = String.Format("https://www.facebook.com/dialog/oauth?" +
"client_id={0}" +
"&redirect_uri={1}" +
"&scope={2}" +
"&state={3}",
Uri.EscapeUriString(ApplicationConstants.APP_ID),
Uri.EscapeUriString(ApplicationConstants.REDIRECT_URL),
Uri.EscapeUriString(ApplicationConstants.PERMISSIONS),
Uri.EscapeUriString(_unique));
AuthenticationWindow.Navigate(new Uri(targetUrl));
}
The ProcessWebPage()
method looks at the URL to which the browser is navigating to see whether or not it is our redirect URL. If it's not then nothing happens. If it is then the query parameters are extracted. Theparameters we are most interested in are code
and state
since these will indicate a successful authentication attempt. The code will be needed to request an access token for making Facebook API calls.
void ProcessWebPageLoad(string targetUriString)
{
if (targetUriString.IndexOf(ApplicationConstants.REDIRECT_URL) == 0)
{
string _code = GetQueryParam(targetUriString, "code");
string _state = GetQueryParam(targetUriString, "state");
string _error = GetQueryParam(targetUriString, "error");
string _errorReason = GetQueryParam(targetUriString, "error_description");
string _errorDescription = GetQueryParam(targetUriString, "error_reason");
if (
(!String.IsNullOrEmpty(_code)) &&
(!String.IsNullOrEmpty(_state)) &&
(_unique.Equals(_state))
)
{
ExchangeCodeForAccessToken(_code);
}
}
}
Requessting the access token is another web call, but it's not one for which we need the browser. It doesn't return anything user friendly so we can make the call without using the browser. Getting the access token is just a matter of building a URL and grabbing the token from the results.
void ExchangeCodeForAccessToken(string code)
{
string targetUrl = String.Format("https://graph.facebook.com/oauth/access_token?" +
"client_id={0}" +
"&redirect_uri={1}" +
"&client_secret={2}" +
"&code={3}",
Uri.EscapeUriString(ApplicationConstants.APP_ID),
Uri.EscapeUriString(ApplicationConstants.REDIRECT_URL),
Uri.EscapeUriString(ApplicationConstants.APP_SECRET),
Uri.EscapeUriString(code));
var webRequest = HttpWebRequest.CreateHttp(targetUrl);
webRequest.BeginGetResponse((o) =>
{
var response = webRequest.EndGetResponse(o);
var rs = response.GetResponseStream();
StreamReader sr = new StreamReader(rs);
var responseString = sr.ReadToEnd();
string accessToken = GetQueryParam(responseString,"access_token");
DateTime expirationDate = DateTime.MinValue;
string expiresString = GetQueryParam(responseString, "expires");
if (!String.IsNullOrEmpty(expiresString))
{
int expireTime;
if (int.TryParse(expiresString, out expireTime))
{
expirationDate = DateTime.Now.AddSeconds(expireTime);
}
}
if (!String.IsNullOrEmpty(accessToken) && (expirationDate != DateTime.MinValue))
{
ViewModel.AccessInfo = new AccessInfo() { ExpirationDate = expirationDate, Token = accessToken };
ViewModel.SaveAccessToken();
ViewModel.UpdateFriendList();
ReturnToPreviousPage();
}
}, null);
}
Navigation is a little different on Windows Phone 7 and Windows 8. So I added a ReturnToPreviousPage()
method in both the code-behind for the Metro and Windows Phone 7 versions of the application. Each of which contains the appropriate version of the navigation code.
void ReturnToPreviousPage()
{
ViewModel.DispatchInvoke(() =>
{
(Window.Current.Content as Frame).GoBack();
}
);
}
|
|
void ReturnToPreviousPage()
{
ViewModel.DispatchInvoke(() =>
{
NavigationService.GoBack();
}
);
}
|
Windows 8 Metro Version
|
|
Windows Phone Version
|
Retreiving the Friend List
The code for retrieving the friend list looks identical on both platforms. Facebook may not return the entire friend list in one call. If there are more friends to be retrieved in the return structure from the call there will be an address in a member in the return structure named Next
(nested in Paging
) which contains the address to the next page of the results. I'm not parsing the results myself but am instead making use of JSON.Net by James Newton-King. The results are parsed and accumulated into a temporary list. Once I know that I have all the results they are moved to the bindable list that drives the list box on the UI.
public void UpdateFriendList(string targetUrl = null, List previousUserList = null)
{
if (AccessInfo == null)
return;
if (previousUserList == null)
previousUserList = new List();
var request = System.Net.HttpWebRequest.CreateHttp(targetUrl ?? String.Format(FRIEND_LIST_REQUEST, AccessInfo.Token));
request.BeginGetResponse((o) =>
{
var response = request.EndGetResponse(o);
var responseStreamReader = new StreamReader(response.GetResponseStream());
string friendListText = responseStreamReader.ReadToEnd();
Newtonsoft.Json.JsonSerializer s = new JsonSerializer();
Data.FriendListResponse v = Newtonsoft.Json.JsonConvert.DeserializeObject(friendListText);
if ((v != null) && (v.Data != null))
{
foreach (var user in v.Data)
{
previousUserList.Add(user);
}
}
if ((v!=null) && (v.Paging != null) && (v.Paging.Next != null))
UpdateFriendList(v.Paging.Next, previousUserList);
else
{
previousUserList.Sort((a1, a2) => { return a1.Name.CompareTo(a2.Name); });
DispatchInvoke(() =>
{
FriendList.Clear();
foreach (var u in previousUserList)
FriendList.Add(u);
}
);
}
}, null);
}
Dispatching Differences
There is a rule in Windows Platforms that you cannot modify the UI from a seconday thread. On the XAML based platforms you can ensure calls that modify the UI are marshalled to the UI thread by making use of an object called the Dispatcher
. The way that you use this object differs on Windows Phone 7 and Windows 8. I abstracted away the differences by making a method called DispatchInvoke()
. It compiles different on each platform and will call the appropriate code on each platform.
public void DispatchInvoke(Action a)
{
#if SILVERLIGHT
if (MainViewModel.Dispatcher == null)
a();
else
Dispatcher.BeginInvoke(a);
#else
if ((Dispatcher != null) && (!Dispatcher.HasThreadAccess))
{
Dispatcher.InvokeAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, (obj, invokedArgs) => { a(); }, this, null);
}
else
a();
#endif
}
Saving Data
For saving simple data types I tend to use a simple component that I made for Windows Phone 7 (it can be found here). On Windows 8 all file IO is asynchronous. So the component had to be modified to work on Metro. I've shown the details of the changes on another page. Since the Windows Phone 7 version uses a blocking call and the Metro version is asynchronous the calls could not be made to look the same. But this difference was compatmentalized to a single method.
public void SaveAccessToken()
{
if (this.AccessInfo != null)
{
#if NETFX_CORE
DataSaver<AccessInfo>.SaveMyDataAsync(this.AccessInfo, "_accessToken.xml");
#endif
#if SILVERLIGHT
DataSaver<AccessInfo>.SaveMyData(AccessInfo, "_accessToken.xml");
#endif
}
}
Most of the code for this project is in the MainViewModel
class. The entirity of the code for it is below. It shouldn't contain anything that you don't recognize from above. The using directives at the top contain conditional compilation directives since the namespaces in which some of the classes exists differs on Windows Phone 7 and on Windows 8 Metro.
using System;
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using J2i.Net.FacebookAuthenticationTest.Data;
using J2i.Net.FacebookAuthenticationTest.Utility;
#if SILVERLIGHT
using System.Windows.Threading;
#endif
#if NETFX_CORE
using System.Threading.Tasks;
using J2i.Net.FacebookAuthenticationTest.Data;
using Windows.UI.Xaml;
#endif
namespace J2i.Net.FacebookAuthenticationTest.ViewModel
{
public class MainViewModel: INotifyPropertyChanged
{
const string FRIEND_LIST_REQUEST = "https://graph.facebook.com/me/friends?access_token={0}";
public MainViewModel()
{
#if SILVERLIGHT
var a = DataSaver<AccessInfo>.LoadMyData("_accessToken.xml");
if(a !=null)
{
this.AccessInfo = a;
this.UpdateFriendList();
}
#endif
#if NETFX_CORE
DataSaver<AccessInfo>.LoadDataAsync("_accessToken.xml", (info, exc)=>
{
if (info != null)
{
this.AccessInfo = info;
this.UpdateFriendList();
}
});
#endif
}
ObservableCollection<FacebookUser> _friendList;
public ObservableCollection<FacebookUser> FriendList
{
get { return (_friendList) ?? (_friendList = new ObservableCollection<FacebookUser>()); }
set { _friendList = value; }
}
string _friendListText = String.Empty;
public string FriendListText
{
get { return _friendListText; }
set
{
if (value != _friendListText)
{
_friendListText = value;
RaisePropertyChanged("FriendListText");
}
}
}
public void SaveAccessToken()
{
if (this.AccessInfo != null)
{
#if NETFX_CORE
DataSaver<AccessInfo>.SaveMyDataAsync(this.AccessInfo, "_accessToken.xml");
#endif
#if SILVERLIGHT
DataSaver<AccessInfo>.SaveMyData(AccessInfo, "_accessToken.xml");
#endif
}
}
AccessInfo _accessInfo;
public AccessInfo AccessInfo
{
get { return (_accessInfo) ?? (_accessInfo = new AccessInfo()); }
set
{
if (_accessInfo != value)
{
_accessInfo = value;
}
}
}
bool _authenticationAttempted = false;
public bool AuthenticationAttempted
{
get { return _authenticationAttempted || (!String.IsNullOrEmpty(AccessInfo.Token)); }
set { _authenticationAttempted = value; }
}
#if NETFX_CORE
static Windows.UI.Core.CoreDispatcher _dispatcher;
public static Windows.UI.Core.CoreDispatcher Dispatcher
{
get
{
if (_dispatcher != null)
return _dispatcher;
if ((Window.Current==null)||(Window.Current.Content == null))
return null;
return Window.Current.Content.Dispatcher;
}
set { _dispatcher = value; }
}
#endif
#if SILVERLIGHT
static Dispatcher _dispatcher;
public static Dispatcher Dispatcher
{
get
{
if (_dispatcher!=null)
return _dispatcher;
var app = (App.Current as J2i.Net.FacebookAuthenticationTest.App);
if (app.RootFrame == null)
return null;
return (app.RootFrame.Dispatcher);
}
set
{
_dispatcher = value;
}
}
#endif
public void UpdateFriendList(string targetUrl = null, List<FacebookUser> previousUserList = null)
{
if (previousUserList == null)
previousUserList = new List<FacebookUser>();
var request = System.Net.HttpWebRequest.CreateHttp(targetUrl ?? String.Format(FRIEND_LIST_REQUEST, AccessInfo.Token));
request.BeginGetResponse((o) =>
{
var response = request.EndGetResponse(o);
var responseStreamReader = new StreamReader(response.GetResponseStream());
string friendListText = responseStreamReader.ReadToEnd();
Newtonsoft.Json.JsonSerializer s = new JsonSerializer();
Data.FriendListResponse v = Newtonsoft.Json.JsonConvert.DeserializeObject<Data.FriendListResponse>(friendListText);
if ((v != null) && (v.Data != null))
{
foreach (var user in v.Data)
{
previousUserList.Add(user);
}
}
if ((v!=null) && (v.Paging != null) && (v.Paging.Next != null))
UpdateFriendList(v.Paging.Next, previousUserList);
else
{
previousUserList.Sort((a1, a2) => { return a1.Name.CompareTo(a2.Name); });
DispatchInvoke(() =>
{
FriendList.Clear();
foreach (var u in previousUserList)
FriendList.Add(u);
}
);
}
}, null);
}
void DispatchInvoke(Action a)
{
#if SILVERLIGHT
if (MainViewModel.Dispatcher == null)
a();
else
Dispatcher.BeginInvoke(a);
#else
if ((Dispatcher != null) && (!Dispatcher.HasThreadAccess))
{
Dispatcher.InvokeAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, (obj, invokedArgs) => { a(); }, this, null);
}
else
a();
#endif
}
protected void RaisePropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
DispatchInvoke(()=>
{
RaisePropertyChanged(propertyName);
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
CodeProject