Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Let your apps sell themselves!

Don't miss any opportunity to market your Windows Phone apps!  Each one of your apps can serve as an ad for your other apps.    Learn how to add a listing of everything you have published in Marketplace to each of your apps.  Even better, it will always be up to date!

Introduction

imageI've written a number of Windows Phone apps, and each time I've wanted to let people know about my other apps.  While I could mention the other apps somewhere, I wouldn't want to update all of my apps each time I release a new one.  It finally occurred to me that I could use the listing of apps directly from Marketplace instead.  This provides me with an easy-to-parse XML feed (Atom) of my apps with all of the info that I need.  Armed with that, it wasn't too much work to create a user control to let me drop in the list of apps anytime I need it.

This code doesn't require a physical phone, but it isn't very useful if you don't have a Marketplace account!  Ideally, you should have several published apps under your account for this to make much sense.  Once you have it in place though, all of your apps will always show your complete list without any special updates.

If you don't have the software installed, go to create.msdn.com, then click Download the free tools to download the Windows Phone Developer Tools (or use the direct download link provided above this Introduction section). This code is written for the Windows Phone Developer Tools 7.1 (Mango).  It’s is a mostly online install, and it’s pretty big so expect it to take some time. Even if you don’t have any development tools, this will give you Visual Studio Express, Blend, and XNA Game Studio. If you have the full-version tools already, it will add new templates.

Project Basics

The intention is to create a user control to display a list of apps from a given publisher (preferably yourself!).  This user control will be implemented as a ListBox with individual apps showing up visually similar to the way they do in Marketplace.  Touching an app in the list should bring the user directly to the appropriate Marketplace page.  It should be as easy as adding a project reference, adding the control to a XAML page, and setting the publisher (this could potentially be set by reading the WMAppManifest.xml file).

MyAppsList

Marketplace Data

At first glance, getting Marketplace data programmatically isn't an option.  Sadly, there's no API for this.  Fortunately, there's a solution!  If you're using the Zune app to browse publishers and apps, you can watch the network traffic using Fiddler (http://fiddler2.com/fiddler2/).  What's nice is that everything you do in Zune results in a simple HTTP request for the data, which is returned as an XML stream.  A simple WebRequest object can do a DownloadStringAsync call to get the data, then the SyndicationFeed class can load and parse it.  There are extension elements for rating, release date, and price.  Even nicer, is that the image thumbnail can be resized server-side based on the URL query string.  This makes it super easy to get exactly what we need.

The server of interest is catalog.zune.net, and the URL format is:

 

/v3.2/en-US/apps?q=PublisherName&clientType=WinMobile 7.1&store=zest

The "q" parameter just needs to be set to the publisher name of interest.  Remember to URL encode yours if you have spaces in it.

Note the "clientType" parameter.  If you change it to WinMobile 7.0 you will get NoDo (pre-Mango) apps only.  You may or may not have anything show up for that, but it's not likely you'll want that list.

The "store" parameter is set to "zest" but we'll just have to take this one on faith!  I'm not aware of any other options here.  This could vary by country, but I don't have any data on that.

This will return all apps for Mango.  This returns feed information starting with a header:

 

<?xml version="1.0" encoding="utf-8"?>
<a:feed xmlns:a="http://www.w3.org/2005/Atom" xmlns:os=http://a9.com/-/spec/opensearch/1.1/ 
 xmlns="http://schemas.zune.net/catalog/apps/2008/02">
  <a:link rel="self" type="application/atom+xml" 
    href="/v3.2/en-US/apps?q=Arian+T.+Kulp&amp;store=zest&amp;clientType=WinMobile+7.1" />
  <os:startIndex>1</os:startIndex>
  <os:totalResults>5</os:totalResults>
  <os:itemsPerPage>5</os:itemsPerPage>
  <a:updated>2012-01-30T04:21:41.9702941Z</a:updated>
  <a:title type="text">List Of Items</a:title>
  <a:id>tag:catalog.zune.net,2012-01-30:/apps</a:id>

 

You can pretty much ignore this section.  The important stuff comes next!  Each Atom entry is one app, along with every bit of metadata that you need to make an interesting view:

 

  
<a:entry>
    <a:updated>2012-01-30T04:21:41.9858942Z</a:updated>
    <a:title type="text">Metro Lockscreen Creator</a:title>
    <a:id>urn:uuid:0f5eaaa8-e75e-4a04-a5f9-24db1c176a6b</a:id>
    <sortTitle>Metro Lockscreen Creator</sortTitle>
    <releaseDate>2011-07-26T14:30:39.987Z</releaseDate>
    <version>1.3.0.0</version>
    <averageUserRating>7.254902</averageUserRating>
    <userRatingCount>51</userRatingCount>
    <averageLastInstanceUserRating>5.2</averageLastInstanceUserRating>
    <lastInstanceUserRatingCount>5</lastInstanceUserRatingCount>
    <image>
      <id>urn:uuid:cb46389d-6d50-4be0-b4ff-75c968f301ea</id>
    </image>
    <categories>
      <category>
        <id>windowsphone.toolsandproductivity</id>
        <title>tools + productivity</title>
        <isRoot>True</isRoot>
      </category>
    </categories>
    <tags>
      <tag>apptag.independent</tag>
    </tags>
    <offers>
      <offer>
        <offerId>urn:uuid:b8d5cc60-0839-4bcb-b26e-e85ba3e92c8d</offerId>
        <mediaInstanceId>urn:uuid:2ff871ea-cf8a-488e-b613-286052b90a13</mediaInstanceId>
        <clientTypes>
          <clientType>WinMobile 7.1</clientType>
        </clientTypes>
        <paymentTypes>
          <paymentType>Credit Card</paymentType>
          <paymentType>Mobile Operator</paymentType>
        </paymentTypes>
        <store>Zest</store>
        <price>0</price>
        <displayPrice>$0.00</displayPrice>
        <priceCurrencyCode>USD</priceCurrencyCode>
        <licenseRight>Purchase</licenseRight>
      </offer>
    </offers>
    <publisher>
      <id>Arian T. Kulp</id>
      <name>Arian T. Kulp</name>
    </publisher>
  </a:entry>

From this block you can get title, release date, rating average and count, category, image ID (easily converted to URL), and price (per market).  The System.ServiceModel.Syndication.SyndicationFeed class can read from an XMLReader object and it handles everything for you.  Dealing with the custom Marketplace namespace can lead to some slight complication, but fortunately that's simplified as well.

Syndicated Data

For standard Atom elements (the ones with the a: namespace here), you can use properties of the SyndicationItem class.  For some reason, the SyndicationItem class isn't actually available in the Windows Phone libraries.  I'm not sure why this is the case, but it turns out that you can use the desktop version without a problem.  Just add a reference to the "C:\Program Files (x86)\Microsoft SDKs\Silverlight\v3.0\Libraries\Client\System.ServiceModel.Syndication" assembly.  You might get a warning when you add it, but it will work fine.  If you download the accompanying code, you'll get all the assemblies you need in it.

The SyndicationItem class gets you properties like Title and Id.  The other properties are all custom types in the "http://schemas.zune.net/catalog/apps/2008/02" namespace.  From these properties, you are interested in shortDescription, userRatingCount, averageUserRating, version, releaseDate, displayPrice, and priceCurrencyCode.  Instead of worrying about namespaces, since you know they are custom elements, you can use the ElementExtensions collection on the SyndicationItem and query the OuterName property.  A simple extension method makes this easy:

Visual Basic

 

Private Shared Function GetExtensionElementValue(Of T)(item As SyndicationItem, extensionElementName As String) As T
  Dim f = (From ee In item.ElementExtensions Where ee.OuterName = extensionElementName).FirstOrDefault()

  Return If(f Is Nothing, Nothing, f.GetObject(Of T)())
End Function

Visual C#

private static T GetExtensionElementValue<T>(SyndicationItem item, string extensionElementName)
{
    var f = item.ElementExtensions.Where(ee => ee.OuterName == extensionElementName).FirstOrDefault();
    return f == null ? default(T) : f.GetObject<T>();
}

With this method in place, you can create most of a new MarketplaceApp object with the following code:

Visual Basic

 

Dim app = New MarketplaceApp() With { _
    .Id = id, _
    .AppLink = "http://windowsphone.com/s?appid=" & Convert.ToString(id), _
    .Title = item.Title.Text, _
    .ShortDescription = GetExtensionElementValue(Of String)(item, "shortDescription"), _
    .UserRatingCount = GetExtensionElementValue(Of Integer)(item, "userRatingCount"), _
    .Version = GetExtensionElementValue(Of String)(item, "version"), _
    .AverageUserRating = GetExtensionElementValue(Of Double)(item, "averageUserRating") / 2.0, _
    .ReleaseDate = GetExtensionElementValue(Of String)(item, "releaseDate"), _
    .PrimaryImageUrl = Root & "/v3.2/en-US/apps/" & Convert.ToString(id) & "/primaryImage?width=95&height=95&resize=true", _
    .DisplayPrice = "Unknown" _
}

Visual C#

var app = new MarketplaceApp
{
    Id = id,
    AppLink = "http://windowsphone.com/s?appid=" + id,
    Title = item.Title.Text,
    ShortDescription = GetExtensionElementValue<string>(item, "shortDescription"),
    UserRatingCount = GetExtensionElementValue<int>(item, "userRatingCount"),
    Version = GetExtensionElementValue<string>(item, "version"),
    AverageUserRating = GetExtensionElementValue<double>(item, "averageUserRating") / 2.0,
    ReleaseDate = GetExtensionElementValue<string>(item, "releaseDate"),
    PrimaryImageUrl = Root + "/v3.2/en-US/apps/" + id + "/primaryImage?width=95&height=95&resize=true"
};

Where it gets a little bit tricky is the "offers" block.  This is an XML block within the overall Item block.  This requires that you shift over to an XmlReader method of parsing.  This works by reading the XML sequentially, stopping at elements of interest.  You start by finding the "offers" element, but instead of using the GetObject method, you use GetReader.  From there, you use a while loop to visit every node, and grab the value if it's of XmlNodeType.Element and either displayPrice or priceCurrencyCode.

Visual Basic

 

Dim offers = (From ee In item.ElementExtensions Where ee.OuterName = "offers").FirstOrDefault().GetReader()

' TODO: Restrict to current country's offer
If offers.ReadToFollowing("offer") Then
    While offers.Read()
        If offers.NodeType = XmlNodeType.Element Then
            If offers.Name = "displayPrice" Then
                app.DisplayPrice = offers.ReadElementContentAsString()
            ElseIf offers.Name = "priceCurrencyCode" Then
                app.PriceCurrencyCode = offers.ReadElementContentAsString()
            End If
        End If
    End While
End If

Visual C#

var offers = item.ElementExtensions.
    Where(ee => ee.OuterName == "offers").FirstOrDefault().GetReader();

// TODO: Restrict to current country's offer
if (offers.ReadToFollowing("offer"))
{
    while(offers.Read())
    {
        if (offers.NodeType == XmlNodeType.Element)
        {
            if (offers.Name == "displayPrice")
                app.DisplayPrice = offers.ReadElementContentAsString();
            else if (offers.Name == "priceCurrencyCode")
                app.PriceCurrencyCode = offers.ReadElementContentAsString();
        }
    }
}

Notice the "TODO" block.  If you are offering your app for sale at different prices in different markets, this will only grab the first offer.  Ideally, this should grab the offer for the user's location, but that's another topic!

Creating the Control

Creating a user control makes it easy to add your list of apps anywhere.  I like to add it to a PivotItem in my About page.  There are really three things to do:

 

  1. Download the XML
  2. Parse the XML into model objects
  3. Bind the collection of items to a list

Of course, before you can show anything, you'll want to create a template to serve as the ItemTemplate in the control.  This will use databinding to point back at the properties of the marketplace entries to display the name, price, image, etc.  This can be created inline within the ListBox markup, or in the UserControl.Resources block (as it is in this case).  The root element is a Grid.  The Image shows the app's icon, and the StackPanel displays the title, price, and rating information.

<DataTemplate x:Key="AppTemplate">
    <Grid x:Name="LayoutRoot" Margin="0,0,0,4" Width="450" Background="Transparent">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="95"/>
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>

        <Image Source="{Binding PrimaryImageUrl}" Margin="0,0,20,10" />
        <StackPanel Grid.Column="1">
            <TextBlock Text="{Binding Title}" />
            <Controls1:RatingControl Max="5" Total="{Binding UserRatingCount}"
Score="{Binding AverageUserRating}" />
            <TextBlock Text="{Binding DisplayPrice}" />
        </StackPanel>
    </Grid>
</DataTemplate>

With the template complete, you just need a ListBox with the ItemTemplate set to the above template. 

<Grid>
    <ListBox x:Name="listBox" ItemsSource="{Binding Apps}"
ItemTemplate="{StaticResource AppTemplate}"SelectionChanged="ListBox_SelectionChanged" />
    <StackPanel x:Name="LoadingView" HorizontalAlignment="Center"
VerticalAlignment="Center"Visibility="Collapsed">
        <TextBlock TextWrapping="Wrap" Text="Loading list of apps..."
d:LayoutOverrides="Height"FontWeight="Black"/>
        <ProgressBar Margin="0,0,0,6" Height="5" IsIndeterminate="True"/>
    </StackPanel>
    <StackPanel x:Name="ErrorView" Orientation="Vertical" VerticalAlignment="Center"
HorizontalAlignment="Center" Visibility="Collapsed">
        <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap"
Text="Sorry, there was an error reading the list of apps." FontStyle="Italic"/>
    </StackPanel>
</Grid>

The other elements are used to display a loading message and an error message.  Switching between the list, loading, or error message is accomplished using VisualStateManager with the "Normal," "Loading," or "Error" states accordingly.

The only other methods needed in the code-behind are to handle selection and caching.  When an item is selected, you create a MarketplaceDetailTask object, set the ContentIdentifier to the app's unique ID, and call Show().  Set the SelectedIndex property back to -1 to prevent a problem where a user can't press the same item twice in the row.

The caching is important so the app doesn't cause data access on every single visit to the control.  It's currently set to reload once per day, but this could be changed (or better yet, made configurable).  Even if it's more than a day, if there's an error downloading the data it will always fallback to cache.

Visual Basic

 

Dim items As IEnumerable(Of MarketplaceApp) = Nothing

If IsolatedStorageSettings.ApplicationSettings.Contains("PublisherAppsControls.Items") Then
  items = DirectCast(IsolatedStorageSettings.ApplicationSettings( _
    "PublisherAppsControls.Items"), IEnumerable(Of MarketplaceApp)) 
  ' Use the cache if within a day...
  Dim dt = CType(IsolatedStorageSettings.ApplicationSettings( _ 
    "PublisherAppsControls.Items.DateTime"), DateTime)

  If DateTime.Now.Subtract(dt).TotalDays < 1 Then
    ctrl.Apps.Clear()
    For Each i In items
      ctrl.Apps.Add(i)
    Next
    Return
  End If
End If

Visual C#

IEnumerable<MarketplaceApp> items = null;

if (IsolatedStorageSettings.ApplicationSettings.Contains("PublisherAppsControls.Items"))
{
    items = (IEnumerable<MarketplaceApp>)
             IsolatedStorageSettings.ApplicationSettings["PublisherAppsControls.Items"];
    // Use the cache if within a day...
    var dt = (DateTime)IsolatedStorageSettings.
               ApplicationSettings["PublisherAppsControls.Items.DateTime"];
    if (DateTime.Now.Subtract(dt).TotalDays < 1)
    {
        ctrl.Apps.Clear();
        foreach (var i in items) ctrl.Apps.Add(i);
        return;
    }
}

Next Steps

I haven't tested this app against publishers with large numbers of apps.  I'm not sure if they would all come back in the response or if there would be more requests to make.  It would also be good to filter out the currently running app.  This could be obtained from the WMAppManifest.xml file.  As mentioned in the article, it would also be good to restrict the offer to the current country, but I really don't know how to do that without using the location API, which seems too heavy-handed.

It would be nice to create a general-purpose library for Zune Marketplace data.  There are some good starts out there from the likes of Brandon Watson and Jesse Liberty, a CodePlex library, and even another Coding4Fun project, but much of that is based on general catalog data, or specialized for music entries.  Even better would be if Microsoft can put out their own API!  That would ensure a better experience if/when formats change over time.

Conclusion

XML has made the world of data so much easier to consume!  As soon as I saw structured data as the foundation for Zune I knew this would be a pretty easy project.  Being able to advertise all of your apps in each of your apps provides a great way to promote with a minimum of effort.

About the Author

Arian Kulp is a software developer living in Western Oregon. He creates samples, screencasts, demos, labs, and articles, speaks at programming events, and enjoys hiking and other outdoor adventures with his family.

Tags:

Follow the Discussion

  • Cool idea! Wink

  • doug mairdoug mair

    Thanks so much, put this control on my app in 10 minutes.

    Found a bug with the pricing value that get displayed. If you have a trail mode, it will display the price of the trail, was is $0.00 which gets translated to Free.

    My fix was to use the first non-zero price. Seemed to work.

  • This is very well done! Thanks for taking the time with all of the details, I love it...

  • This is good, if customizations like filtering certain apps aren't needed, a simple alternative is to use the MarketplaceSearchTask  and use publisher name as the search term.

  • i think this is a Smart and Cool idea from u...Good Job Dude

  • Thanks for this Nice idea and Detailed Explanation Smiley

  • sloboslobo

    Great post. I was not aware of Syndication class. I will need to refactor my code to use it in App Rank app. It provides real-time app ranking of any app by querying marketplace as you described.

  • Nice work!

    On playing around with the API I found that you can use the 'publisher' parameter rather than the 'q' parameter to get all your own apps. This will stop other publishers' apps being shown in the results if you have common words in your publisher name.

    Obviously you can also change the country tag to get ratings and prices, etc, for the market the user is in.

    For example, all my apps with ratings for New Zealand: http://catalog.zune.net/v3.2/en-NZ/apps?publisher=Sheerwater&clientType=WinMobile%207.1&store=zest

  • I had an issue where the list was not displaying my submitted apps.  Originally I had placed it in a panoramaitem, inside of a stackpanel.  However, once I replaced the stackpanel with a grid the list updated correctly.  I highly recommend using this as it's simple to use.  

     

    Is there a way to allow the publishername to be databound?  I find if I override the DataContext property things don't work so I have the name hardcoded.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.