Mobile Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Visual Studio Achievements for Windows Phone - Basic User Comparison

12.15.2011
| 2740 views |
  • submit to reddit

Previous parts:

Comparing achievements is an interesting activity because it is behind the idea of user competitiveness. People tend to attempt more when they see their peers tackling similar or more challenging problems that they haven't tried yet. If you used Xbox Live before, you know that there is a dedicated section both on the console and on the XBL website that allows you to compare your achievements with the ones earned by your friends.

I thought that the same functionality should be available in my Visual Studio Achievements for Windows Phone app. The first idea was to implement a mash-up that would let me compare an unlimited number of Niners. A horizontal StackPanel with multiple users seemed like an intriguing idea, but I had to go back to the easiest implementation first - compare two Niners only.

I would need a comparison page, that would have a structure similar to this one:

A split page should accommodate all general comparison needs. The top row will show the user avatar, his name and the number representing the earned points. Under each user metadata cell will be the earned achievements. In case one of the users has an achievement while the other one doesn't, a lock will be displayed with a "Not Earned" caption.

Getting the Niners that should be compared

First things first - the application user needs to specify two Niners that will be compared. This can be easily done by implementing a prompt similar to the one I used to add new Niners to the general watchlist, and that is exactly what I did. With minor modifications, I ended up with this XAML structure for ComparePrompt.xaml:

<phone:PhoneApplicationPage 
    x:Class="VisualStudioAchievements.ComparePrompt"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480"
    shell:SystemTray.IsVisible="False">

    <Grid x:Name="LayoutRoot" Background="Black">
        <StackPanel Margin="20,20,20,90">
            <TextBlock Margin="15,0,15,0" Text="Compare Users" FontSize="{StaticResource PhoneFontSizeLarge}" FontFamily="{StaticResource PhoneFontFamilySemiBold}"></TextBlock>
            <TextBlock Margin="15,10,15,0" TextWrapping="Wrap" Text="First User:" FontSize="{StaticResource PhoneFontSizeMediumLarge}" FontFamily="{StaticResource PhoneFontFamilyNormal}"></TextBlock>
            <TextBox x:Name="txtFirst" Margin="0,10,0,0"></TextBox>
            <TextBlock Margin="15,10,15,0" TextWrapping="Wrap" Text="Second User:" FontSize="{StaticResource PhoneFontSizeMediumLarge}" FontFamily="{StaticResource PhoneFontFamilyNormal}"></TextBlock>
            <TextBox x:Name="txtSecond"  Margin="0,10,0,0"></TextBox>
        </StackPanel>
        <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal">
            <Button x:Name="btnOK" Content="OK" Width="240" Click="btnOK_Click"></Button>
            <Button x:Name="btnCancel" Content="Cancel" Width="240"></Button>
        </StackPanel>
    </Grid>

</phone:PhoneApplicationPage>

This results in a page that looks like this:

Keeping things simple, I implemented a very simple mechanism that reads metadata associated with the two users that are selected.

private void btnOK_Click(object sender, RoutedEventArgs e)
{
    if ((!string.IsNullOrWhiteSpace(txtFirst.Text)) && (!string.IsNullOrWhiteSpace(txtSecond.Text)))
    {
        BindingPoint.ComparedNiners = new CompareNinerPair();

        var reader = new NinerReader();
        reader.GetNiner(txtFirst.Text, true, () =>
            {
                BindingPoint.ComparedNiners.FirstNiner = reader.CurrentNiner;
                reader.GetNiner(txtSecond.Text, true, () =>
                    {
                        BindingPoint.ComparedNiners.SecondNiner = reader.CurrentNiner;
                        Util.ListComparedAchievements();

                        NavigationService.Navigate(new Uri("/CompareView.xaml", UriKind.Relative));
                    });
            });
    }
}

If both TextBox controls are not empty, I am assuming that both names that are going to be compared are valid. I am creating a new comparison pair - CompareNinerPair:

public class CompareNinerPair
{
    public Niner FirstNiner { get; set; }
    public Niner SecondNiner { get; set; }
}

With a custom class, it is much easier to manage future implementations, so KeyValuePair was out of the picture. You probably noticed by now that NinerReader.GetNiner now has three parameters instead of the regular two that I used before. Even more than that - the third parameter is a custom delegate. Why is it so?

I re-implemented some parts of that class to make it reusable in different parts of the application. Before, it was simply used to add new Niner instances to the main collection. Now, it can do anything I want with the Niner instance after it is obtained, and that custom action is passed through the delegate.

Internal modifications to NinerReader are looking like this. First, I added the delegate declarations in the class header:

public delegate void HelperDelegate();
private static HelperDelegate helperDelegateInstance;

Looking through the previous version of the class,  you can see that the last executed action is CompleteAchievementsRequest - a method that parses the list of achievements and associtates them with the current Niner instance (represented by the CurrentNiner property). I replaced the code that added the Niner instance to BindingPoint.Niners with this line:

App.MainPageDispatcher.BeginInvoke(new Action(() => helperDelegateInstance()));

AddNiner was renamed to GetNiner and helperDelegateInstance is initated on invocation:

public void GetNiner(string name, bool isInit, HelperDelegate ninerDelegate = null)
{
    if (ninerDelegate != null)
        helperDelegateInstance = ninerDelegate;

Let's get back to the code handling the OK click. When the first Niner is acquired, I use the same NinerReader instance to get the second one. Both will be a part of the CompareNinerPair. Once the requests are completed, I am using Util.ListComparedAchievements to populate a dedicated ObservableCollection<CompareAchievementPair> instance in BindingPoint:

public static ObservableCollection<CompareAchievementPair> ComparedAchievements { get; set; }

CompareAchievementPair is a simple pair of Achievement instances that is associated with the compared users:

public class CompareAchievementPair
{
    public Achievement FirstAchievement { get; set; }
    public Achievement SecondAchievement { get; set; }
}

So how exactly are achievements sorted and compared? There are two foreach loops that iterate through achievements present in both Niner instances to see which ones are locked and which are available on both sides:

public static void ListComparedAchievements()
{
    BindingPoint.ComparedAchievements = new ObservableCollection<CompareAchievementPair>();
    // There is going to be at least one achievement for a single user - the one 
    // assigned on registration. Currently there is no need to check whether the achievement
    // list is empty.
    foreach (var achievement in BindingPoint.ComparedNiners.FirstNiner.Achievements)
    {
        var pair = new CompareAchievementPair();
        pair.FirstAchievement = achievement;

        try
        {
            // Comparison is done through FriendlyName and not by the instance because of different earned dates.
            var secondAchievement = (from c in BindingPoint.ComparedNiners.SecondNiner.Achievements where c.FriendlyName == achievement.FriendlyName select c).Single();
            pair.SecondAchievement = secondAchievement;
        }
        catch
        {
            pair.SecondAchievement = NotEarnedAchievement;
        }

        BindingPoint.ComparedAchievements.Add(pair);
    }

    foreach (var achievement in BindingPoint.ComparedNiners.SecondNiner.Achievements)
    {
        try
        {
            var selectedAchievement = (from c in BindingPoint.ComparedAchievements where c.SecondAchievement.FriendlyName == achievement.FriendlyName select c).Single();
        }
        catch
        {
            var pair = new CompareAchievementPair();
            pair.FirstAchievement = NotEarnedAchievement;
            pair.SecondAchievement = achievement;
            BindingPoint.ComparedAchievements.Add(pair);
        }
    }
}

NotEarnedAchievement doesn't change and is used as a placeholder:

public static Achievement NotEarnedAchievement = new Achievement() { FriendlyName = "Not Earned", Icon = new Uri("/Images/locked.png", UriKind.Relative) };

That pretty much concludes the code part.

Displaying the compared achievements

I created a separate page that has a split format, like I mentioned above - CompareView.xaml.

<phone:PhoneApplicationPage 
    x:Class="VisualStudioAchievements.CompareView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignHeight="768" d:DesignWidth="480"
    shell:SystemTray.IsVisible="True"
    xmlns:local="clr-namespace:VisualStudioAchievements">

    <phone:PhoneApplicationPage.Resources>
        <local:BindingPoint x:Key="LocalBindingPoint"></local:BindingPoint>
    </phone:PhoneApplicationPage.Resources>

    <Grid Background="White" x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="200"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal"  DataContext="{Binding Path=ComparedNiners,Source={StaticResource LocalBindingPoint}}">
            <StackPanel Width="240" DataContext="{Binding FirstNiner}">
                <Image Height="120" Source="{Binding Avatar}"></Image>
                <TextBlock Text="{Binding Name}" Foreground="Black"></TextBlock>
                <TextBlock Text="{Binding Points}" Foreground="Black"></TextBlock>
            </StackPanel>
            <StackPanel Width="240" DataContext="{Binding SecondNiner}">
                <StackPanel Width="240">
                    <Image Height="120" Source="{Binding Avatar}"></Image>
                    <TextBlock Text="{Binding Name}" Foreground="Black"></TextBlock>
                    <TextBlock Text="{Binding Points}" Foreground="Black"></TextBlock>
                </StackPanel>
            </StackPanel>
        </StackPanel>

        <ListBox Grid.Row="1" ItemsSource="{Binding Path=ComparedAchievements,Source={StaticResource LocalBindingPoint}}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" >
                        <StackPanel Width="240" DataContext="{Binding FirstAchievement}">
                            <Image Height="64" Source="{Binding Icon}"></Image>
                            <TextBlock Text="{Binding FriendlyName}" Foreground="Black"></TextBlock>
                            <TextBlock Text="{Binding Points}" Foreground="Black"></TextBlock>
                        </StackPanel>
                        <StackPanel Width="240" DataContext="{Binding SecondAchievement}">
                            <Image Height="64" Source="{Binding Icon}"></Image>
                            <TextBlock Text="{Binding FriendlyName}" Foreground="Black"></TextBlock>
                            <TextBlock Text="{Binding Points}" Foreground="Black"></TextBlock>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</phone:PhoneApplicationPage>

I am binding it to the same BindingPoint static class that contains the achievement and the compared user pairs. When the application is launched, the compared data can look similar to this:

This doesn't look too pretty, but it works. It is time to modify the XAML a little bit to make the data more attractive. Here is what I came up with:

Not perfect, but way better than the initial version. You can pull the latest version here.