Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
My first WP7 app was a little Rss reader for one of my favourite news sites. It’s quite easy to learn how to consume Rss feeds in a WP7 app. A quick search will throw up numerous samples including those on msdn (https://dev.windowsphone.com/en-us/home). So I got as far as consuming feeds and displaying the titles, associated pictures etc... fairly quickly. My basic functional design flow has a panorama page per new category and lets the user chose articles by headline. Once they have selected an article to read I launch that link in a new webbrowser instance.
So far so good. Now for the challenge. This particular site has 14 different feeds, so I want/need to let the user configure their preferred feeds. I interpreted this as a requirement for a dynamic no. of panorama pages – one per news category.
1. Managing user selections:
First off I needed to detect and save the user settings. There are two types of storage on WP7, read-only for reference files etc... supplied with your app and the more useful read/write isolated storage for files created by your app. I supply the full list of feeds in an initial xml file with a default set of marked as currently selected.
Initial xml settings file:
<?xml version="1.0" encoding="utf-8" ?>
<RssFeeds>
<Feed Title="Category 1" href="https://thefeedurl/category1 " Selected="0"/>
<Feed Title="Category 2" href="https://thefeedurl/category2" Selected="1"/>
<Feed Title="Category 3" href="https://thefeedurl/category3 " Selected="0"/>
<Feed Title="Category 4" href="https://thefeedurl/category4" Selected="1"/>
<Feed Title="Category 5" href="https://thefeedurl/category5 " Selected="0"/>
<Feed Title="Category 6" href="https://thefeedurl/category6" Selected="1"/>
<Feed Title="Category 7" href="https://thefeedurl/category7 " Selected="0"/>
<Feed Title="Category 8" href="https://thefeedurl/category8" Selected="1"/>
</RssFeeds>
When the user wants to configure their selections I display the list of feed categories with checkboxes and on save write their selection to a new isolated storage file:
IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication();
IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream("Feeds.xml", FileMode.Create, FileAccess.Write, myIsolatedStorage);
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
XmlWriter writer = XmlWriter.Create(isoStream, settings);
writer.WriteStartDocument();
writer.WriteStartElement("RssFeeds");
foreach (Category cSetting in MainPage.feedList)
{
writer.WriteStartElement("Feed");
writer.WriteStartAttribute("Title");
writer.WriteString(cSetting.categoryName);
writer.WriteEndAttribute();
writer.WriteStartAttribute("href");
writer.WriteString(cSetting.categoryFeedURI);
writer.WriteEndAttribute();
writer.WriteStartAttribute("Selected");
writer.WriteString(Convert.ToInt16(cSetting.currentlySelected).ToString());
writer.WriteEndAttribute();
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
writer.Close();
isoStream.Close();
Then on startup (PhoneApplicationPage_Loaded in the main page) I check to see if the isolated storage file exists and if it does I read the selections from there, otherwise I load the default selections from the read-only file:
public void ReadCategorySelections()
{
XElement xmlFeeds = null;
IsolatedStorageFileStream isoFileStream = null;
try
{
IsolatedStorageFile myIsolatedStorage = IsolatedStorageFile.GetUserStoreForApplication();
if (!myIsolatedStorage.FileExists("feeds.xml"))
{
Uri uri = new Uri("Feeds.xml", UriKind.Relative);
StreamResourceInfo sri = App.GetResourceStream(uri);
xmlFeeds = XElement.Load(sri.Stream, LoadOptions.None);
}
else
{
isoFileStream = myIsolatedStorage.OpenFile("feeds.xml", FileMode.Open);
xmlFeeds = XElement.Load(isoFileStream, LoadOptions.None);
}
feedList.Clear();
foreach (XElement childElement in xmlFeeds.Elements())
{
Category rssCat = new Category();
rssCat.categoryName = childElement.Attribute("Title").Value;
rssCat.categoryFeedURI = childElement.Attribute("href").Value;
rssCat.currentlySelected = Convert.ToBoolean(Convert.ToInt16(childElement.Attribute("Selected").Value));
feedList.Add(rssCat);
if (rssCat.currentlySelected)
{
AddItem(rssCat);
}
}
if (isoFileStream != null)
{
isoFileStream.Close();
}
}
catch (Exception ex)
{
Trace(ex.Message);
MessageBox.Show("An initialization error has occurred");
NavigationService.GoBack();
}
}
2. Managing a dynamic no. of Panorama pages
The easiest way I could see to do this was to have a resource template which I use for each panorama page. The template basically has one user control which is a PanoramaItem which has a ListBox to which I can add the news category headlines, pictures if available or publish date and times.
Category template:
<controls:PanoramaItem Name="panoramaFeedList" Header="News" Height="643" Width="450">
<ListBox Name="NewsList" Width="442" Height="516" Margin="0,0,0,0">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<!--<Setter Property="Background" Value="#3F1F1F1F"/>-->
<Setter Property="Background" Value="WhiteSmoke"/>
<Setter Property="Foreground" Value="Black"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderThickness="0,0,0,1" BorderBrush="Gray">
<StackPanel Name="Headline" Orientation="Horizontal" Loaded="Headline_Loaded">
<Image Name="NewsPic" Width="100" Height="100" Source="{Binding Media}" Margin="0,0,0,5" Visibility="Visible"/>
<TextBlock Name="PubString" CacheMode="BitmapCache" Height="100" Width="100" Text="{Binding PubString}" TextWrapping="NoWrap" Margin="0,0,5,5" Visibility="Collapsed" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBlock Name="HeadLine" CacheMode="BitmapCache" Height="100" Width="300" Margin ="5,0,0,0" Text="{Binding Title}" TextWrapping="Wrap" />
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PanoramaItem>
To fill the Panorama with pages and lists of articles then I read the the list (I’ve stored the category url data in local class called RssFeed – see below snippet).
This next method “AddItem” is called for each selected category. Here I add the panorama page using the template and give it the category name as a title. The next bit is potentially messy if there is any conflict between the list of categories/feeds and the actual titres in the feed data streams. To re-use code and avoid cloning the DownloadStringCompletedEventHandler for each category I give the same handler delegate to every call to DownloadStringAsync.
So I am dependent on the category titles for matching each returned data feed stream to the correct panorama page. In the case of the site I’m using this works - the stream returned contains the category title, so when I parse the returned data I can use the title to put the article list on the correct panorama page.
The AddItem method:
private void AddItem(Category rssFeed)
{
try
{
var pItem = new NewsFeedControl();
pItem.panoramaFeedList.Header = rssFeed.categoryName;
pItem.NewsList.SelectionChanged += new SelectionChangedEventHandler(newsList_SelectionChanged);
pItem.panoramaFeedList.HeaderTemplate = App.Current.Resources["NewsFeedHeaderTemplate"] as DataTemplate;
Item.ApplyTemplate();
panoramaControlMain.Items.Add(pItem);
WebClient feedClient = new WebClient();
feedClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(feed_DownloadStringCompleted);
feedClient.DownloadStringAsync(new Uri(rssFeed.categoryFeedURI));
}
catch (Exception ex)
{
Trace(ex.Message);
DisplayError("Error adding feed item");
}
}
The DownloadStringCompletedEventHandler basically idebntifies the channel/category title for the returned data and calls FillNewsPane.
void feed_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
string listBoxName = string.Empty, channelString=string.Empty;
if (e.Error != null)
{
Trace(e.Error.Message);
DisplayError("A news feed download error has occurred");
return;
}
try
{
XElement xmlNews = XElement.Parse(e.Result);
//first id the channel name
if (xmlNews.Descendants("channel").Count() != 0)
{
channelString = xmlNews.Descendants("channel").First().Value;
FillNewsPane(channelString, xmlNews);
}
}
catch (Exception ex)
{
Trace(ex.Message);
DisplayError("Error parsing feed item");
}
}
private void FillNewsPane(string channelName, XElement xmlNews)
{
if (string.IsNullOrEmpty(channelName))
{
Trace("Failed to identify downloaded channel");
DisplayError("ERROR - Channel identification failure");
return;
}
NewsFeedControl newsFeedPane = (NewsFeedControl)panoramaControlMain.Items.FirstOrDefault(i => ((NewsFeedControl)i).panoramaFeedList.Header.ToString().ToLower().Equals(channelName.ToLower()));
if (newsFeedPane == null)
{
Trace("Failed to find Panel for news feed");
DisplayError("ERROR - Panel id failure");
return;
}
try
{
foreach (var item in xmlNews.Descendants("item"))
{
RssItem rssItem = new RssItem();
rssItem.Title = (string)item.Element("title").Value;
rssItem.Content = (string)item.Element("description").Value;
rssItem.Link = (string)item.Element("link").Value;
rssItem.PubString = ((DateTime)item.Element("pubDate")).ToShortTimeString();
foreach (var mediaItem in item.Descendants("enclosure"))
{
if (mediaItem.HasAttributes && mediaItem.Attribute("type").Value == "image/jpeg")
rssItem.Media = (string)mediaItem.Attribute("url").Value;
}
newsFeedPane.NewsList.Items.Add(rssItem);
}
}
catch (Exception ex)
{
Trace(ex.Message);
DisplayError("Error filling news panel");
}
}