Статьи

Достижения Visual Studio для Windows Phone — сравнение основных пользователей

Предыдущие части:

Сравнение достижений — интересное занятие, потому что оно стоит за идеей конкурентоспособности пользователей. Люди, как правило, делают больше, когда видят, что их сверстники решают похожие или более сложные проблемы, которые они еще не пробовали. Если вы раньше использовали Xbox Live, вы знаете, что на консоли и на веб-сайте XBL есть специальный раздел, позволяющий сравнивать ваши достижения с достижениями ваших друзей.

Я думал, что та же функциональность должна быть доступна в моем приложении Visual Studio Achievements для Windows Phone . Первой идеей было создать мэш-ап, который позволил бы мне сравнивать неограниченное количество Niners. Горизонтальная StackPanel с несколькими пользователями казалась интригующей идеей, но сначала мне пришлось вернуться к самой простой реализации — сравнить только два Niners.

Мне нужна страница сравнения, которая будет иметь структуру, похожую на эту:

Разделенная страница должна соответствовать всем общим потребностям сравнения. В верхней строке отобразится аватар пользователя, его имя и номер, представляющий заработанные очки. Под каждым пользователем метаданных будет отображаться ячейка заработанных достижений. Если у одного из пользователей есть достижение, а у другого нет, будет отображена блокировка с надписью «Не заработано».

Получение Niners, которые следует сравнить

Перво-наперво — пользователь приложения должен указать два Niners, которые будут сравниваться. Это можно легко сделать, внедрив приглашение, подобное тому, которое я использовал для добавления новых Niners в общий список наблюдения, и это именно то, что я и сделал. С небольшими изменениями я получил следующую структуру XAML для 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>

Это приводит к странице, которая выглядит следующим образом:

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

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));
                    });
            });
    }
}

Если оба элемента управления TextBox не пусты, я предполагаю, что оба имени, которые будут сравниваться, действительны. Я создаю новую пару сравнения — CompareNinerPair:

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

С пользовательским классом намного легче управлять будущими реализациями, поэтому KeyValuePair был вне поля зрения. Вы, наверное, уже заметили, что NinerReader.GetNiner теперь имеет три параметра вместо обычных двух, которые я использовал ранее. Более того, третий параметр — это пользовательский делегат. Почему это так?

Я повторно реализовал некоторые части этого класса, чтобы сделать его многоразовым в разных частях приложения. Раньше он просто использовался для добавления новых экземпляров Niner в основную коллекцию. Теперь он может делать все, что я хочу, с экземпляром Niner после его получения, и это настраиваемое действие передается через делегат.

Внутренние модификации NinerReader выглядят так. Сначала я добавил объявления делегатов в заголовок класса:

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

Просматривая предыдущую версию класса , вы можете увидеть , что последнее выполняются действие CompleteAchievementsRequest — это метод , который анализирует список достижений и associtates их с текущей Девяткой , например ( в лице CurrentNiner собственности). Я заменил код, который добавил экземпляр Niner в BindingPoint.Niners, на следующую строку:

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

AddNiner был переименован в GetNiner, а helperDelegateInstance запускается при вызове:

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

Давайте вернемся к коду, обрабатывающему щелчок ОК. Когда первый Niner получен, я использую тот же экземпляр NinerReader, чтобы получить второй. Оба будут частью CompareNinerPair . После завершения запросов я использую Util.ListComparedAchievements для заполнения выделенного экземпляра ObservableCollection <CompareAchievementPair> в BindingPoint :

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

CompareAchievementPair — это простая пара экземпляров Achievement , связанная с сравниваемыми пользователями:

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

Так как именно сортируются и сравниваются достижения? Существует два цикла foreach, которые перебирают достижения, представленные в обоих экземплярах Niner, чтобы увидеть, какие из них заблокированы, а какие доступны с обеих сторон:

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 не изменяется и используется в качестве заполнителя:

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

Это в значительной степени завершает часть кода.

Отображение сравниваемых достижений

Я создал отдельную страницу, которая имеет разделенный формат, как я упоминал выше — 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>

Я связываю его с тем же статическим классом BindingPoint, который содержит достижение и сравниваемые пары пользователей. Когда приложение запускается, сравниваемые данные могут выглядеть примерно так:

Это выглядит не очень красиво, но это работает. Настало время немного изменить XAML, чтобы сделать данные более привлекательными. Вот что я придумал:

Не идеально, но лучше, чем в начальной версии. Вы можете вытащить последнюю версию здесь .