Windows Phone 7: Dynamically adding an Rss feed per panorama page

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");
 }
 }