Introduction
In this article, I will explain how to create a simple application that reads Dilbert, xkcd and Phd comic feeds and show them in WP7. By simple modifications to the code, you can add feeds of various other comic series and see them on your Windows Phone 7.
Background
Kudos to Microsoft for providing an awesome platform for WP7 application development using Silverlight. It's easy, quick and fun.
Using the Code
Let's start with the XAML first.
The home screen of Comic Reader is easy to create. I have a grid containing two rows, one column. The first grid contains a stack panel that contains the Header "Comic Reader" while the other grid contains a Listbox
hosting buttons for each type of comic.
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="PageTitle" Text="Comics Reader" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
<TextBlock x:Name="ApplicationTitle"
Text="Read your favorite web comics on the move!"
Style="{StaticResource PhoneTextNormalStyle}"/>
</StackPanel>
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<controls:ComicButton x:Name="btnComicDilbert"
Click="btnComicDilbert_Click"/>
<controls:ComicButton x:Name="btnComicXkcd" Click="btnComicXkcd_Click" />
<controls:ComicButton x:Name="btnComicPhdComics"
Click="btnComicPhdComics_Click" />
</ListBox>
</Grid>
</Grid>
In the code behind, we are forcing Landscape orientation by doing the following:
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
Each comic feed has three most important tags for providing:
- The comic article image
- The comic article title
- The article description
Based on this assumption, the comicpage.xaml where the comic articles are hosted is as follows:
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="PageTitle" Text="" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"
FontStretch="SemiCondensed" />
</StackPanel>
-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<phone:WebBrowser HorizontalAlignment="Stretch" Margin="0,0,0,203"
Name="webBrowser1" VerticalAlignment="Stretch" Width="Auto" />
<TextBlock Height="76" HorizontalAlignment="Left" Margin="9,451,0,0"
Name="DescriptionTextBlock" Text="" VerticalAlignment="Top" Width="441"
TextWrapping="Wrap" ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto" />
<Grid x:Name="Spinner" Background="Transparent" Visibility="Collapsed"
HorizontalAlignment="Center" Margin="168,0,79,-140" Width="209">
<Grid.RenderTransform>
<ScaleTransform x:Name="SpinnerScale" ScaleX="0.5" ScaleY="0.5" />
</Grid.RenderTransform>
<Canvas RenderTransformOrigin="0.5,0.5" Width="120" Height="120">
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="20.1696" Canvas.Top="9.76358"
Stretch="Fill" Fill="#E6FFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="2.86816" Canvas.Top="29.9581"
Stretch="Fill" Fill="#CDFFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="5.03758e-006" Canvas.Top="57.9341"
Stretch="Fill" Fill="#B3FFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="12.1203" Canvas.Top="83.3163"
Stretch="Fill" Fill="#9AFFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="36.5459" Canvas.Top="98.138"
Stretch="Fill" Fill="#80FFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="64.6723" Canvas.Top="96.8411"
Stretch="Fill" Fill="#67FFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="87.6176" Canvas.Top="81.2783"
Stretch="Fill" Fill="#4DFFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="98.165" Canvas.Top="54.414"
Stretch="Fill" Fill="#34FFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="92.9838" Canvas.Top="26.9938"
Stretch="Fill" Fill="#1AFFFFFF"/>
<Ellipse Width="21.835" Height="21.862"
Canvas.Left="47.2783" Canvas.Top="0.5"
Stretch="Fill" Fill="#FFFFFFFF"/>
<Canvas.RenderTransform>
<RotateTransform x:Name="SpinnerRotate" Angle="0" />
</Canvas.RenderTransform>
<Canvas.Triggers>
<EventTrigger RoutedEvent="ContentControl.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="SpinnerRotate"
Storyboard.TargetProperty="(RotateTransform.Angle)"
From="0" To="360" Duration="0:0:01"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Canvas.Triggers>
</Canvas>
</Grid>
</Grid>
</Grid>
Till the time the content is not loaded into the browser, I run the spinner using storyboard animation shown above.
How is the Code Organizer?
When we see the code, we have ComicFeed
class that contains a list of ComicArticle
objects. Each ComicArticle
object contains all the information for showing a comic article on ComicPage.xaml.
Though all the feeds are similar, we do require to parse each feed separately due to minor differences in the feeds. Hence, for each comic series, we have a separate class DilbertFeed
, XkcdFeed
and PhdFeed
that inherit from ComicFeed
class.
The ComicFeed
class is written as follows:
public abstract class ComicFeed : INotifyPropertyChanged
{
public Uri ImageSource
{
get;
set;
}
public string NotificationCount
{
get;
set;
}
public string ComicTitle
{
get;
set;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
virtual protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{ PropertyChanged(this, new PropertyChangedEventArgs(name)); }
}
#endregion
public EventHandler ArticlesUpdated;
public TextBlock comicTitle;
public string rssFeedLink = string.Empty;
public int latestArticleNumber = 0;
public bool downloadInProgress = false;
public virtual string ModifyHtml(string html, string title)
{
return html;
}
public virtual void SyncLatestArticles()
{
WebClient webclient = new WebClient();
webclient.DownloadStringCompleted += new DownloadStringCompletedEventHandler
(this.webclient_DownloadStringCompleted);
webclient.DownloadStringAsync(new Uri(this.rssFeedLink));
}
public virtual ComicArticle GetArticle(int counter)
{
throw new NotImplementedException();
}
public virtual void webclient_DownloadStringCompleted
(object sender, DownloadStringCompletedEventArgs e)
{
}
public List<ComicArticle> comicArticles = new List<ComicArticle>();
public int limitOfArticles = 4;
}
By default, the limit to the number of articles per comic feed is 4. The current implementation stores the comic articles in the isolated storage hence there was a need to limit the comic articles so that our application does not consume all the space in isolated storage.
For each comic series, a folder by the corresponding name is created in isolated storage and the articles are stored in those folders. The GetArticles(int articleNumber)
is used to retrieve the articles from isolated storage as follows:
public override ComicArticle GetArticle(int counter)
{
using (var appStorage = IsolatedStorageFile.GetUserStoreForApplication())
{
if (!appStorage.DirectoryExists(this.comicTitle.Text))
{
return null;
}
List<string> articleDirectory = appStorage.GetDirectoryNames
(string.Format(@"{0}\*", this.comicTitle.Text)).ToList();
if (articleDirectory.Count <= counter)
{
return null;
}
else
{
ComicArticle article = new ComicArticle();
string filePath = System.IO.Path.Combine
(this.comicTitle.Text, articleDirectory[counter]);
using (var cachedFile = appStorage.OpenFile
(string.Format("{0}\\content.html", filePath), FileMode.Open))
using (TextReader reader = new StreamReader(cachedFile))
{
string html = reader.ReadToEnd();
article.html = html;
article.htmlFilePath = filePath;
article.publishDate = articleDirectory[counter];
}
return article;
}
}
}
The Result
Dibert Comic Series...
Phd Comic Series...
xkcd Comic Series...
Things To Do
While creating this application (Sept 2010), I wanted a quick solution to render the images that I was downloading from the feed. During that time, there was no decoder for GIF images, hence I used a web browser. Recently, ImageTools project at CodePlex has provided support for displaying all GIF images on WP7.
History
- 30th November, 2010: Initial post