Сам себе 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: конструктивная критика, а так же предложения и пожелания приветствуются.
Теги: .NET, C#, RSS, Soft-programing, ООП