Figure 1. Game screen
Figure 2. Player's profile screen
Figure 3. Global top player's scoreboard
Table of Contents
As of December 2010, Windows Phone 7 devices lack native support for databases. Yes, there are third-party solutions for this problem, but they are either paid, like Perst DB, or not stable enough like the Windows Phone 7 Database on CodePlex.
When I started the development of my first really simple application, BubbleWrap (I know every mobile platform has this kind of an app), I never thought that I would need a database. Then I came to read user comments on this web-site: Windows Phone 7 AppReviews (it just display user comments from all markets combined). The reviews were mostly negative; users blamed me for not including any scoring system in my app.
In my opinion, a local (on the phone) top players score board would be boring and not competitive, so I started thinking about using Microsoft Azure cloud technologies. When I searched for possible solutions in the Internet, lots of people said: “Yes, Windows Phone 7 phones are cool because they allow easy integration with the cloud.” But finding a working example was not an easy task.
For the top players score board, I needed the SQL Azure database. I had to be able to write and read from a database. But Windows Phone 7 has no classes do to that! Yes, there is a version of OData client for Windows Phone 7, but this project is in CTP state, and can only read from a database. I needed something really stable and simple, and I needed to record player results to a database. Everything changed when I saw Steve Marx's session: “Building Windows Phone 7 Applications with the Windows Azure Platform” at PDC10. He showed how to use the System.Net.WebClient
class to access an Azure Web Service. “Eureka!” – I said. Windows Phone 7 has a WebClient that can call Azure WebService methods with parameters, and an Azure WebService can have full access to a SQL Azure database (no need to even set up firewall rules)!
Thus, I developed my solution, and now I want to share it with you.
I started learning how to program for Windows Phone 7 OS about two months ago. I don’t know how to make complex and beautiful user interfaces for this platform, so I made the simplest dialog possible using Silverlight. Here is the scheme:
As you can see, I did not make several dialogs; instead, I use three grids and change their Visibility
property on button clicks.
To create a screen with bubbles, I just made a grid with enough rows and columns and placed image elements in it. Important: each bubble should span two columns (thus we get a distinctive pattern). And don’t forget to put the appropriate image on the background!
<Grid x:Name="LayoutRoot" Grid.Row ="1">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.Background>
<ImageBrush ImageSource="..\Resources\Images\image 90.jpg"/>
</Grid.Background>
-->
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="1"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="3"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="5"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="7"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="9"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="11"
Grid.ColumnSpan="2" Grid.Row="0"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
-->
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="0"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="2"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="4"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="6"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="8"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="10"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<Image MouseLeftButtonDown="ImageMouseLeftButtonDown" Grid.Column ="12"
Grid.ColumnSpan="2" Grid.Row="1"
Source="..\Resources\Images\image 60.png" Stretch="Fill" />
<!—other rows-->
</Grid>
For images, we should set Build Action as “Resource”.
When we push a bubble, it should remain pressed. So we add additional pictures on popped bubbles as app Resources.
<phone:PhoneApplicationPage.Resources>
<Image x:Name="original" Source="..\Resources\Images\image 60.png"/>
<Image x:Name="pushed1" Source="..\Resources\Images\image 62.png"/>
<Image x:Name="pushed2" Source="..\Resources\Images\image 65.png"/>
<Image x:Name="pushed3" Source="..\Resources\Images\image 68.png"/>
<Image x:Name="pushed4" Source="..\Resources\Images\image 71.png"/>
<Image x:Name="pushed5" Source="..\Resources\Images\image 74.png"/>
<Image x:Name="pushed6" Source="..\Resources\Images\image 77.png"/>
</phone:PhoneApplicationPage.Resources>
There are two ways for playing sounds in Windows Phone 7 applications.
First is by using the MediaElement
control. This is the easiest way, but it has drawbacks. You can’t put more than one MediaElement
in a dialog, and changing sound tracks in this control is a little bit complicated. For my applications, I needed several sounds as when bubbles burst, they provide different kinds of sounds.
So I went a different way by adding XNA framework support to my Silverlight application. All I did was just add Microsoft.Xna.Framework
to my application as a reference assembly. In this case, you can use XNA to play your sounds (*.wav format only).
All you need to play sound is:
- Add the required
using
directives:
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
- Call:
var stream = TitleContainer.OpenStream("sound 1.wav");
var effect = SoundEffect.FromStream(stream);
FrameworkDispatcher.Update();
effect.Play();
- I’ve placed sounds in the project directory and set their building action as Content.
All bubbles react on only one event: MouseLeftButtonDown
. I use a random variable to detect which picture to put and what sound to play. To change the image, you have to set a new Source
for it.
private void ImageMouseLeftButtonDown(object sender,
System.Windows.Input.MouseButtonEventArgs e)
{
var image = sender as Image;
if (image == null)
return;
var bitmapImage = image.Source as BitmapImage;
var originalImage = original.Source as BitmapImage;
if (bitmapImage == null)
return;
if (originalImage == null)
return;
if (bitmapImage.UriSource != originalImage.UriSource)
return;
switch (_random.Next(5))
{
case 0:
image.Source = pushed1.Source;
break;
case 1:
image.Source = pushed2.Source;
break;
case 2:
image.Source = pushed3.Source;
break;
case 3:
image.Source = pushed4.Source;
break;
case 4:
image.Source = pushed5.Source;
break;
case 5:
image.Source = pushed6.Source;
break;
}
settings[_playerName + "score"] = _score++;
Score.Text = ScoreText.Text = "Bubbles popped so far: " + _score;
Stream stream = null;
switch (_random.Next(5))
{
case 0:
stream = TitleContainer.OpenStream("sound 64.wav");
break;
case 1:
stream = TitleContainer.OpenStream("sound 67.wav");
break;
case 2:
stream = TitleContainer.OpenStream("sound 70.wav");
break;
case 3:
stream = TitleContainer.OpenStream("sound 73.wav");
break;
case 4:
stream = TitleContainer.OpenStream("sound 76.wav");
break;
case 5:
stream = TitleContainer.OpenStream("sound 79.wav");
break;
}
if (stream == null)
return;
var effect = SoundEffect.FromStream(stream);
FrameworkDispatcher.Update();
effect.Play();
}
Sometimes, we run out of bubbles. In that case, we have to replace all bubble images with new sources. This is what the button “Want more!” is doing:
if (!Renovate())
return;
var stream = TitleContainer.OpenStream("sound 1.wav");
var effect = SoundEffect.FromStream(stream);
FrameworkDispatcher.Update();
effect.Play();
Here is the Renovate()
function main code:
foreach (var image in LayoutRoot.Children.Select(child => child as Image))
{
if (image == null)
return false;
var bitmapImage = image.Source as BitmapImage;
var originalImage = original.Source as BitmapImage;
if (bitmapImage == null)
return false;
if (originalImage == null)
return false;
image.Source = original.Source;
}
So far, we have the basic functionality: our bubbles are popping and sounds are playing.
The next thing is to make simple user profiles. I thought about it like this: let’s say every phone has a GUID (it is generated by the app itself and not users - they should be unaware of its existence) associated with it. Then, we have a player and his scores and the number of attempts.
Windows Phone 7 has a system called “Tombstoning” for saving app state. I read about it from Jeff Blankenburg's article.
Now I will show you how I am using it.
- Declare a variable that represents the isolated storage.
IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
- Create the phone GUID (only once). It is actually very handy if you want to display all players’ results from a particular phone (e.g., local top).
private void InitializePhoneGuid()
{
if (settings.Contains("guid"))
{
_phoneGuid = (Guid) settings["guid"];
}
else
{
_phoneGuid = Guid.NewGuid();
settings.Add("guid", _phoneGuid);
}
}
- Player’s name validation: this is a little tricky. If we already have somebody who played the game, we should just load his name from local storage and load the score and attempts, else we should take the user to the Settings menu and force him to enter his name.
private void InitializePlayerName()
{
if (settings.Contains("player"))
{
_playerName = (string) settings["player"];
PlayerName.Text = _playerName;
PlayerNameTextBox.Text = _playerName;
InitializeScore();
InitializeAttempts();
ShowGrid(Grids.LayoutRootGrid);
}
else
{
ShowGrid(Grids.SettingsGrid);
}
}
- When we are loading the player’s attempts and scores, we should use his name.
private void InitializeScore()
{
if (settings.Contains(_playerName + "score"))
{
_score = (int)settings[_playerName + "score"];
}
else
{
_score = 0;
settings.Add(_playerName + "score", 0);
}
Score.Text = ScoreText.Text = "Bubbles popped so far: " + _score;
}
private void InitializeAttempts()
{
if (settings.Contains(_playerName + "attempts"))
{
_attempts = (int)settings[_playerName + "attempts"];
}
else
{
_attempts = 0;
settings.Add(_playerName +"attempts", 0);
}
settings[_playerName + "attempts"] = _attempts++;
Attempts.Text = "You had " + _attempts + " attempts";
}
- As for the player’s name validation, I am using two simple rules:
- It should be different from the default “Player”
- It should not be empty
private bool CheckValidPlayer()
{
var playerNameString = PlayerNameTextBox.Text;
playerNameString = playerNameString.Trim();
if (String.IsNullOrEmpty(playerNameString))
{
MessageBox.Show("Player's name could not be empty.");
return false;
}
if (String.Compare(playerNameString, "Player",
StringComparison.InvariantCultureIgnoreCase) == 0)
{
MessageBox.Show("You can't set default name \"Player\".");
return false;
}
{
_playerName = playerNameString;
if (settings.Contains("player"))
{
settings["player"] = _playerName;
}
else
{
settings.Add("player", _playerName);
}
PlayerName.Text = _playerName;
PlayerNameTextBox.Text = _playerName;
}
InitializeScore();
InitializeAttempts();
return true;
}
So we now have a simple game and we save the player’s scores and number of attempts. It is time to put this data to the cloud so everyone can see.
In my solution, I am using two Azure technologies: Azure SQL Database and a Hosted Service. I have to say that using cloud technologies turned out to be relatively simple!
So far, this is the most exciting part of my article!
We will start with the creation of the Azure SQL database. For this, we will use SQL Server Management Studio from Microsoft SQL Server 2008 R2. I’ve got my own copy as a participant of the BizSpark program, I guess you can try too.
Note: for security purposes, I will hide my real server address, so the provided solution won’t work until you enter the name of your server.
Note 2: some parts of this might repeat the Windows Azure Platform training kit. Really, it is hard to tell something completely new on this subject.
If you have already got a chance to get the Windows Azure platform 30 day pass, then you would be able to login with your credentials to http://windows.azure.com and see the following starting page:.
By clicking the Database button, you will see your server information and your current databases.
If you don’t have a database server created yet, then click the “Create a new server” button and follow the instructions. Unfortunately, I cannot show you the process as there is a limit to only one Azure SQL Server per subscription.
Don’t forget to set Firewall Rules to allow access for yourself and Windows Azure services:
Now it’s time to start Microsoft SQL Server Management Studio. Make sure you’ve entered your server name and selected SQL Server Authentication. Login with your administrator login and password.
Then click Options and enter “master” in the Connect to database field.
Congratulations, you’ve connected to the master database!
Let’s now create a new database to save the players top scores. Execute the following query:
Create Database TopScore
If the query executed successfully, you will see the following message:
Now, we will make credentials for the new user:
CREATE LOGIN
SomeUser WITH password='SomePassword1'
GO
Now we should connect to the newly created database with admin credentials:
Change the database name in the Connection Properties tab.
We will create the user with the login and password we entered before:
CREATE USER SomeUser FROM LOGIN SomeUser
GO
Make sure you are making this query to the database TopScore, not the master database. Otherwise you will create a new user for your master database.
To check if the new user was created, open the Security>Users branch.
Now we will set SomeUser rights. Don’t forget that we will save scores to the database and load the top players list.
Execute two queries:
EXEC sp_addrolemember 'db_datareader', 'SomeUser'
GO
EXEC sp_addrolemember 'db_datawriter', 'SomeUser'
GO
Now only one thing is left: we should create a table where we will record the player’s scores:
CREATE TABLE BubbleWrapTopScore(
[PhoneId] [nvarchar](50) NOT NULL,
[PlayerName] [nvarchar](50) NOT NULL,
[Score] [int] NULL,
[Attempts] [int] NOT NULL
CONSTRAINT PKplayer PRIMARY KEY ([PhoneId],[PlayerName])
)
This is a very simple table: we save only the phone ID, player’s name, scores, and number of attempts. In the future, we can add more columns, for example, time of record, show best player of the day, of month, and so on. Or you can add columns to show if a player reached, for example, some notable achievements.
We have to check one last thing: if we can actually connect with the new user's credentials. Disconnect from the database and connect again with his login and password.
Make sure that you are connecting to the right database:
The next task is to create a Windows Azure Cloud service that would actually do requests to the database.
Open Visual Studio, start a new project, select C#>Cloud> Windows Azure Cloud Service.
Enter a new name:
In my project, I am using ASP.NET MVC 2 Web Role. It is the easiest to implement in our case.
I decided not to create a unit test for my project.
We will modify the HomeController.cs file.
In our case, we will make a service that receives all data (server name, user password, and login etc.) from the Windows Phone 7 app, that greatly simplifies it.
Add two methods to HomeController
to save the results and to read them from the database:
public ActionResult SaveScore(string id)
{
try
{
var result = id.Split(',');
var databaseWrite =
new AdoConnection(result[0], result[1], result[2], result[3]);
databaseWrite.WriteToDataBase(result[4], result[5],
result[6], result[7], result[8]);
return Content("Score saved.");
}
catch(Exception)
{
return Content("Failed to save score.");
}
}
public ActionResult TopScore(string id)
{
try
{
var result = id.Split(',');
var databaseWrite = new AdoConnection(result[0],
result[1], result[2], result[3]);
var topScore = databaseWrite.ReadFromDataBase(result[4]);
return Content(topScore);
}
catch (Exception)
{
return Content("Failed to read top score table.");
}
}
Then add new classes to the same file:
public class AdoConnection : SqlAzureConnection
{
public AdoConnection(string userName, string password,
string dataSource, string databaseName)
: base(userName, password, dataSource, databaseName)
{
}
protected override DbConnection CreateConnection(string userName,
string password, string dataSource, string databaseName)
{
return new SqlConnection(CreateAdoConnectionString(userName,
password, dataSource, databaseName));
}
private static string CreateAdoConnectionString(string userName,
string password, string dataSource, string databaseName)
{
var connectionStringBuilder = new SqlConnectionStringBuilder
{
DataSource = dataSource,
InitialCatalog = databaseName,
Encrypt = true,
TrustServerCertificate = false,
UserID = userName,
Password = password,
};
return connectionStringBuilder.ToString();
}
protected override DbCommand CreateCommand(DbConnection connection)
{
return new SqlCommand { Connection = connection as SqlConnection };
}
}
public abstract class SqlAzureConnection
{
private readonly DbConnection _connection;
protected SqlAzureConnection(string userName, string password,
string dataSource, string databaseName)
{
_connection = CreateConnection(userName, password,
dataSource, databaseName);
}
protected abstract DbConnection CreateConnection(string userName,
string password, string dataSource, string databaseName);
protected abstract DbCommand CreateCommand(DbConnection connection);
public void WriteToDataBase(string tableName, string phoneGuid,
string playerName, string playerScore, string playerAttempts)
{
_connection.Open();
var command = CreateCommand(_connection);
command.CommandText="UPDATE " + tableName +
" SET Score = '" + playerScore +
"', Attempts = '" + playerAttempts +
"' WHERE PhoneId = N'" + phoneGuid +
"' AND PlayerName = N'" + playerName + "' " +
"IF @@ROWCOUNT=0 " +
"INSERT INTO " + tableName +
" (PhoneId, PlayerName, Score, Attempts) VALUES (N'" +
phoneGuid + "', N'" + playerName + "', " +
playerScore + ", " + playerAttempts + ")";
command.ExecuteNonQuery();
_connection.Close();
}
public string ReadFromDataBase(string tableName)
{
_connection.Open();
var command = CreateCommand(_connection);
command.CommandText =
"SELECT TOP 10 PlayerName, Score, Attempts FROM " +
tableName + " ORDER BY Score DESC, Attempts ASC";
IDataReader reader = command.ExecuteReader();
var result = "";
// loop over the results and write them out to the console
while (reader.Read())
{
for (var col = 0; col < reader.FieldCount; col++)
{
result += reader.GetValue(col) + ",";
}
}
reader.Close();
//removing last ',' sign
result = result.Substring(0, result.Length - 1);
return result;
}
}
Those two classes create a connection to the SQL Azure database and make the necessary queries. We submit and return data as comma separated values in a string.
To deploy the service on the server, do the following:
Right click on the SomeTopScoreCloudService project and select the “Publish” item.
Select the "Create Service Package Only" item.
This will create the necessary files for you.
Now go to your management portal. Open the "Hosted Services, Storage Accounts & CDN" dialog, and click on the "Create a new hosted service" button.
Enter data (name, URL, and so on) for your subscription, and point to the files that Visual Studio created for you on publishing.
Click OK and you’re done!
Note: The system will tell you that you are recommended to have two instances of the service running together all the time as that will create 99% uptime. Ignore it, as we have really low load on the service, and Azure Services might be pricy.
Now that we have the service running on Azure platform, we should add calls to the Web Service to our app.
I save database parameters as constants in my app. I know: this might not be very secure, but it is easier this way.
#region constants
const string UserName = "SomeUser";
const string Password = "SomePassword1";
const string Datasource = "[server].database.windows.net";
const string DatabaseName = "TopScore";
const string TableName = "TopScore";
#endregion
Now when the user wants to see the top users list, we do the following:
- Submit the score:
void SubmitScore()
{
var id = UserName + ',' + Password + ',' + Datasource + ',' +
DatabaseName + ',' + TableName + ',' +
_phoneGuid + ',' + PlayerName.Text + ',' +
_score + ',' + _attempts;
var client = new WebClient();
client.DownloadStringCompleted += SubmitScoreCompleted;
client.DownloadStringAsync(
new Uri("http://[your URL].cloudapp.net/home/SaveScore/" + id));
}
void SubmitScoreCompleted(object sender, DownloadStringCompletedEventArgs args)
{
try
{
MakeTopList();
}
catch (Exception)
{
}
}
- Ask for a list of top players:
void MakeTopList()
{
var id = UserName + ',' + Password + ',' + Datasource + ',' +
DatabaseName + ',' + TableName;
var client = new WebClient();
client.DownloadStringCompleted += MakeTopListCompleted;
client.DownloadStringAsync(
new Uri("http:// [your URL].cloudapp.net/home/TopScore/" + id));
}
void MakeTopListCompleted(object sender, DownloadStringCompletedEventArgs args)
{
try
{
if (String.IsNullOrEmpty(args.Result))
return;
var result = args.Result.Split(',');
var position = result.Length/3;
_playerPositions.Clear();
for (var i = 0; i < position; i++)
{
_playerPositions.Add(new PlayerPosition(i+1,
result[i * 3], result[i * 3 + 1], result[i * 3 + 2]));
}
topPlayersList.ItemsSource = _playerPositions;
}
catch (Exception)
{
MessageBox.Show("Can't connect to server.");
}
}
To show the list of top players, we have a special class:
public class PlayerPosition
{
public int Position { get; set; }
public string Name { get; set; }
public string Score { get; set; }
public string Attempts { get; set; }
public PlayerPosition(int position, string name,
string score, string attempts)
{
Position = position;
Name = name;
Score = score;
Attempts = attempts;
}
}
And a list with player positions:
List<PlayerPosition> _playerPositions;
In our dialog, we have a Grid
with a ListBox
:
<ListBox x:Name="topPlayersList">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,10,0,5">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="100"/>
<ColumnDefinition />
<ColumnDefinition Width="120"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock FontSize="44" Text="{Binding Position}"
Grid.RowSpan="2" FontWeight="Bold" />
<TextBlock FontSize="22" Text="{Binding Name}"
Grid.Column="1" Grid.ColumnSpan="4"
Margin="20,0,0,0" FontWeight="Bold" />
<TextBlock FontSize="22" Text="Score:"
Grid.Row="1" Grid.Column="1" Margin="20,0,0,0" />
<TextBlock FontSize="22" Text="{Binding Score}"
Grid.Row="1" Grid.Column="2" Margin="0,0,0,20" />
<TextBlock FontSize="22" Text="Attempts:"
Grid.Row="1" Grid.Column="3" Margin="20,0,0,0" />
<TextBlock FontSize="22" Text="{Binding Attempts}"
Grid.Row="1" Grid.Column="4" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now we have all the functionality implemented.
I’ve made my first application free, but Windows Azure Services are not. Having a virtual service running in a 24/7 regime might be very pricy. So I’ve decided to stick with the Microsoft Advertisement platform and place a banner in my application. I have to say it – that's worth 100%!
It is really simple to deploy. Here you can find all the instructions for how to do that.
In my article, I demonstrated a relatively simple approach of how you can add Windows Azure support to your application. It can be improved by adding more columns to the database table that would represent player's achievements, playing time, and so on. One important feature that I want to highlight is that you have the opportunity to make a single leaderboard for your Android, iOS, and Windows Phone 7 applications, because all your app needs is to make web requests to an Azure Service and get a response.
Any feedback would be greatly welcome!
I want to say thanks to Steve Marx for his wonderful session, and PDC10.