Let your apps sell themselves!

EDN Admin

Well-known member
Joined
Aug 7, 2010
Messages
12,794
Location
In the Machine
Dont 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! <h3>Introduction</h3> http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image%5B2%5D-2.png <img title="image" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image_thumb-2.png" alt="image" width="244" height="189" align="left" border="0 Ive written a number of Windows Phone apps, and each time Ive wanted to let people know about my other apps. While I could mention the other apps somewhere, I wouldnt 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 wasnt too much work to create a user control to let me drop in the list of apps anytime I need it. This code doesnt require a physical phone, but it isnt very useful if you dont 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 dont have the software installed, go to http://create.msdn.com/ 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. <h3>Project Basics</h3> 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). http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/MyAppsList7.png <img title="MyAppsList" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/MyAppsList_thumb3.png" alt="MyAppsList" width="465" height="772" border="0 <h3>Marketplace Data</h3> At first glance, getting Marketplace data programmatically isnt an option. Sadly, theres no API for this. Fortunately, theres a solution! If youre using the Zune app to browse publishers and apps, you can watch the network traffic using Fiddler ( http://fiddler2.com/fiddler2/ http://fiddler2.com/fiddler2/ ). Whats 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: <pre class="csharpcode <pre class="brush: text /v3.2/en-US/apps?q=PublisherName&clientType=WinMobile 7.1&store=zest[/code][/code] 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 its not likely youll want that list. The "store" parameter is set to "zest" but well just have to take this one on faith! Im not aware of any other options here. This could vary by country, but I dont have any data on that. This will return all apps for Mango. This returns feed information starting with a header: <pre class="csharpcode <pre class="brush: xml <?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>[/code][/code] 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: <pre class="csharpcode <pre class="brush: xml <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>[/code][/code] 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 thats simplified as well. <h3>Syndicated Data</h3> 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 isnt actually available in the Windows Phone libraries. Im 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 SDKsSilverlightv3.0LibrariesClientSystem.ServiceModel.Syndication" assembly. You might get a warning when you add it, but it will work fine. If you download the accompanying code, youll 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 <pre class="csharpcode <pre class="brush: vb 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[/code][/code] Visual C# <pre class="csharpcode <pre class="brush: csharp 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>();
}[/code][/code] With this method in place, you can create most of a new MarketplaceApp object with the following code: Visual Basic <pre class="csharpcode <pre class="brush: vb 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" _
}[/code][/code] Visual C# <pre class="csharpcode <pre class="brush: csharp 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"
};[/code][/code] 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 its of XmlNodeType.Element and either displayPrice or priceCurrencyCode. Visual Basic <pre class="csharpcode <pre class="brush: vb Dim offers = (From ee In item.ElementExtensions Where ee.OuterName = "offers").FirstOrDefault().GetReader()

TODO: Restrict to current countrys 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[/code][/code] Visual C# <pre class="csharpcode <pre class="brush: csharp var offers = item.ElementExtensions.
Where(ee => ee.OuterName == "offers").FirstOrDefault().GetReader();

// TODO: Restrict to current countrys 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();
}
}
}[/code][/code] 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 users location, but thats another topic! <h3>Creating the Control</h3> 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: <ol>Download the XML Parse the XML into model objects Bind the collection of items to a list </ol> Of course, before you can show anything, youll 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 apps icon, and the StackPanel displays the title, price, and rating information. <pre class="csharpcode <pre class="brush: xml <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>[/code][/code] With the template complete, you just need a ListBox with the ItemTemplate set to the above template. <pre class="csharpcode <pre class="brush: xml <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>[/code][/code] 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 apps unique ID, and call Show(). Set the SelectedIndex property back to -1 to prevent a problem where a user cant press the same item twice in the row. The caching is important so the app doesnt cause data access on every single visit to the control. Its currently set to reload once per day, but this could be changed (or better yet, made configurable). Even if its more than a day, if theres an error downloading the data it will always fallback to cache. Visual Basic <pre class="csharpcode <pre class="brush: vb 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[/code][/code] Visual C# <pre class="csharpcode <pre class="brush: csharp 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;
}
}[/code][/code]<h3>Next Steps</h3> I havent tested this app against publishers with large numbers of apps. Im 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 dont 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. <h3>Conclusion</h3> 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. <h3>About the Author</h3> http://www.ariankulp.com/ 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. <img src="http://m.webtrends.com/dcs1wotjh10000w0irc493s0e_6x1g/njs.gif?dcssip=channel9.msdn.com&dcsuri=http://channel9.msdn.com/Feeds/RSS&WT.dl=0&WT.entryid=Entry:RSSView:f3231d90e6ca4435ade49ff2014347ff

View the full article
 
Back
Top