Home

tompaana edited this page Nov 12, 2014 · 6 revisions
Clone this wiki locally

Background

RSS Reader Silverlight is a Windows Phone port of QML RSS Reader. See the porting notes here.

Main screen  Item screen

RSS Reader implementation

General design approach

The main screen is implemented as a panorama view, with a panorama item for each feed category. On the main screen the user can flick through the categories, open a feed, and customise each panorama item to control which feeds are visible and which are not (the configuration screen is not in the wireframes).

The feed screen is implemented as a pivot view, where each pivot item represents one RSS feed in the selected category. The pivot view opens in the feed the user tapped on the main screen. On the feed screen the user sees RSS item titles and timestamps. The user can filter the RSS items by tapping the search button in the application bar. Tapping the search button takes the user to the filter items screen (not in the wireframes), where the user can enter text to filter only certain RSS items.

Tapping an RSS item on the feed screen takes the user to the item screen, which displays the article heading, text, and the first image in the article (if any). In the application bar, there is also a button to launch the link to the original article in the platform browser.

Design

Implementation approach

The data model

The data model consists of a class named RSSCache, which has an ObservableCollection that contains instances of the RSSPage class, which in turn contains a list of RSSFeeds. An RSSFeed contains a list of RSSItems. The RSSCache class can be used in XAML as it is listed in the Application.Resources block of App.xaml:

<Application.Resources>
    <local:RSSCache x:Key="RSSPagesDataSource" />
</Application.Resources>

After this, an instance of RSSCache can be referenced as RSSPagesDataSource. The cache is used in MainPano.xaml's Grid named LayoutRoot like this:

<controls:PanoramaItem DataContext="{Binding Source={StaticResource RSSPagesDataSource}, Path=FirstPage}">
    <controls:PanoramaItem.Header>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Title}"/>
            ...
        </StackPanel>
    </controls:PanoramaItem.Header>
    ...
</controls:PanoramaItem>

Thus the FirstPage property of RSSPagesDataSource is the DataContext of everything in that particular PanoramaItem, and is referenced in the panorama item header and content.

Tombstoning

The application automatically persists its RSSCache to disk when it is killed. The application is killed when, for instance, the user exits the application, the user taps the 'View in browser' button in item view, or there's an incoming call.

RSSCache has methods Load() and Save() to handle the persisting. The application writes its state to the IsolatedStorage using DataContractSerializer, which produces XML files from the data model. The classes that are persisted to the disk must have [DataContract] right before the class name, and the properties that are to be stored must have [DataMember] right before them. See RSSItem.cs for examples. IsReference = True is defined in some classes to preserve object references when persisting the objects.

public static RSSCache Load()
{
    RSSCache cache = null;
    using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication())
    {
        using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("rssCache.dat", System.IO.FileMode.OpenOrCreate, file))
        {
            if (stream.Length > 0)
            {
                App.Log("Reading cache from file");
                DataContractSerializer serializer = new DataContractSerializer(typeof(RSSCache));
                cache = serializer.ReadObject(stream) as RSSCache;
            }
        }
    }

    // File was not found, create a new cache
    if (cache == null)
    {
        App.Log("Creating a new cache");
        cache = new RSSCache();
    }

    return cache;
}

public void Save()
{
    App.Log("Persisting cache to file");
    using (IsolatedStorageFile file = IsolatedStorageFile.GetUserStoreForApplication())
    {
        using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("rssCache.dat", System.IO.FileMode.Create, file))
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(RSSCache));
            serializer.WriteObject(stream, this);
        }
    }
}

ObservableCollection and CollectionViewSource

The application utilises ObservableCollections and CollectionViewSources in multiple places to ease the updating of the UI. Whenever an ObservableCollection changes, the UI element it is attached to changes automatically, so this is ideal for, for instance, loading the feed images on the fly. The default RSS icons are shown first, and as the actual feed images are downloaded, they are shown as well. CollectionViewSource can be used to filter a Collection easily. This is used, for instance, in the filter items screen. CollectionViewSource's View property has a Filter property into which the filtering rules can be defined. In the SearchPage.xaml.cs case, the filter just checks if an RSSItem's title contains the text the user has entered into the search field.

Handling the RSS feeds

The RSSFeeds are read asynchronously in RSSService.cs's GetRSSItems method. The method first looks into the application's cache to see if the feed was recently fetched, and if it wasn't, a WebClient is created. An event handler is attached to the WebClient's OpenReadCompleted event, which triggers after the feed is fetched from the internet. This event handler builds the RSSItems from the XML using SyndicationFeed and SyndicationItem classes that are part of .NET. The items are looped through and an RSSItem is created from each SyndicationItem. After successful fetching of the feed, the method calls the onGetRSSItemsCompleted callback method that was passed into GetRSSItems as a parameter. After attaching the event handler, the web request is launched by calling the OpenReadAsync method of the WebClient.

List<RSSItem> rssItems = new List<RSSItem>();
XmlReader response = XmlReader.Create(e.Result);
SyndicationFeed rssFeed = SyndicationFeed.Load(response);
foreach (SyndicationItem syndicationItem in rssFeed.Items)
{
    RSSItem rssItem = new RSSItem(
        syndicationItem.Title.Text,
        syndicationItem.Summary.Text,
        syndicationItem.Links[0].Uri.AbsoluteUri,
        syndicationItem.PublishDate,
        feed);
    rssItems.Add(rssItem);
}

Reading the OPML file

The RSSFeeds are parsed from the included sample-opml.xml file in RSSService.cs. The method ParseOPML is called the first time the application is started, and the RSSCache is populated with the generated RSSPages. One RSSPage is generated for each 'topic' in the OPML, for instance:

<outline title="News" text="News">
    <outline text="Euronews" title="Euronews" type="rss" xmlUrl="http://feeds.feedburner.com/euronews/en/news/"/>
</outline>

would be the News RSSPage with one RSSFeed in it. The ParseOPML method takes a Stream as a parameter, and in the application the Stream is generated by reading the file locally:

StreamResourceInfo xml = App.GetResourceStream(new Uri("/RSSReader;component/sample-opml.xml", !UriKind.Relative));

One could also read the file from the internet and use the same method to parse the XML. The XML is parsed using LINQ (http://msdn.microsoft.com/en-us/library/bb308959.aspx), a way to query collections that is integrated into .NET.

private static List<RSSPage> ParseOPML(Stream stream)
{
    RSSCache cache = GetDataModel();
    List<RSSPage> rssCategories = null;

    if (cache.Cache.Count == 0)
    {
        XDocument xDocument = XDocument.Load(stream);

        // XML parsed using Linq
        rssCategories = (from outline in xDocument.Descendants("outline")
                         where outline.Attribute("xmlUrl") == null
                         select new RSSPage()
                         {
                             Title = outline.Attribute("title").Value,
                             Feeds = (from o in outline.Descendants("outline")
                                     select new RSSFeed
                                     {
                                         URL = o.Attribute("xmlUrl").Value,
                                         ImageURL = "/Resources/rss-icon.jpg",
                                         Title = o.Attribute("title").Value,
                                         IsVisible = true
                                     }).ToList<RSSFeed>()
                         }).ToList<RSSPage>();
    }

    return rssCategories;
}

Upgrading RSS Reader to Windows Phone 8

While testing for compatibility with WP8 it was found out that the implementation did not work after upgrading the project to Windows Phone 8.0. It seems that there is some kind of a bug in the Panorama control in WP8, causing problems if PanoramaItems are created dynamically using ItemsSource property. For example the Panorama's SelectedIndex does not work while using ItemsSource and also, when navigating back to the panorama from feed pivot, it always resets back to the first PanoramaItem, not the one which was visible when navigating deeper. This issue has been reported to https://connect.microsoft.com/VisualStudio/feedback/details/772715/wp8-panorama-using-itemssource-does-not-keep-track-of-selectedindex and http://social.msdn.microsoft.com/Forums/en-US/silverlightbugs/thread/a654447c-94b9-47e0-a360-62554258b7bb.

For this reason, there are now four explicitly defined PanoramaItems in the main panorama (News, Leisure, Sports, and Tech), and there is a DataMember in RSSCache for retrieving title and contents for each of the four items.