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