Я идеалист.
Разработчику программных продуктов тяжело быть идеалистом — оптимизация кода никогда не прекращается. Всегда, чуть позже, находятся более элегантные решения, что приводит к постоянному рефакторингу кода. Если проект имеет ограниченные сроки, то это хоть как-то останавливает процесс постоянного улучшения, но в случае личных разработок, программист ограничен лишь своими знаниями, которые, в свою очередь, тоже постоянно растут, что превращается в замкнутый круг.
Но, отойдем немного от философии к практике. Разрабатывая приложения, я всегда стремился к идеалу, стремился следовать актуальным концепциям. По ходу разработки я всегда старался следовать принципу 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
Указывает, какую кодировку нужно использовать при чтении данных. |
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
Срабатывает при отсутствии сети. |
OnProgress | public event System.Action<DownloadProgressChangedEventArgs> OnProgress
Срабатывает при изменении прогресса загрузки. |
OnStart | public event System.Action OnStart
Срабатывает при старте загрузки данных. |
Класс FileManager
Конструктор | |
FileManager | public class FileManager
Инициализирует новый экземпляр объекта. |
Методы | |
Read | public void Read(string FileName)
Открывает файл на чтение. |
Save | public void Save(string FileName, string Data)
Открывает существующий файл на запись или создает новый при его отсутствии. |
Свойства | |
WriteAfter | public string WriteAfter
Строка, которая будет добавлена в конце данных. |
WriteBefore | public string WriteBefore
Строка, которая будет добавлена в начале данных (по аналогии с WriteAfter). |
События | |
OnReadError | public event System.Action<Exception> OnReadError
Срабатывает при возникновении ошибки чтения файла. |
OnReadFileMissing | public event System.Action OnReadFileMissing
Срабатывает при отсутствии файла, который пытаются открыть на чтение. |
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();
});
}
}
<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>
Результат будет выглядеть примерно так:
На написание статьи ушло намного больше времени, чем на само тестовое приложение :)
Ссылки по теме:
- DRY
- Async/Await
- Visual Studio Async CTP для VS2010, включающее поддержку Async/Await
- Исходный код библиотеки
- Исходный код тестового проекта
Автор: vbilenko