У Zune есть довольно крутой вспомогательный сервис под названием Zune Card — он отображает наиболее проигранные треки пользователя, а также общее количество воспроизведений. Если вы когда-либо использовали Xbox Live, он имеет ту же концепцию, что и Xbox Gamercard, с той лишь разницей, что он применяется к музыкальному контенту, а не к играм. Поскольку людям нравится демонстрировать свои достижения Xbox, пользователи Zune могут захотеть продемонстрировать свои музыкальные предпочтения. Вот почему я решил создать элемент управления ZuneCard, который можно легко встроить в приложение Windows Phone, чтобы представлять точную копию карты Zune в настольном клиенте Zune.
Я все еще работаю в контексте моего комплекта управления Windows Phone , поэтому я включил в него элемент управления ZuneCard. Прежде всего, давайте посмотрим на макет XAML, который я создал в файле generic.xaml .
<!--ZuneCard--> <Style TargetType="local:ZuneCard"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:ZuneCard"> <Border Width="420" Height="260" BorderBrush="Black" BorderThickness="2"> <Grid Background="White"> <Grid.RowDefinitions> <RowDefinition Height="84" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid Grid.Row="0"> <Grid.Resources> <cvs:BadgeCountToVisibility x:Key="BadgeCountToVisibility"></cvs:BadgeCountToVisibility> </Grid.Resources> <Image HorizontalAlignment="Left" Margin="10" Source="{TemplateBinding AvatarImageSource}" Height="64" Width="64"></Image> <StackPanel Margin="84,10,10,10"> <TextBlock Foreground="Black" FontWeight="Bold" Text="{TemplateBinding UserID}"></TextBlock> <StackPanel Orientation="Horizontal"> <TextBlock Foreground="Black" Text="{TemplateBinding PlayCount}"></TextBlock> <TextBlock Foreground="Black" Text=" plays"></TextBlock> </StackPanel> </StackPanel> <Grid HorizontalAlignment="Right" Height="60" Width="60" Margin="10" VerticalAlignment="Center" DataContext="{TemplateBinding BadgeCount}" Visibility="{Binding Converter={StaticResource BadgeCountToVisibility}}"> <Image Source="Graphics/badge.png" Height="60" Width="60" Stretch="UniformToFill"></Image> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White" Text="{TemplateBinding BadgeCount}"></TextBlock> </Grid> </Grid> <Grid Grid.Row="1" Margin="10,0,10,10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="10"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Grid.Background> <ImageBrush ImageSource="Graphics/default_bg.png"></ImageBrush> </Grid.Background> <Image Stretch="UniformToFill" Source="{TemplateBinding BackgroundImageSource}" Grid.ColumnSpan="2"></Image> <Image Height="86" Stretch="UniformToFill" Width="10" Grid.Column="0" Source="Graphics/side.png" VerticalAlignment="Center"></Image> <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Disabled" Margin="10,0,0,0" Grid.Column="1" Height="86" ItemsSource="{TemplateBinding RecentTracks}"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Image Margin="0,0,5,0" Source="{Binding}" Height="86" Width="86"></Image> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
Это может показаться довольно сложным, но, разобравшись, вы заметите, что на самом деле его очень легко понять и изменить.
Основной контейнер окружен рамкой — это решение было принято, потому что сама карта белого цвета (как вы видите на скриншоте выше), поэтому, чтобы выделить ее пределы, граница выполняет свою работу довольно хорошо. Выглядит хорошо без одного с темной включенной темой, но с более легкой может случиться так, что карта сливается с фоном.
Сетка контейнера разделена на две части. В верхнем ряду представлена общая пользовательская информация, такая как тег Zune, количество игр, аватар и количество заработанных значков. Эти данные напрямую получены из свойств, которые связаны с элементами управления TextBlock и Image. Вы также можете заметить, что у меня объявлен ресурс — это конвертер, который определяет видимость графического элемента. Если у пользователя нет значков — его отображать не нужно.
Это так просто, как это:
using System; using System.Windows; using System.Windows.Data; using System.Diagnostics; namespace ControlKit.Converters { public class BadgeCountToVisibility : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { string countStr = value.ToString(); if (countStr == "0") return Visibility.Collapsed; else return Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return string.Empty; } } }
Если строка значка равна нулю, это означает, что значков нет, и поэтому для изображения значка установлено значение Свернуть .
У меня есть пара изображений наложений во втором ряду. По умолчанию пользовательский фон извлекается непосредственно с сервера. Однако существуют ситуации, когда фоновое изображение, которое возвращается, имеет формат GIF. Как вы, наверное, знаете, Windows Phone не очень-то нравится работать с GIF- файлами , поэтому я получаю пустой экземпляр BitmapImage, и в качестве фона у меня остается пустой пробел . Чтобы избежать этого, есть фоновое изображение-заполнитель, которое отображается только тогда, когда не используется действительный фон. Во всех других ситуациях верхнее изображение покрывает общее изображение.
И последнее, но не менее важное: у меня есть ListBox с пользовательским шаблоном данных для отображения последних треков. Он будет содержать только четыре из них одновременно, поэтому я отключил горизонтальную и вертикальную прокрутку.
Код позади немного интереснее. Я объявил семь конечных точек API Zune, которые используются внутри элемента управления:
private const string SourceURL = "http://socialapi.zune.net/en-US/members/{0}"; private const string AvatarURL = "http://cache-tiles.zune.net/tiles/user/{0}"; private const string BackgroundURL = "http://cache-tiles.zune.net/tiles/background/{0}"; private const string BadgeURL = "http://socialapi.zune.net/en-US/members/{0}/badges"; private const string RecentTracksURL = "http://socialapi.zune.net/en-US/members/{0}/playlists/BuiltIn-RecentTracks"; private const string AlbumURL = "http://catalog.zune.net/v3.2/en-US/music/album/{0}/"; private const string PictureURL = "http://image.catalog.zune.net/v3.2/en-US/image/{0}?width=64&height=64";
Все это уже задокументировано как часть моего проекта Zune Data Viewer .
Элемент управления начинает загрузку информации, как только она будет загружена — используя обработчик событий Loaded, явно.
public ZuneCard() { DefaultStyleKey = typeof(ZuneCard); this.Loaded += new RoutedEventHandler(ZuneCard_Loaded); RecentTracks = new ObservableCollection<ImageSource>(); } void ZuneCard_Loaded(object sender, RoutedEventArgs e) { if (!string.IsNullOrWhiteSpace(UserID)) { DownloadData(DownloadType.BasicUserInfo); BitmapImage bSource = new BitmapImage(new Uri(string.Format(AvatarURL, UserID))); AvatarImageSource = bSource ?? new BitmapImage(new Uri("Graphics/64x64_tile.jpg", UriKind.Relative)); bSource = new BitmapImage(new Uri(string.Format(BackgroundURL, UserID))); BackgroundImageSource = bSource ?? new BitmapImage(new Uri("Graphics/default_bg.jpg", UriKind.Relative)); } }
UserID — это ключевое свойство, которое устанавливает текущего пользователя, для которого должны быть загружены данные. Я инициирую процесс, только если это свойство не является пустым или нулевым. Обратите внимание на подтверждение, что я также использую аватар и фоновые изображения. Помните сценарий GIF? Также бывают случаи, когда результат равен нулю, поэтому я должен убедиться, что в наличии есть изображение-заполнитель.
DownloadData — это единственный метод, который обрабатывает сбор данных. В зависимости от переданного типа загрузки, он обращается к другому URL. Вот возможные типы загрузки, показанные под одним перечислением:
public enum DownloadType { BasicUserInfo, Badges, RecentTracks, AlbumImage }
Here is what the actual DownloadData method looks like:
private void DownloadData(DownloadType downloadType, string parameter = "") { var client = new WebClient(); XDocument document; if (downloadType == DownloadType.BasicUserInfo) { client.DownloadStringAsync(new Uri(string.Format(SourceURL, UserID))); client.DownloadStringCompleted += (s, e) => { try { document = XDocument.Parse(e.Result); PlayCount = document.Root.Element("{http://schemas.zune.net/profiles/2008/01}playcount").Value.ToString(); DownloadData(DownloadType.Badges); } catch { PlayCount = "0"; } }; } else if (downloadType == DownloadType.Badges) { client.DownloadStringAsync(new Uri(string.Format(BadgeURL, UserID))); client.DownloadStringCompleted += (s, e) => { try { document = XDocument.Parse(e.Result); BadgeCount = (from c in document.Root.Elements() where c.Name == "{http://www.w3.org/2005/Atom}entry" select c).Count().ToString(); DownloadData(DownloadType.RecentTracks); } catch { BadgeCount = "0"; } }; } else if (downloadType == DownloadType.RecentTracks) { client.DownloadStringAsync(new Uri(string.Format(RecentTracksURL, UserID))); client.DownloadStringCompleted += (s, e) => { try { document = XDocument.Parse(e.Result); var selection = (from c in document.Root.Elements() where c.Name == "{http://www.w3.org/2005/Atom}entry" select foreach (XElement element in selection) { string albumID = element.Element("{http://schemas.zune.net/catalog/music/2007/10}track") .Element("{http://schemas.zune.net/catalog/music/2007/10}album") .Element("{http://schemas.zune.net/catalog/music/2007/10}id").Value.Replace("urn:uuid:",""); DownloadData(DownloadType.AlbumImage, albumID); } } catch { } }; } else if (downloadType == DownloadType.AlbumImage) { client.DownloadStringAsync(new Uri(string.Format(AlbumURL, parameter))); client.DownloadStringCompleted += (s, e) => { try { document = XDocument.Parse(e.Result); string imageID = document.Root.Element("{http://schemas.zune.net/catalog/music/2007/10}image") .Value.Replace("urn:uuid:", ""); RecentTracks.Add(new BitmapImage(new Uri(string.Format(PictureURL, imageID)))); } catch { } }; } }
Every returned feed is in XML format — as you can see, I am using LINQ-to-XML to read it. Instead of working on the serialization layer, I decided to simply use node-specific requests because I am working with small amounts of data from every feed. Notice that DownloadData also has a string parameter = «». It is used when I need to retrieve the album-specific feed based on its ID. Look at how the download is handled for RecentTracks and AlbumImage.
When I get the list of tracks, I also get the album ID
string albumID = element.Element("{http://schemas.zune.net/catalog/music/2007/10}track") .Element("{http://schemas.zune.net/catalog/music/2007/10}album") .Element("{http://schemas.zune.net/catalog/music/2007/10}id").Value.Replace("urn:uuid:","");
I now need to pass this ID to the next cascading call — for the album image. Here is where the parameter becomes useful, and I am able to do this:
DownloadData(DownloadType.AlbumImage, albumID);
When downloading the album metadata, I can simply pass the ID to the regualr URL formatter:
client.DownloadStringAsync(new Uri(string.Format(AlbumURL, parameter)));
So where is all this data stored? In dependency properties, of course:
public static readonly DependencyProperty RecentTracksProperty = DependencyProperty.Register("RecentTracks", typeof(ObservableCollection<ImageSource>), typeof(ZuneCard), new PropertyMetadata(n public ObservableCollection<ImageSource> RecentTracks { get { return GetValue(RecentTracksProperty) as ObservableCollection<ImageSource>; } set { SetValue(RecentTracksProperty, value); } } public static readonly DependencyProperty UserIDProperty = DependencyProperty.Register("UserID", typeof(string), typeof(ZuneCard), new PropertyMetadata(string.Empty)); public string UserID { get { return GetValue(UserIDProperty) as string; } set { SetValue(UserIDProperty, value); } } public static readonly DependencyProperty BadgeCountProperty = DependencyProperty.Register("BadgeCount", typeof(string), typeof(ZuneCard), new PropertyMetadata("0")); public string BadgeCount { get { return (string)GetValue(BadgeCountProperty); } set { SetValue(BadgeCountProperty, value); } } public static readonly DependencyProperty PlayCountProperty = DependencyProperty.Register("PlayCount", typeof(string), typeof(ZuneCard), new PropertyMetadata("0")); public string PlayCount { get { return (string)GetValue(PlayCountProperty); } set { SetValue(PlayCountProperty, value); } } public static readonly DependencyProperty AvatarImageSourceProperty = DependencyProperty.Register("AvatarImageSource", typeof(ImageSource), typeof(ZuneCard), new PropertyMetadata(null)); public ImageSource AvatarImageSource { get { return (ImageSource)GetValue(AvatarImageSourceProperty); } set { SetValue(AvatarImageSourceProperty, value); } } public static readonly DependencyProperty BackgroundImageSourceProperty = DependencyProperty.Register("BackgroundImageSource", typeof(ImageSource), typeof(ZuneCard), new PropertyMetadata(null)); public ImageSource BackgroundImageSource { get { return (ImageSource)GetValue(BackgroundImageSourceProperty); } set { SetValue(BackgroundImageSourceProperty, value); } }
Pretty much everything that is going on behind the scenes relies on UserID, starting with the first web call. The rest are potential metadata holders. Potential, because in some scenarios the user will not have that specific chunk of information — for example, the number of badges or the list of recent tracks (remember that there are privacy settings enforced that might be blocking access).
To experiment with the control, simply add the reference to the library, add the XML namespace reference declaration and add it to your page like this:
<ckit:ZuneCard UserID="ZeBond"></ckit:ZuneCard>
You should get this at the end:
This is my first attempt at this control. It can be improved in the future, and here is what I plan on adding:
- Ability to view badges if the user taps on the badge sign.
- Open the Marketplace once the user taps on an album image
- Show more information related to the artists the user listens to
- Add an explicit Refresh() method that will allow control manipulation from the code-behind
- The possibility to choose between the dark and light styles.
- Show more than 4 recent tracks
- Remove tracks that belong to the same album from the RecentTracks collection.
As always, you can download all this awesomeness on GitHub.