Статьи

Достижения Visual Studio для Windows Phone — как это выглядит

Поскольку ядро уже есть , пришло время показать заслуженные достижения на шоу. Это интересная часть, так как в мобильном приложении пользовательский опыт является ключом к его успеху. Хотя я все еще улучшаю общую структуру, чтобы подготовить ее для Marketplace, вы можете увидеть, как я могу получить опыт, похожий на тег игрока, для достижений Visual Studio.

Главная страница

Именно здесь происходит большинство действий — страница, на которой перечислены все связанные учетные записи Channel9 и показаны некоторые основные метаданные, такие как последние 5 достижений, аватар пользователя и подпись. Чтобы дать вам представление о том, как это выглядит, вот краткий обзор:

Но обо всем по порядку — на главной странице у меня включена панель приложений. Через него я могу добавлять или удалять пользователей Channel9. XAML выглядит так:

<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>

Довольно просто Есть кнопка, которая позволяет добавить пользователя, а другого удалить выбранного. Давайте посмотрим, как выглядит btnAddUser_Click :

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

Ничего, кроме навигационной ссылки на InputPage — страницу, которая действует как диалог ввода пользователя. Давайте посмотрим на это в первую очередь.

Посмотрите внимательно на параметры, которые я передаю выше, и на содержание страницы. Как видите, текстовые поля не являются статичными — они не имеют содержимого, запрограммированного на странице напрямую, а скорее связаны с переданными параметрами. Это добавляет гибкости диалогу и позволяет мне повторно использовать его в различных частях приложения, если это необходимо.

Это возможно благодаря наличию пользовательских свойств и привязок. Вот что у меня есть для этого в коде:

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.