Пример принципа DRY в Windows Phone 7

в 8:47, , рубрики: DRY, windows phone, windows phone 7, Блог компании Инфопульс Украина, разработка под windows phone, метки: , ,

Пример принципа DRY в Windows Phone 7

Я идеалист.

Разработчику программных продуктов тяжело быть идеалистом — оптимизация кода никогда не прекращается. Всегда, чуть позже, находятся более элегантные решения, что приводит к постоянному рефакторингу кода. Если проект имеет ограниченные сроки, то это хоть как-то останавливает процесс постоянного улучшения, но в случае личных разработок, программист ограничен лишь своими знаниями, которые, в свою очередь, тоже постоянно растут, что превращается в замкнутый круг.

Но, отойдем немного от философии к практике. Разрабатывая приложения, я всегда стремился к идеалу, стремился следовать актуальным концепциям. По ходу разработки я всегда старался следовать принципу 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

Конструктор
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>

Результат будет выглядеть примерно так:
Пример принципа DRY в Windows Phone 7На написание статьи ушло намного больше времени, чем на само тестовое приложение :)

Ссылки по теме:

Автор: vbilenko

Источник

Поделиться

* - обязательные к заполнению поля