Сам себе RSS ридер

Однажды, в середине 5-го курса, попросила меня одногрупница помочь ей с лабами по C#, так как его она только изучала. Узнав задание – «написать RSS ридер» — и оценив ситуацию – конец семестра – я решил ей помочь, так как RSS ридер нужен был самому.

Немного теории

RSS – это формат передачи веб-контента. Название технологии — акроним «Really Simple Syndication», то есть, «по-настоящему простая передача информации».
RSS — это диалект XML. Все файлы RSS обязаны соответствовать спецификации XML1.0, опубликованной на веб-сайте консорциума WWW (W3C).
На высшем уровне документ RSS представляет собой элемент <rss> с обязательным атрибутом version, указывающим версию RSS (кстати, я свое приложение делал опираясь на RSS 2.0). Дочерний элемент rss — один элемент channel, который включает информацию о канале (метаданные) и его содержимое.

Пример файла RSS 2.0 выглядит так:

<?xml version="1.0"?>
<rss version="2.0">
   <channel>
      <title>Liftoff News</title>
      <link>
      http://liftoff.msfc.nasa.gov/</link>
      <description>Liftoff to Space Exploration.</description>
      <item>
         <title>Star City</title>
         <link>
         http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
         <description>
            How do Americans get ready to work with Russians aboard the
            International Space Station? They take a crash course in culture, language
            and protocol at Russia's Star City.
         </description>
         <pubDate>Tue, 03 Jun 2003 09:39:21 GMT</pubDate>
      </item>
      <item>
         <title>Space Exploration</title>
         <link>
         http://liftoff.msfc.nasa.gov/</link>
         <description>
            Sky watchers in Europe, Asia, and parts of Alaska and Canada
            will experience a partial eclipse of the Sun on Saturday, May 31st.
         </description>
         <pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
      </item>
   </channel>
</rss>

Достаточно простой файл. Что же он нам предоставляет? Во-первых, канал (<channel>, он же Feed) и различную информацию о нем — заголовок, ссылку на официальный сайт, описание канала и т.д.). Во-вторых, список статей/новостей/записей (<item>) называйте как хотите, а так же свойства этих записей: заголовок, ссылка, описание, дата публикации. На самом деле свойств, как канала, так и записи может быть гораздо больше. Все они приведены в спецификации rss 2.0. Ее перевод, сделанный Алексеем Бешеновым, можно найти здесь. Последнюю версию спецификации можно найти здесь. Для работы с информацией предоставляемой rss нам понадобится 3 класса: класс для хранения канала, назовем его RssFeed ; класс для хранения списка записей – RssItems ; класс для хранения записи – RssItem .

Создание формы

Открываем Microsoft Visual Studio 2005 (линуксойды открывают MonoDevelop) и создаем новое приложение. Назовем его RssReader.
Интерфейс бедет простейший: первый и основной контрол, который нужно разместить на форме – это TableLayoutPanel , растянутый на всю форму (Dock=Fill). Это очень удобный контрол, он представляет собой таблицу, в каждой ячейке которой можно разместить какой либо элемент интерфейса (элемент может занимать несколько столбцов и/или строк таблицы одновременно). Размеры столбцов могут быть фиксированными либо указываться в процентах от размера таблицы. Это очень удобно при изменении размеров таблицы. Назначение остальных элементов, думаю, понятно: TextBox — ввод адреса канала; Button – кнопка для обновления фида; в ListView – выводится список записей; в WebBrowser – выводится содержание записи. Теперь начнем разработку самого ридера. Начнем мы ее с низов, а именно с класса RssItem, затем создадим класс RssItems, последним будет написан RssFeed.

RssItem

Этот класс предоставляет нам информацию о записи. Согласно спецификации rss 2.0 «все элементы <item> являются необязательными, однако, по крайней мере, <title> или <description> должен существовать». Добавим к проекту новый класс, назовем его RssItem. В итоге получаем следующее:

using System;
using System.Collections.Generic;
using System.Text;

namespace RssReader
{
 class RssItem
 {
 }
}

Наш класс будет хранить минимальную информацию о записи: <title> (заголовок), <link> (ссылка на полный текст) и <description> (краткий обзор сообщения). Добавим к классу 3 публичных поля, для хранения этой информации:

class RssItem
{
 public String title;  // заголовок записи
 public String link;   // ссылка на полный текст
 public String description; // описание записи
}

Теперь добавим конструктор, который будет заполнять эти свойства. На вход конструктору должна передаваться конкретная запись из rss. Так как rss это всего лишь диалект XML, то передаем мы на вход конструктору ветвь <item>. Да. Далее конструктор циклически перебирает каждый тег находящийся внутри полученной записи и, встречая нужный тег, записывать из него информацию в соответствующее свойство класса. Реализовано это может быть так:

///
/// Конструктор для заполнения записи
///

public RssItem(XmlNode ItemTag)
{
 //просматриваем все теги записи
 foreach (XmlNode xmlTag in ItemTag.ChildNodes)
 {
  // проверяем имя тега, если соответствует одному из укаазных,
  // то в соответствующее свойство объекта записывается содержимое тега

  switch (xmlTag.Name)
  { 
   case "title":
   {
    this.Title = xmlTag.InnerText;
    break;
   }
   case "description":
   { 
    this.Description = xmlTag.InnerText;
    break;
   }
   case "link":
   {
    this.Link = xmlTag.InnerText;
    break;
   }
  }
 }
}

Да, кстати, так как мы работаем с XmlNode , нужно включить соответствующую сборку в using секцию:

using System.Xml;

RssItems

Этот класс у нас представляет собой список всех записей фида. Его будем реализовывать посредствам генериков, а именно моего любимого генерик класса List<T>. А нравится мне этот генерик тем, что предоставляет очень удобные методы работы с массивами данных. Добавим к проекту новый класс, и назовем его RssItems . Получаем следующее:

using System;
using System.Collections.Generic;
using System.Text;

namespace RssReader
{
 class RssItems
 {
 }
}

Далее наследуем RssItems от List<T>, вместо T указав тот тип, объекты которого будут храниться в списке. Заодно переопределим метод Contains(), для определения существования записи в списке по ее заголовку.

///
/// Проверка существования указаного элемента в списке
///

new public bool Contains(RssItem Item)
{
 foreach (RssItem itemForCheck in this)
 {
  // Сравниваем заголовки записей
  if (Item.Title == itemForCheck.Title)
  {
   // нашли совпадение. возвращаем истину
   return true;
  }
 }
 // совпадений не найдено. возвращаем лож
 return false;
}

Так же было бы не плохо иметь возможность поулчить интересующую нас запись из списка используя ее заголовок. Для этого напишем еще один метод, подобный методу Contains():

///
/// Получить запись из списка, по ее заголовку
/// 
public RssItem GetItem(String Title)
{
 foreach (RssItem itemForCheck in this)
 {
  //Сравниваем заголовок записей с запросом
  if (Item.Title == Title)
  {
   // нашли совпадение.возвращаем найденую запись
   return itemForCheck;
  }
 }
 // совпадений не найдено.
 return null;
}

RssFeed

Вот и добрались до основного класса нашего ридера. Этот класс будет хранить информацию о канале. Согласно спецификации rss 2.0 к обязательным элементам канала относятся: <title> — название канала, по которому люди будут ссылаться на сервис; <link> — URL веб-сайта, связанного с каналом; <description> — фраза или предложение для описания канала. Снова добавляем новый класс к проекту и называем его RssFeed .

using System;
using System.Collections.Generic;
using System.Text;

namespace RssReader
{
 class RssFeed
 {
 }
}

Так как все вышеперечисленные свойсвта канала обязательны, то нам необходимо добавить их и в наш класс. Так же мы добавляем свойство типа RssItems , для хранения списка записей канала:

class RssFeed
{
 public String Title; // заголовок канала
 public String Description; //описание канаала
 public String Link; // ссылка на связаный с каналом веб-сайт
 public RssItems Items; // список записей канала
}

Все что осталось сделать теперь, это написать конструктор класса, который будет получать, в качестве параметра, ссылку на rss канал, и, если rss там действительно существует, заполнять свойства создаваемого объекта данными из rss (надеюсь по коду вопросов не возникнет, я постарался его подробно прокомментировать):

public RssFeed(String Url) 
{ 
  // Инициализируем список записей 
  Items = new RssItems(); 
  // Создаем ридер для чтения Rss из указаного адреса 
  XmlTextReader xmlTextReader = New XmlTextReader(Url); 
  // создаем новый xml документ, для записи в него оплученого RSS 
  XmlDocument xmlDoc = New XmlDocument(); 
  try 
  { 
    // загружаем RSS в документ с помощью ридера 
    xmlDoc.Load(xmlTextReader); 
    // закрываем ридер за ненадобностью 
    xmlTextReader.Close(); 
    // так как вся информация об RSS-фиде записана между тегов ,
    // грузим получаем эту ветку. 
    XmlNode channelXmlNode = xmlDoc.GetElementsByTagName("channel")[0]; 
    // если ветка существует, то начинаем заоплнять свойства объекта 
    // данными из ветки 
    if (channelXmlNode != null) 
    { 
      // перебираем всех потомков тега 
      foreach (XmlNode channelNode in channelXmlNode.ChildNodes) 
      { 
        // если имя тега-потомка с интересующим нас, то записываем его данные 
        // в определенное совйство объекта 
        switch (channelNode.Name) 
        { 
          case "title": 
            { 
              Title = channelNode.InnerText; 
              break; 
            } 
          case "description": 
            { 
              Description = channelNode.InnerText; 
              break; 
            } 
          case "link": 
            { 
              Link = channelNode.InnerText; 
              break; 
            } 
          case "item": // если имя проверяемого тега равно item, то 
            { 
              // создаем из этого тега новый объект типа запись 
              RssItem channelItem = new RssItem (channelNode); 
              // и добавляем его к списку записей канала 
              Items.Add(channelItem); 
              break; 
            } 
        } 
      } 
    } 
    else // если в полученом файле тега не найдено, то выбрасываем исключение 
    { 
      throw New Exception("Ошибка в XML. Описание канала не найдено!"); 
    } 
  } 
  // если url канала указане не верно то выбрасываем исключение о недоступности источника   
  catch (System.Net.WebException ex) 
  { 
    if (ex.Status == System.Net.WebExceptionStatus.NameResolutionFailure) 
      throw new Exception("Невозможно соединиться с указаным источником.\r\n" + Url); 
    else throw ex; 
  } 
  // если в качестве адреса RSS был указан локальный пути, который еще и не существует, 
  // то выбрасываем соответствующее исключение 
  catch (System.IO.FileNotFoundException) 
  { 
    throw New Exception("Файл " + Url + " не найден!"); 
  } 
  // ну и на последок, ловим все остальные исключения, и передаем их дальше, как есть 
  catch (Exception ex) 
  { 
    throw ex; 
  } 
  finally 
  { 
    // закрываем ридер 
    xmlTextReader.Close(); 
  } 
}

Финальная стадия

Теперь, все что осталось сделать, это написать код обработчиков событий нажатия кнопки «обновить» и выбора элемента в ListView , а так же добавить глобальную переменную CurrentFeed, в которой будет храниться загруженный канал:

// Глобальная переменная хранящая данные канала 
RssFeed CurrentFeed; 

// оброботка нажатия на кнопку "Обновить" 
private void btRefresh_Click(object sender, EventArgs e) 
{ 
  // Проверяем задан ли адрес 
  if (!String.IsNullOrEmpty(tbUrl.Text)) 
  { 
    // Очищаем ListView перед добавлением новых данных 
    lvNews.Clear(); 
    // Инициализируем канал 
    CurrentFeed = new RssFeed(tbUrl.Text); 
    foreach (RssItem feedItem in CurrentFeed.Items) 
    { 
      // создаем элемент для вывода в ListView 
      ListViewItem listViewItem = new ListViewItem(feedItem.Title); 
      // задаем его имя 
      listViewItem.Name = feedItem.Title; 
      // заносим его в ListView 
      lvNews.Items.Add(listViewItem); 
    } 
  } 
}

// Оброботка смены выбора элемента в ListView 
private void lvNews_SelectedIndexChanged(object sender, EventArgs e) 
{ 
  // получаем связаную с выбраным ListViewItem новость 
  if (lvNews.SelectedItems.Count > 0 && // проверяем что чтото действительно выбрано 
      CurrentFeed != null && // проверяем, что канал инициализирован 
      CurrentFeed.Items.Count > 0 // проверяем существование записей в канале 
      ) 
  { 
    // выводим полный текст выбраной записи 
    wbDescription.DocumentText = CurrentFeed.Items.GetItem(lvNews.SelectedItems[0].Text).Description;
  }
}

Заключение

В итоге мы получили простой RSS-ридер, который может читать фиды стандарта 2.0. В следующей статье я постараюсь рассказать, как можно сделать наши классы более универсальными, а так же как можно организовать хранение истории посещенных лент. Скачать исходники написанного ридера можно здесь. PS: конструктивная критика, а так же предложения и пожелания приветствуются.

Рамиль Алиякберов a.k.a. R@Me0!

Теги: .NET, C#, RSS, Soft-programing, ООП

Комментарии ()