Tuesday, September 14, 2010

Messenger Connect – Getting Contacts Three Different Ways

Roadmap
This post is part of a series of related posts on how to use Windows Live Messenger Connect.

1. Using Messenger Connect REST Explorer for exploring Windows Live resources.
2. Using the Messenger Connect JavaScript API for listing your contacts.
3. (this post) Getting contacts in three different ways (API and REST).
4. Messenger Connect - Initializing Controls

Overview

Comparing Three Ways of Getting Contacts
A Page That Gets Contacts Three Different Ways

This post compares side-by-side three methods of getting a resource (in this case we stick with contacts):

1. Using the JavaScript API
- Pros: API does all the hard work for you; no cross browser issues.
- Cons: You have to load a library.

2. Using the REST endpoint directly and getting data with a jQuery.ajax call.
- Pros: Stays all in JavaScript; jQuery makes it easy.
- Cons: Cross-browser issues (to work in IE you must enable cross domain posts, other browsers?).

3. Using the REST endpoint directly and getting data with an HttpWebRequest in the page code-behind.
- Pros: Can use LINQ to process response; no cross-browser issues.
- Cons: You have to have a code-behind, a server-side page.*

* To be fair, approach 1 requires a code file that implements a session handler for unsecured (non HTTPS) scenarios. See the Messenger Connect JavaScript API post for details on that.

Method 1 is probably going to be what you'll use most of the time, but it is interesting to compare the other approaches. To that end, this post shows an ASPX and code-behind page that shows these three approaches for getting the first five contacts in your contact list. It is built on the Windows Live Application template which can be downloaded here and of course requires that you are registered as a Messenger Connect application. Again, all is explained here.

Update 2011-02-13: Here's a zip of the ASPX and code-behind page.

Without further ado, here's the code (a thorough explanation follows):

The Page Markup


<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ContactsComparison.aspx.cs"
Inherits="WindowsLiveWebApplicationBeta.ContactsComparison" %>

<%@ Import Namespace="System.Web.Configuration" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wl="http://apis.live.net/js/2010">
<head id="Head1" runat="server">
<title>Get Contacts Comparison</title>
<script type="text/javascript" src="http://js.live.net/4.1/loader.js"></script>
<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.min.js" ></script>
<script type="text/javascript">
var dvContactList;
var dataContext;
var contactsView;
var totalNumContacts;
var displayNumContacts = 5;
var cid;
var accessToken;
var uriContactGet = "http://apis.live.net/V4.1/cid-{0}/Contacts/AllContacts";
jQuery.noConflict();
function appLoaded(applicationLoadCompletedEventArgs) {
}

function SignedOutCallback() {
}
function SignedInCallback(signInCompletedEventArgs) {
if (signInCompletedEventArgs.get_resultCode() !== Microsoft.Live.AsyncResultCode.success) {
log("Failed to sign in: " + signInCompletedEventArgs.get_resultCode());
return;
}
else {
auth = Microsoft.Live.App.get_auth();
accessToken = auth.get_accessToken();
cid = auth.get_cid();
}
}
// *** Using JavaScript API *** //
function getContactsUsingAPI() {
if (Microsoft.Live.App.get_auth().get_state() === Microsoft.Live.AuthState.authenticated) {
dataContext = Microsoft.Live.App.get_dataContext();
dataContext.loadAll(Microsoft.Live.DataType.contacts, contactsLoaded);
}
else {
log("Failed to get contacts using API. Are you authenticated?");
}
}
function contactsLoaded(dataLoadCompletedEventArgs) {
if (dataLoadCompletedEventArgs.get_resultCode() !== Microsoft.Live.AsyncResultCode.success) {
alert("Contacts failed to load...!");
return;
}

contactsCollection = dataLoadCompletedEventArgs.get_data();
totalNumContacts = contactsCollection.items.length;
for (var i = 0; i < Math.min(totalNumContacts, displayNumContacts); i++) {
var contact = contactsCollection.getItem(i);
addItemToList("ContactsAPI", contact.get_firstName() + " " + contact.get_lastName());
}

}
// *** Using REST with jQuery *** //
function getContactsUsingJQuery() {
if (Microsoft.Live.App.get_auth().get_state() === Microsoft.Live.AuthState.authenticated) {
try {
jQuery.ajax({
url: uriContactGet.replace('{0}', cid) + "?$top=" + displayNumContacts,
type: "GET",
async: true,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "WRAP access_token=" + accessToken);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Accept", "application/json");
},
success: function (data, status, xhr) {
var contactsCollection = data.Entries;
for (var i = 0; i < Math.min(contactsCollection.length, displayNumContacts); i++) {
if (contactsCollection[i] !== undefined) {
var contact = contactsCollection[i];
addItemToList("ContactsRESTJQuery", contact.FirstName + " " + contact.LastName);
}
}
},
complete: function (xhr, status) {
var response = status "no response text";
log("Ajax request completed with " + response + ".");
},
ajaxError: function (data) {
var response = data "no response text";
log("Failure: " + response);
},
error: function (xhr, status, err) { log('Exception: ' + status + ', status = ' + xhr.status + ', message = ' + xhr.statusText); }
});
}
catch (err) {
log("Could not complete a status request. Is cross domain access enabled? Message = " + err);
}
}
else {
log("Failed to get contacts using jQuery Ajax call. Are you authenticated?");
}
}
function addItemToList(parent, item) {
$get(parent).innerHTML += "<li>" + item + "</li>";
}
function log(msg) {
$get('Output').innerHTML += msg + ";";
}

</script>
<style type="text/css">
div.left { float: left; width: 300px; border: 1px solid blue; min-height: 250px; padding: 5px;}
div.output {clear: both;}
ul { list-style-type: none;}
</style>
</head>
<body>
<wl:app
channel-url="<%=WebConfigurationManager.AppSettings["wl_wrap_channel_url"]%>"
callback-url="<%=WebConfigurationManager.AppSettings["wl_wrap_client_callback"]%>?<%=SessionId%>"
client-id="<%=WebConfigurationManager.AppSettings["wl_wrap_client_id"]%>"
scope="WL_Contacts.View, WL_Profiles.View"
onload="appLoaded">
</wl:app>
<form id="form1" runat="server">
<div>
<h1>
Getting Contacts - Comparing Three Different Approaches</h1>
<wl:signin onsignin="SignedInCallback" onsignout="SignedOutCallback" signedouttext="Sign In To Authenticate and Begin">
</wl:signin>
<div class="left">
<h4>Using the JavaScript API.</h4> <input type="button" value="Fetch" onclick="getContactsUsingAPI()" />
<ul id="ContactsAPI">
</ul>
</div>
<div class="left">
<h4>Using REST with jQuery Ajax request.</h4> <input type="button" value="Fetch" onclick="getContactsUsingJQuery()" />
<ul id="ContactsRESTJQuery">
</ul>
</div>
<div class="left">
<h4>Using REST with C# HttpWebRequest.</h4> <asp:Button ID="GetContactsButton"
runat="server" Text="Fetch" onclick="GetContactsButton_Click" />
<ul id="ContactsRESTCodeBehind" runat="server">
</ul>
</div>
<div class="output"><span id="Output" runat="server" /></div>
</div>
</form>
</body>
</html>


The Page Code-Behind

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
// Additional using statements.
using System.Net;
using System.Web.Configuration;
using System.IO;
using Microsoft.Live;
using System.Xml.Linq;

namespace WindowsLiveWebApplicationBeta
{
public class AuthStoreProvider : Microsoft.Live.IAuthStoreProvider
{
public void StoreToken(HttpContext context, string userId, IDictionary<string, string> authInfo)
{
// Get and store the AccessToken.
AccessToken = authInfo["AccessToken"];
CID = authInfo["UID"];
}
public static string AccessToken { get; set; }
public static string CID { get; set; }
}
public partial class ContactsComparison : System.Web.UI.Page
{
public string SessionId
{
get
{
SessionIdProvider oauth = new SessionIdProvider();
return "wl_session_id=" + oauth.GetSessionId(HttpContext.Current);
}
}

protected void Page_Load(object sender, EventArgs e)
{

}

string baseApplicationsEndpoint = "http://apis.live.net/V4.0/cid-{0}/Contacts/AllContacts";
string ApplicationsEndpoint = "";
string MaxNumToDisplay = "5";
string br = "<>";

// *** Using REST with HttpWebRequest *** //
protected void GetContactsButton_Click(object sender, EventArgs e)
{
ApplicationsEndpoint = baseApplicationsEndpoint.Replace("{0}", AuthStoreProvider.CID) + "?$top=" + MaxNumToDisplay;
try
{
// Initialize a request and define its characteristics.
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ApplicationsEndpoint);

request.Method = "GET";
request.Accept = "application/atom+xml";
request.ContentType = "application/atom+xml";

// Get the AccessToken stored when the user signed in.
request.Headers.Add("Authorization", "WRAP access_token=" + AuthStoreProvider.AccessToken);

// Get the client Id from the web.config.
request.Headers.Add("X-HTTP-LiveFX-ApplicationId", WebConfigurationManager.AppSettings["wl_wrap_client_id"]);

// Send the request and get the response.
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

Stream responseStream = response.GetResponseStream();
StreamReader streamReader = new StreamReader(responseStream);

string responseText = streamReader.ReadToEnd();

CreateContactList(responseText);

streamReader.Close();
responseStream.Close();
}
catch (WebException Ex) // Catch is reached when the request is not successful.
{
HttpWebResponse response = (HttpWebResponse)Ex.Response;

Stream responseStream = response.GetResponseStream();
StreamReader streamReader = new StreamReader(responseStream);

string errorText = streamReader.ReadToEnd();
Output.InnerHtml += errorText;
Output.InnerHtml += br;
Output.InnerHtml += "Access Token = " + AuthStoreProvider.AccessToken + br;
Output.InnerHtml += "CID = " + AuthStoreProvider.CID + br;

streamReader.Close();
response.Close();
}
}
protected void CreateContactList(string input)
{
XNamespace ns = "http://www.w3.org/2005/Atom";
XNamespace nsp3 = "http://schemas.microsoft.com/ado/2007/08/dataservices"; // p3
TextReader tr = new StringReader(input);
XDocument xdoc = XDocument.Load(tr);
var contentQuery = from entry in xdoc.Descendants(ns + "content")
select entry;

foreach (XElement contentElement in contentQuery)
{
string firstName = contentElement.Descendants(nsp3 + "FirstName").FirstOrDefault().Value;
string lastName = contentElement.Descendants(nsp3 + "LastName").FirstOrDefault().Value;
ContactsRESTCodeBehind.InnerHtml += "<li>" + firstName + " " + lastName + "</li>";
}
}
}
}


Web.config key

<add key="wl_wrap_store_provider_type"
value="WindowsLiveWebApplicationBeta.AuthStoreProvider,WindowsLiveWebApplicationBeta"/>


Explanation of the code:

  1. Approach 1 (Javascript API) is all based off the loader (http://js.live.net/4.1/loader.js) which figures out what is needed for your page and enables you to work with a data context, ask for resources through the data context, and then work with the returned resource collection. Behind the scenes it is using the REST endpoints similarly to what is done in Approach 2 and 3.
  2. Approach 2 (REST with jQuery) depends on an endpoint for the resource you are interested in (e.g. http://apis.live.net/V4.1/cid-{0}/Contacts/AllContacts). The {0} value is replaced with the unique ID of the authenticated user Windows Live User. How is this endpoint URI known? You can figure it out from using the Windows Live Rest Explorer or you can look in the documentation, for example, for the AllContacts collection documentation page.
  3. Approach 2 requires the access token to be sent as a header as part of the AJAX request. The access token can be obtained from the Auth class.
  4. Approach 3 (REST in code-behind) also depends on an endpont for the resource. In this example it's defined in the code-behind similarly to Approach 2.
  5. Approach 3 as well depends on putting the access token into the request header. It's a bit more complicated in this case because you need to define a class (AuthStoreProvider in this code exampled) to implement the IAuthStoreProvider interface and get access to the access token. Finally, you need to make sure you hook up your AuthStoreProvider with an app key in the web.config file shown above.

A screenshot of the resulting page is at the beginning of this post. No surprises, each approach returns the same data! For more information on using LINQ with the Atom feed see this post. Finally, if you are using this in a web farm scenario you might consider storing the access token and cid in a cookie instead of using a static as shown here.

17 comments:

  1. Ho do you have samples in zip?

    ReplyDelete
  2. I updated the post with a link to the zip file. The link is this: http://cid-3faa5c9b69a80c67.office.live.com/self.aspx/Public/ContactsComparison.aspx.zip

    ReplyDelete
  3. Hi Marx... well i found many many problems to implement AuthStoreProvider you can explain a bit more...

    ReplyDelete
  4. Where did you find problems? In the code shown above? In the downloadable zip? What kind of problems? I just tried my version of the code and it produces the screenshot shown at the start of the post.

    ReplyDelete
  5. where can i get a working example but using PHP?

    ReplyDelete
  6. Sorry, I don't know. I didn't create one for this post. Want to contribute one?

    ReplyDelete
  7. Question - while trying out your code, I'm getting the following when loading data via jQuery.ajax(() :
    XMLHttpRequest cannot load http://apis.live.net/V4.1/cid-93c5812f08131729/Contacts/AllContacts. Origin http:// is not allowed by Access-Control-Allow-Origin.

    (mysite is registered with/under Live dev, I and did put my clientID and secret)

    Any idea why would it not allow?
    many thanks in advance,
    Greg

    ReplyDelete
  8. What browser are you using?

    I just tried my test site and everything works with IE as the browser. I recently upgraded to IE 9 and when I first tried the jQuery request it failed. I had to then go into IE and set the "Access data sources across domains". This setting is IE-specific and is shown here really just for demonstration. To set: go to IE Internet Options, Security Tab, Custom Level, and find the Miscellaneous settings. For more information see http://www.webdavsystem.com/ajax/programming/cross_origin_requests. What I don't know is if the Messenger Connect endpoints support CORS headers. I'd like to know more to make it work for all browsers.

    ReplyDelete
  9. using the javascript i keep getting
    Exception: error, status = 0, message = error

    ReplyDelete
  10. I'm using the c# code behind example

    int the beginning i was receiving he contacts email's but then suddenly no emails any more in the json response

    ReplyDelete
  11. Tala: I just tried the JavaScript sample and it works for me. Any more info to troubleshoot the problem? Try to get the specifics of the error with the .message property.

    Anonymous: I just tried the C# code-behind sample and it works for me. Any more info to troubleshoot?

    ReplyDelete
  12. the # code-behind sample and it works for me
    the only problem that it is not returning emails addresses of my contacts :
    2011-06-02 17:18:28->{"entries":[{"connected":"false","emails":[],"id":"urn:uuid:WLNVLZNHPV4UHKRSJCW4XBOAXQ","name":{"familyName":"בוגבוג","formatted":"טל בוגבוג","givenName":"טל"},"phoneNumbers":[],"updated":"2011-05-30T09:53:33","urls":[]},{"connected":"false","emails":[],"id":"urn:uuid:6XNC4WTFUJUENDULPOPXRXWWP4","name":{"familyName":"גרב","formatted":"איציק גרב","givenName":"איציק"},"phoneNumbers":[],"updated":"2011-05-30T09:53:16","urls":[]},{"connected":"false","emails":[],"id":"urn:uuid:PVWGKDUDFEEUDLSLZC5E2RQSJQ","name":{"familyName":"גרבר","formatted":"יצחק גרבר","givenName":"יצחק"},"phoneNumbers":[],"updated":"2011-05-29T06:04:19","urls":[]},{"connected":"true","emails":[],"id":"f469c7fe3abb6b2c","name":{"familyName":"Farber","formatted":"Oren Farber","givenName":"Oren"},"phoneNumbers":[],"updated":"2011-05-29T06:12:20","urls":[{"type":"profile","value":"http:\/\/profiles.apis.live.net\/V4.1\/cid-f469c7fe3abb6b2c\/Profiles\/"},{"type":"statusMessage","value":"http:\/\/psm.apis.live.net\/V4.1\/cid-f469c7fe3abb6b2c\/StatusMessage"}]},{"connected":"false","emails":[],"id":"urn:uuid:LN7MCMWW3GHUJOXW26X4QV4ARA","name":{"familyName":"יובל","formatted":"אילנה יובל","givenName":"אילנה"},"phoneNumbers":[],"updated":"2011-05-29T06:14:29","urls":[]}],"itemsPerPage":5,"startIndex":0,"totalResults":5}

    ReplyDelete
  13. see also
    http://code.google.com/p/socialauth/issues/detail?id=67

    ReplyDelete
  14. The history of the Messenger Connect service/product is that at first they allowed emails to be accessed, but then after some initial thinking and feedback pulled that feature. So if a user (U1) logs into your Messenger Connect-enabled site and you are using the scope WL_Contacts.View (http://msdn.microsoft.com/en-us/library/ff749529.aspx) then you will not see the emails of the user's contacts. You can work with the CID of the U1's contacts and that allows you (the web site owner) to provide many social features still. I believe you have to ask Windows Live for an additional scope WL_Contacts.View_Full(?) in order to be able to access email addresses. I would expect that they don't enable just anyone. So I would try to think of what you are attempting to do with your code and see if you can do it with the CIDs. You can still do a lot, like send emails just knowing the CID of user. Hope this helps.

    ReplyDelete
  15. you have to use WL_Profiles.View as control, and in the javascript code you do contactsCollection.get_item(i).get_emails(), which returns an array of contactEmail then you can call the method get_address() on each contactEmail.
    VM.

    ReplyDelete
  16. Really? When I try it with WL_Profile.View (which is used in the example in this post) I get undefined. The code shown in the post can can be easily modified in the contactsLoaded JavaScript function to add "contact.get_emails()[0].get_address()" and when I add it I see nothing. Do you have a working example or can figure out what I'm doing wrong? Thanks.

    ReplyDelete

All comments go through a moderation process. Even though it may not look like the comment was accepted, it probably was. Check back in a day if you asked a question. Thanks!