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 - What it looks like

11.27.2011
| 3631 views |
  • submit to reddit

As the core is already there, it is time to put earned achievements on the show. This is the interesting part, since in a mobile application the user experience is the key to its success. Although I am still improving the overall structure to make it ready for the Marketplace, you can see how I can get a gamertag-like experience for Visual Studio Achievements.

Main page

This is where most of the action takes place - the page that lists all associated Channel9 accounts and shows some basic metadata, such as last 5 achievements, user avatar and caption. To give you an idea of what it looks like, here is a sneak peek:

But first things first - in my main page I have the ApplicationBar enabled. Through it, I can add or remove Channel9 users. The XAML looks like this:

<phone:PhoneApplicationPage.ApplicationBar>
    <shell:ApplicationBar BackgroundColor="Black" IsVisible="True" IsMenuEnabled="True">
        <shell:ApplicationBarIconButton x:Name="btnAddUser" Click="btnAddUser_Click" IconUri="/Images/appbar.add.png" Text="add"/>
        <shell:ApplicationBarIconButton IconUri="/Images/appbar.delete.png" Text="remove"/>
        <shell:ApplicationBar.MenuItems>
            <shell:ApplicationBarMenuItem Text="settings"/>
            <shell:ApplicationBarMenuItem Text="about"/>
        </shell:ApplicationBar.MenuItems>
    </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Pretty simple. There is a button that allows adding an user and another one to remove the selected one. Let's look at what btnAddUser_Click looks like:

private void btnAddUser_Click(object sender, EventArgs e)
{
    NavigationService.Navigate(new Uri("/InputPage.xaml?title=add user&message=add channel9 user.", UriKind.Relative));
}

Nothing but a navigational reference to InputPage - the page that acts as a user input dialog. Let's look at that first.

Look carefully at the parameters I am passing above and at the page contents. As you can see, the text fields are not static - they do not have the content programmed in the page directly, but rather associated to the passed parameters. This adds more flexibility to the dialog and lets me re-use it in various parts of the application, if necessary.

This is possible by having custom properties and bindings. Here is what I have for it in the code-behind:

public InputItem Input
{
    get
    {
        return (InputItem)GetValue(App.Input);
    }
    set
    {
        SetValue(App.Input, value);
    }
}

public InputPage()
{
    InitializeComponent();
    this.Loaded += new RoutedEventHandler(InputPage_Loaded);
}

void InputPage_Loaded(object sender, RoutedEventArgs e)
{
    Input.Title = NavigationContext.QueryString["title"];
    Input.Message = NavigationContext.QueryString["message"];
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    NinerReader reader = new NinerReader();
    reader.AddNiner(Input.Content,true);

    NavigationService.GoBack();
}

There is a class of type InputItem, defined as this:

public class InputItem : INotifyPropertyChanged
{
    private string title;
    public string Title
    {
        get
        {
            return title;
        }
        set
        {
            if (value != title)
            {
                title = value;
                NotifyPropertyChanged("Title");
            }
        }
    }

    private string message;
    public string Message
    {
        get
        {
            return message;
        }
        set
        {
            if (value != message)
            {
                message = value;
                NotifyPropertyChanged("Message");
            }
        }
    }

    private string content;
    public string Content
    {
        get
        {
            return content;
        }
        set
        {
            if (value != content)
            {
                content = value;
                NotifyPropertyChanged("Content");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

It is nothing else but a class that is able to hold three values - the dialog title, message and default content. The class instance in the InputPage class is associated with a DependecyProperty, that is registered in the application constructor:

public static DependencyProperty Input = 
        DependencyProperty.Register("Input", typeof(InputItem), typeof(PhoneApplicationPage),
        new PropertyMetadata(new InputItem()));

When InputPage is loaded, parameter content is transferred to individual properties. In the XAML part of the page, each field is bound to the InputItem instance:

<Grid x:Name="LayoutRoot" Background="Black">
    <StackPanel Margin="20,20,20,90">
        <TextBlock Margin="15,0,15,0" Text="{Binding ElementName=MobileInputBox,Path=Input.Title}" FontSize="{StaticResource PhoneFontSizeLarge}" FontFamily="{StaticResource PhoneFontFamilySemiBold}"></TextBlock>
        <TextBlock Margin="15,10,15,0" TextWrapping="Wrap" Text="{Binding ElementName=MobileInputBox,Path=Input.Message}" FontSize="{StaticResource PhoneFontSizeMediumLarge}" FontFamily="{StaticResource PhoneFontFamilyNormal}"></TextBlock>
        <TextBox Margin="0,10,0,0" Text="{Binding ElementName=MobileInputBox,Path=Input.Content,Mode=TwoWay}"></TextBox>
    </StackPanel>
    <StackPanel VerticalAlignment="Bottom" Orientation="Horizontal">
        <Button Content="OK" Width="240" Click="Button_Click"></Button>
        <Button Content="Cancel" Width="240"></Button>
    </StackPanel>
</Grid>

When the user clicks on OK, I am initiating the process of adding a new Niner through NinerReader and at the same time I am navigating back because I no longer need user input:

private void Button_Click(object sender, RoutedEventArgs e)
{
    NinerReader reader = new NinerReader();
    reader.AddNiner(Input.Content,true);

    NavigationService.GoBack();
}

Going back to the main page, let's look at what I have on the XAML side. First, there are page resources:

<phone:PhoneApplicationPage.Resources>
    <local:CountToVisibilityConverter x:Key="CountToVisibility"></local:CountToVisibilityConverter>
    <local:BindingPoint x:Key="LocalBindingPoint"></local:BindingPoint>
    <local:FullToSelectConverter x:Key="EarnedSelector"></local:FullToSelectConverter>
</phone:PhoneApplicationPage.Resources>

The local namespace is declared as this:

xmlns:local="clr-namespace:VisualStudioAchievements"

CountToVisibilityConverter is a class that allows me to show the placeholder label if the Niner collection is empty. If it is not, then I am hiding it. Basically, it is an integer-to-visibility converter:

public class CountToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int count = (int)value;

        if (count > 0)
        {
            return Visibility.Collapsed;
        }
        else
        {
            return Visibility.Visible;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

BindingPoint was already shown in the previous article - it is the class that holds the Niner collection. FullToSelectConverter allows me to select five achievements out of the entire list of earned achievements associated with a registered Niner.

public class FullToSelectConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        List<Achievement> achievements = (List<Achievement>)value;
        List<Achievement> earned = (from c in achievements where c.IsEarned select c).Take(5).ToList();
        return earned;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Take(5) will, in fact, return the minimal number of achievements, so if the number of items that fit my criteria (achievements that are earned) is less than 5, than that number of items will be returned.

Here is the main work area XAML:

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

    <Image Source="Images/pagelogo.png"></Image>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="20,0,20,20">
        <TextBlock Visibility="{Binding Path=Niners.Count,Source={StaticResource LocalBindingPoint},Converter={StaticResource CountToVisibility}}" TextAlignment="Right" FontFamily="{StaticResource PhoneFontFamilySemiLight}" FontSize="{StaticResource PhoneFontSizeLarge}" TextWrapping="Wrap" Text="add users to track their visual studio achievements." Foreground="Gray"></TextBlock>
        <ListBox Foreground="Black" ItemsSource="{Binding Path=Niners,Source={StaticResource LocalBindingPoint}}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" Height="200">
                        <Image Source="{Binding Avatar}" HorizontalAlignment="Left" Width="150" Height="150" VerticalAlignment="Top" Margin="10,10,0,0"></Image>
                        <StackPanel Width="250" HorizontalAlignment="Right" Margin="10,10,10,0">
                            <TextBlock Text="{Binding Alias}" Foreground="Black"></TextBlock>
                            <TextBlock Text="{Binding Name}" Foreground="Gray"></TextBlock>
                            <TextBlock Text="{Binding Caption}" Foreground="Gray"></TextBlock>
                            <StackPanel>
                                <ListBox ItemsSource="{Binding Path=Achievements,Converter={StaticResource EarnedSelector}}">
                                    <ItemsControl.ItemsPanel>
                                        <ItemsPanelTemplate>
                                            <StackPanel Orientation="Horizontal"></StackPanel>
                                        </ItemsPanelTemplate>
                                    </ItemsControl.ItemsPanel>
                                    <ListBox.ItemTemplate>
                                        <DataTemplate>
                                            <Image Source="{Binding IconSmall}" Margin="0,0,5,0"></Image>
                                        </DataTemplate>
                                    </ListBox.ItemTemplate>
                                </ListBox>
                                <TextBlock Foreground="Black" Text="{Binding Points}"></TextBlock>
                            </StackPanel>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Grid>

The top row of the main grid is just for the logo. The next one is dedicated to a ListBox with a custom DataTemplate to show all possible user data in a small rectangle. Simple as that. This is all that's needed for the basic achievement visualization structure. 

In the next article, I will discuss about the way I can show achievement details for each specific user.