
Я идеалист.
Разработчику программных продуктов тяжело быть идеалистом — оптимизация кода никогда не прекращается. Всегда, чуть позже, находятся более элегантные решения, что приводит к постоянному рефакторингу кода. Если проект имеет ограниченные сроки, то это хоть как-то останавливает процесс постоянного улучшения, но в случае личных разработок, программист ограничен лишь своими знаниями, которые, в свою очередь, тоже постоянно растут, что превращается в замкнутый круг.
Но, отойдем немного от философии к практике. Разрабатывая приложения, я всегда стремился к идеалу, стремился следовать актуальным концепциям. По ходу разработки я всегда старался следовать принципу 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>
Результат будет выглядеть примерно так:
На написание статьи ушло намного больше времени, чем на само тестовое приложение :)Ссылки по теме:

