CP Vanity for Windows Phone 7
Introduction
This app started as a (very) quick and dirty port of Luc Pattyn's CPVanity app to the Windows Phone 7 platform. Once that was done it just seemed lacking a Rss reader of codeproject articles and forums.
Why? Because if we don't have one already we definitely need one!
Background
To do the port of CPVanity, three files were copied from the desktop version:
- Article.cs
- User.cs
- CPSite.cs
Prerequisites
There are some things you will need to have in order to play around with this code.
- The WP7 SDK and associated tools - for obvious reasons
- GalaSoft's MVVM Light - for MVVM support
- The Silverlight for Windows Phone Toolkit - for page transitions
In order to get CPSite
(where the Html content is downloaded and scraped) to work some small changes were needed. First of all the desktop version tells the CPSite
class to do its work on a background thread. CPSite
then creates the HttpWebRequests
on that thread and download synchronously on that thread. Since all WP7 web communication is asynchronous at the point where it is invoked, the background threading needed to be moved into that class.
So there are some #if WINDOWS_PHONE
sprinkled throughout:
#if WINDOWS_PHONE
public void GetArticlePage(Action<string> callback)
{
Debug.Assert(callback != null);
downloadPage("script/Articles/MemberArticles.aspx?amid="
+ memberID, callback);
}
#else
public string GetArticlePage() {
page=downloadPage("script/Articles/MemberArticles.aspx?amid="
+ memberID);
return page;
}
#endif
And then it just uses the WebClient to get the Html content:
#if WINDOWS_PHONE
private void downloadPage(string URL, Action<string> callback)
{
if (!URL.StartsWith("http"))
URL = baseURL + "/" + URL;
WebClient client = new WebClient();
client.DownloadStringCompleted += new
DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
client.DownloadStringAsync(new Uri(URL), callback);
}
void client_DownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs e)
{
Debug.Assert(e.UserState is Action<string>);
var callback = (Action<string>)e.UserState;
try
{
page = e.Result;
callback(page);
}
catch (Exception ex)
{
callback(ex.Message);
}
finally
{
((WebClient)sender).DownloadStringCompleted -= new
DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
}
}
#endif
And then up in the ViewModel, in response to the user id changing, it does:
private CPSite _site;
...
if (_site != null)
_site.GetArticlePage(GotUserPage);
...
private void GotUserPage(string result)
{
if (_site != null)
{
string name = _site.GetName();
string adornedName = _site.GetAdornedName();
if (adornedName.Length == 0)
adornedName = name;
UserName = adornedName;
var articles = _site.GetArticles();
ArticleCount = plural(articles.Count,
"article#s available");
if (articles.Count > 1)
AverageRating = "Average rating: " +
_site.GetAverageRating() + " / 5";
foreach (var a in articles.OrderByDescending(a => a.Updated))
Articles.Add(new ArticleViewModel(a));
}
}
All the rest is just run of the mill Silverlight databinding to populate the UI.
Rss Reader
Retrieving and displaying the Rss feeds also uses the WebClient
, but also some basic Xml serialization.
public void Load()
{
WebClient client = new WebClient();
client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
client.DownloadStringAsync(Uri);
}
void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
var serializer = new XmlSerializer(typeof(RssChannel));
try
{
using (var text = new StringReader(e.Result))
using (var reader = XmlReader.Create(text))
{
reader.ReadToDescendant("channel");
Channel = (RssChannel)serializer.Deserialize(reader);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
((WebClient)sender).DownloadStringCompleted -= new DownloadStringCompletedEventHandler(client_DownloadStringCompleted);
}
}
where an RssChannel
and RssItem
look like this
[XmlRoot("channel")]
public class RssChannel
{
public RssChannel()
{
}
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("item")]
public List<rssitem> Items { get; set; }
}
[XmlRoot("item")]
public class RssItem
{
public RssItem()
{
}
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("description")]
public string Description { get; set; }
[XmlElement("link")]
public string Link { get; set; }
[XmlElement("author")]
public string Author { get; set; }
[XmlElement("pubDate")]
public string Date { get; set; }
[XmlElement("GUID")]
public string GUID { get; set; }
}</rssitem>
View to ViewModel Binding
Though the MVVM ViewModelLocator
works quite well when there is a 1:1 mapping between view and view model, it doesn't work quite so well when you want to reuse a Page to render different ViewModels. The ViewModelLocator
is set statically in the Xaml creating the 1:1 binding.
Since the rendering of the forum and article Rss feeds is identical, but the ViewModels (and definitely the models behind those) are slightly different I ended up with 1 page and 2 ViewModels.
To get around the 1:1 mapping in ViewModelLocator
an Attribute
is used to mark up the ViewModel:
[AttributeUsage(AttributeTargets.Class)]
public sealed class PageAttribute : Attribute
{
public readonly Uri Page;
public PageAttribute(string address)
{
Page = new Uri(address, UriKind.Relative);
}
}
[Page("/ArticlesPage.xaml?vm=ArticlesStatic")]
public class ArticlesViewModel : ContainerViewModel{}
[Page("/ArticlesPage.xaml?vm=ForumsStatic")]
public class ForumsViewModel : ContainerViewModel{}
and then in the code that handls the navigation events:
private void Select(object o)
{
var vm = o as CPViewModel;
if (vm != null)
{
Debug.Assert(vm.GetType().HasAttribute<PageAttribute>());
var page = vm.GetType().GetAttribute<PageAttribute>();
Navigate(page.Page);
}
}
The page codebehind does need to do a little bit of the work because the desired view model is specificed in the Uri's query string
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (NavigationContext.QueryString.ContainsKey("vm"))
{
Dispatcher.BeginInvoke(() => DataContext =
ViewModelLocator.FindViewModel(NavigationContext.QueryString["vm"]));
}
base.OnNavigatedTo(e);
}
...
public class ViewModelLocator
{
public static object FindViewModel(string key)
{
var prop = typeof(ViewModelLocator).GetProperty(key,
BindingFlags.Public | BindingFlags.Static);
Debug.Assert(prop != null);
return prop.GetValue(null, null);
}
}
The linkage between view and ViewModel is still declarative and loosely coupled, but it also has an additional degree of freedom which seems advantageous in that a single page can render any ViewModel as long at it exposes two properties: Name
and Items
<controls:Pivot Title="{Binding Name}"
ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource DynamicContentTemplate}"/>
Points of Interest
The most interesting thing I learned is that once you figure out the platform, WP7 development is very easy. This took all of a couple of hours to get together (of course plagiarizing much of the original work wholesale).
History
- 12/14/2010 - initial upload
- 12/17/2010 - added progress bar and bigger rep image page
- 12/27/2010 - added rss reader
Post Comment
oeAn3O Very informative blog post. Want more.
Most involve try Infection the to. Sometimes needs just time for Non.