Я идеалист.
Разработчику программных продуктов тяжело быть идеалистом — оптимизация кода никогда не прекращается. Всегда, чуть позже, находятся более элегантные решения, что приводит к постоянному рефакторингу кода. Если проект имеет ограниченные сроки, то это хоть как-то останавливает процесс постоянного улучшения, но в случае личных разработок, программист ограничен лишь своими знаниями, которые, в свою очередь, тоже постоянно растут, что превращается в замкнутый круг.
Но, отойдем немного от философии к практике. Разрабатывая приложения, я всегда стремился к идеалу, стремился следовать актуальным концепциям. По ходу разработки я всегда старался следовать принципу DRY. Некоторое время назад я начал заниматься разработкой под Windows Phone. В результате появились «обертки» для операций, которые используются чаще всего. Некоторыми из них хочу поделиться.
DRY и зачем это нужно
DRY – Don`t Repeat Yourself.
Это принцип разработки программного обеспечения, нацеленный на снижение повторения информации различного рода, особенно в системах со множеством слоёв абстрагирования. Принцип DRY формулируется как: «Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы».
Википедия
В каждом проекте существуют типовые операции, которые повторяются из одного проекта в другой. Казалось бы, не так сложно написать несколько строчек кода, но это не правильно. Данный подход увеличивает время разработки и вероятность повторения ошибок. Не нужно каждый раз описывать одно и то же, те же перехваты ошибок, те же сообщения, тот же код из проекта в проект, более правильно будет вынести типовые операции в некую библиотеку и попросту использовать ее.
Таким образом, мы:
- избегаем повторного написания рутинного кода, который занимает огромную часть времени разработки
- избегаем ошибок, которые уже были замечены и исправлены в предыдущих проектах
- дробим код на более обобщенные абстракции, что упрощает его поддержку и понимание в целом
Библиотека
Я уверен, что подобные наработки есть у многих. Данная библиотека написана исходя из моих личных потребностей. Хочу заметить, что с выходом Windows Phone 8 и Visual Studio 2012, а также обновления для Visual Studio 2010 и Windows Phone 7.5, эти примеры уже не несут большой практической пользы, так как подобные операции заменены async/await. Но для демонстрации принципа DRY вполне подходят.
Исходный код библиотеки доступен на http://vb.codeplex.com
Итак, в библиотеке существует два класса, один для работы с асинхронным чтением ресурсов, второй для работы с локальным хранилищем файлов. Каждый класс содержит свои методы, свойства, события и перехват ошибок.
Рассмотрим более детально.
Класс LoadManager
Конструктор | |
LoadManager | public class LoadManager Инициализирует новый экземпляр объекта. |
Методы | |
Load | public void Load(string url) Создает новый объект WebClient и вызывает его метод DownloadStringAsync. |
Свойства | |
Encoding | public int Encoding Указывает, какую кодировку нужно использовать при чтении данных. Важно! Для работы с различными кодировками используется библиотека MSPToolkit.dll. Она уже добавлена в проект. Этот параметр необязателен, но иногда, например, для работы с кодировкой 1251, его нужно использовать. |
SaveTo | public string SaveTo Имя локального файла, в который будут сохраняться данные. Файл храниться в изолированном хранилище приложения. |
События | |
OnCancel | public event System.Action OnCancel Срабатывает при отмене загрузки данных. |
OnError | public event System.Action<Exception> OnError Срабатывает при ошибке загрузки данных. |
OnFinish | public event System.Action OnFinish Срабатывает при окончании загрузки, независимо были ошибки или нет. Удобно использовать для скрытия панели прогресса загрузки. |
OnLoad | public event System.Action<string> OnLoad Срабатывает при удачной загрузке данных. Данные приходят в виде строки. |
OnNoNetwork | public event System.Action OnNoNetwork Срабатывает при отсутствии сети. Конечно, если сеть найдена, но сам доступ к интернету отсутствует, то отработает уже событие OnError, с сообщением о недоступности ресурса. |
OnProgress | public event System.Action<DownloadProgressChangedEventArgs> OnProgress Срабатывает при изменении прогресса загрузки. |
OnStart | public event System.Action OnStart Срабатывает при старте загрузки данных. Удобно использовать для показа панели прогресса загрузки. |
Конструктор | |
FileManager | public class FileManager Инициализирует новый экземпляр объекта. |
Методы | |
Read | public void Read(string FileName) Открывает файл на чтение. Принимает имя файла в виде строки. |
Save | public void Save(string FileName, string Data) Открывает существующий файл на запись или создает новый при его отсутствии. |
Свойства | |
WriteAfter | public string WriteAfter Строка, которая будет добавлена в конце данных. Не всегда XML имеет формат, который нормально сериализуется. Например, иногда не хватает рутового узла. Данную проблему можно решить более изящно, но самый простой метод, это обернуть данные в дополнительный рутовый узел. |
WriteBefore | public string WriteBefore Строка, которая будет добавлена в начале данных (по аналогии с WriteAfter). |
События | |
OnReadError | public event System.Action<Exception> OnReadError Срабатывает при возникновении ошибки чтения файла. |
OnReadFileMissing | public event System.Action OnReadFileMissing Срабатывает при отсутствии файла, который пытаются открыть на чтение. Если это событие было перехвачено, то событие OnReadError уже не будет вызываться. |
OnReadReady | public event System.Action<StreamReader> OnReadReady Срабатывает, когда файл открыт на чтение. Передает открытый дескриптор файла. |
OnSaveError | public event System.Action<Exception> OnSaveError Срабатывает при ошибке записи файла. |
Простые примеры
Пример использования LoadManager:
LoadManager DataLoader = new LoadManager();
DataLoader.OnLoad += new Action<string>(DataLoader_OnLoad);
DataLoader.Load(resorce_url);
void DataLoader_OnLoad(string data)
{
try
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
// do something with data string
});
}
catch (Exception ex)
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
MessageBox.Show(ex.Message, "Exception", MessageBoxButton.OK);
});
}
}
Пример использования FileManager:
FileManager CacheFile = new FileManager();
CacheFile.OnReadReady += new Action<StreamReader>(File_OnReadOpen);
CacheFile.Read(file_name);
void File_OnReadOpen(StreamReader Stream)
{
// do something with file stream
}
Казалось бы, ничего сложного и особенного. Добавлен дополнительный уровень абстракции и код вынесен в отдельную библиотеку. Но теперь этот код, который уже написан и готов к использованию, без труда можно применить в других проектах!
Реальный пример
Для примера разработаем информер курса валют, который будет обновлять данные в формате XML со стороннего сервера. Просто читать XML и выводить данные не интересно, поэтому дополнительно результат будет кэшироваться локально, для того чтобы курс валют можно было просмотреть и без интернета. Конечно, он не будет актуальным, но лучше показать последний обновленный результат, нежели просто пустую строчку.
Я не буду выводить листинги всех файлов, так как исходный код проекта доступен на http://exchangeexample.codeplex.com. Уделю внимание только ключевым моментам, где была использована библиотека.
Итак, схема такова:
- читаем локальный файл, если он существует
- если есть сеть, пытаемся получить обновленные данные
- сохранить кэш в локальном файле
- снова перечитываем локальный файл для обновления данных на экране
Считывание локального файла
// читаем локальный файл
public void LoadData()
{
// создаем екземпляр класса
FileManager CacheFile = new FileManager();
// вешаем обработчик события на успешное открытие файла
CacheFile.OnReadReady += new Action<StreamReader>(File_OnReadOpen);
// вешаем обработчик на отсутствие файла, который пытаемся открыть
CacheFile.OnReadFileMissing += new Action(File_OnReadFileMissing);
// собственно начинаем читать файл
CacheFile.Read(Common.Constants.ExchangeTmpFile);
}
// вызввается если файл не найден
void File_OnReadFileMissing()
{
// выводим сообщение что файл не найден
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
MessageBox.Show("Local file missing, first time application start?", "FileManager OnReadFileMissing exception", MessageBoxButton.OK);
});
}
// вызвается при успешном открытии файла на чтение
void File_OnReadOpen(StreamReader Stream)
{
using (XmlReader XmlReader = XmlReader.Create(Stream))
{
// сериализируем XML и записываем данные в коллекцию
XmlSerializer DataSerializer = new XmlSerializer(typeof(RatesList));
_RatesList = (RatesList)DataSerializer.Deserialize(XmlReader);
Rates = _RatesList.Collection;
}
}
Обновление локального файла
// обновляем локальный файл
public void UpdateData()
{
// создаем екземпляр класса
LoadManager DataLoader = new LoadManager();
// вешаем обработчик события на отсутствие сети
DataLoader.OnNoNetwork += new Action(DataLoader_OnNoNetwork);
// вешаем обработчик события на возникновение ошибки
DataLoader.OnError += new Action<Exception>(DataLoader_OnError);
// вешаем обработчик события на начало загрузки данных
DataLoader.OnStart += new Action(DataLoader_OnStart);
// вешаем обработчик события на успешное выполнение загрузки данных
DataLoader.OnLoad += new Action<string>(DataLoader_OnLoad);
// вешаем обработчик события на окончание загрузки данных
DataLoader.OnFinish += new Action(DataLoader_OnFinish);
// переопределяем логику сохранения файла, так как нам нужно изменить формат XML при
// сохранении
DataLoader.OnSaveTo += new Action<string>(DataLoader_OnSaveTo);
// указываем имя локального кэш файла
DataLoader.SaveTo = Common.Constants.ExchangeTmpFile;
// инициализируем загрузку данных
DataLoader.Load(Common.Constants.ExchangeApiUrl);
}
// вызывается когда данные загруженны и готовы к кэшированию в файл
// переопределяем метод сохранения файла
// событие не обзательно, по умолчанию файл сохраняется as is
void DataLoader_OnSaveTo(string data)
{
// создаем новый екземпляр класса
FileManager CacheFile = new FileManager();
// добавляем текст (открывыющий тег узла) в начало текста
CacheFile.WriteBefore = "<Root>";
// добавляем текст (закрывыющий тег узла) в конец текста
CacheFile.WriteAfter = "</Root>";
// открываем файл на запись, и сохраняем данные
CacheFile.Save(Common.Constants.ExchangeTmpFile, data);
}
// вызывается когда сеть не доступна
void DataLoader_OnNoNetwork()
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
MessageBox.Show("No network available.", "LoadManager OnNoNetwork exception", MessageBoxButton.OK);
});
}
// вызывается когда возникла ошибка
void DataLoader_OnError(Exception e)
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
MessageBox.Show(e.Message, "LoadManager OnError exception", MessageBoxButton.OK);
});
}
// вызывается перед стартом загрузки данных
void DataLoader_OnStart()
{
// показываем панель процесса загрузки
IsProgressVisible = true;
// указываем что данные нужно будет перечитать
IsDataLoaded = false;
}
// вызывается после выполнения загрузки, независимо с ошибками или нет
void DataLoader_OnFinish()
{
// прячем панель процесса загрузки
IsProgressVisible = false;
}
// вызывается при успешной загрузке, когда данные закэшированы и готовы к обработке
void DataLoader_OnLoad(string data)
{
try
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
// опять перечитываем локальный файл
LoadData();
// обновляем дату последнего апдейта данных
LastUpdate = DateTime.Now;
});
}
catch (Exception ex)
{
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
MessageBox.Show(ex.Message, "LoadManager OnLoad outer exception", MessageBoxButton.OK);
});
}
}
Указываем контекст
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// указываем контеск для страницы
DataContext = App.MainViewModel;
// вешаем обработчик загрузки страницы
Loaded += new RoutedEventHandler(MainPage_Loaded);
}
// вызвается когда страница загружена
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// переносим в отдельный поток, чтобы не тормозил UI
Deployment.Current.Dispatcher.BeginInvoke(
delegate
{
// вызываем обновление данных
App.MainViewModel.UpdateData();
});
}
}
XAML для биндинга и вывода данных
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" ItemsSource="{Binding Rates}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17">
<StackPanel Orientation="Horizontal">
<TextBlock Text="1" Style="{StaticResource PhoneTextLargeStyle}" Foreground="{StaticResource PhoneAccentBrush}" />
<TextBlock Text="{Binding Currency}" Style="{StaticResource PhoneTextLargeStyle}" Foreground="{StaticResource PhoneAccentBrush}"/>
</StackPanel>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Buy" Style="{StaticResource PhoneTextNormalStyle}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Buy}" Style="{StaticResource PhoneTextLargeStyle}"/>
<TextBlock Text="UAH" Style="{StaticResource PhoneTextLargeStyle}" Opacity="0.5" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Sale" Style="{StaticResource PhoneTextNormalStyle}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Sale}" Style="{StaticResource PhoneTextLargeStyle}"/>
<TextBlock Text="UAH" Style="{StaticResource PhoneTextLargeStyle}" Opacity="0.5"/>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,12,0,0">
<TextBlock Text="Last update:" Style="{StaticResource PhoneTextNormalStyle}" />
<TextBlock Text="{Binding LastUpdate}" Style="{StaticResource PhoneTextAccentStyle}" />
</StackPanel>
</Grid>
Результат будет выглядеть примерно так:
На написание статьи ушло намного больше времени, чем на само тестовое приложение :)
Ссылки по теме: