Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

MVVM Framework для Windows Phone своими руками. Часть 1

Anna | 17.06.2014 | нет комментариев
Разработка приложений для платформ WPF, Silverlight, Windows Store и Windows Phone, примерно неизменно подразумевает применение паттерна MVVM. Это правомерно, так как базовой философией этих платформ, является распределение представления (так же я буду применять термин интерфейс пользователя) и остальной логики программы. Данный подход разрешает получить следующие превосходства:

  1. Распределение пользовательского интерфейса и логики представления: что разрешает дизайнерам трудиться над пользовательским интерфейсом, а программистам над бизнес логикой приложения применяя для взаимодействия отвлеченный интерфейс модели представления
  2. Расширенные вероятности автоматизированного тестирования: отделение пользовательского интерфейса от остальной логики, разрешает всецело протестировать логику представления без ограничений накладываемых автоматизацией тестирования через пользовательский интерфейс
  3. Множественные представления для одной модели представления: одна модель представления может применяться многими реализациями интерфейса пользователя. Скажем, сокращенный и полный вариант представления данных, интерфейс зависящий от прав пользователя. Вероятность применять одну реализацию модели представления на разных платформах
  4. Расширенные вероятности повторного применения компонентов: так как модели представления отделены от реализации представления, допустимы всякие варианты их применения, наследование от базовых моделей, композиция нескольких моделей и т.п.

Разрабатывая приложения под платформу Windows Phone, я столкнулся с тем, множество статей описывают базовую реализацию паттерна MWWM, которая традиционно сводится к реализации в классе модели представления интерфейса INotifyPropertyChanged, создания примитивный реализации ICommand и примитивные сценарии связывания этих данных с представлением. К сожалению, остаются за рамками обсуждения такие значимые вопросы как, реализация обобщенных классов с комфортным интерфейсом, синхронизация потоков при асинхронном исполнении, навигация на ярусе модели представления и многие другие.

Отдавая должное таким фреймворкам как MVVM Light и Prism, я выбираю в своих планах применять собственную реализацию данного паттерна, так как даже самые примитивные фреймворки излишне массивны в силу своей универсальности.

Данная статья рассчитана на начинающих разработчиков, знакомых с основами разработки приложений для платформы Windows Phone, которые хотят больше подробно вникнуть в реализацию паттерна MVVM для платформы Windows Phone и обучиться находить и использовать больше эластичные и примитивные решения для реализации приложений, построенных с его применением. Допустимо опытные разработчики обнаружат для себя статью увлекательной и предложат другие комфортные решения описанных задач.

В качестве примера, сотворим примитивное приложение «Кредитный калькулятор», каждая функциональность которого будет реализована в Code-behind жанре.

Приложение содержит каждого две страницы: основная страница приложения предуготовлена для ввод параметров кредита и страница подробной информации о рассчитанном кредите предуготовлена для отображения подробной информации о расчете. Начальные коды этого плана доступны на GitHub ветка codebehind

Фрагмент файла разметки основной страницы MainPage.xaml
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
      <ScrollViewer>
        <StackPanel>
          <StackPanel.Resources>
            <Style TargetType="TextBlock" BasedOn="{StaticResource PhoneTextNormalStyle}"/>
          </StackPanel.Resources>

          <TextBlock Text="Сумма кредита" />
          <TextBox x:Name="viewAmount" InputScope="Number" />
          <TextBlock Text="Процентная ставка"/>
          <TextBox x:Name="viewPercent" InputScope="Number" />
          <TextBlock Text="Срок кредита" />
          <TextBox x:Name="viewTerm" InputScope="Number"/>
          <Button x:Name="viewCalculate" Content="расчитать" Click="CalculateClick" />

          <Border x:Name="viewCalculationPanel" BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
            <StackPanel>
              <StackPanel Orientation="Horizontal">
                <TextBlock Text="Платеж:" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock x:Name="viewPayment" Style="{StaticResource PhoneTextNormalStyle}"/>
              </StackPanel>
              <StackPanel Orientation="Horizontal">
                <TextBlock Text="Выплаты:" Style="{StaticResource PhoneTextNormalStyle}"/>
                <TextBlock x:Name="viewTotalPayment" Style="{StaticResource PhoneTextNormalStyle}" />
              </StackPanel>
              <Button Content="детально" Click="DetailsClick" />
            </StackPanel>
          </Border>
        </StackPanel>
      </ScrollViewer>
    </Grid>

    <Grid x:Name="viewProgressPanel" Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource OpacityBackgroundBrush}" Visibility="Collapsed">
      <ProgressBar Opacity="1" IsIndeterminate="True" />
    </Grid>

В данной разметке всецело отсутствует связывание данных. Все данные устанавливаются с поддержкой доступа к свойствам элементов управления из code-behind файла.

Code-behind файл основной страницы MainPage.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Phone.Controls;

namespace MVVM_Article
{
  public partial class MainPage
    : PhoneApplicationPage
  {
    public MainPage()
    {
      InitializeComponent();
    }

    private void CalculateClick(object sender, RoutedEventArgs e)
    {
      decimal amount;
      decimal percent;
      int term;

      if(!decimal.TryParse(viewAmount.Text, out amount))
      {
        viewProgressPanel.Visibility = Visibility.Collapsed;
        MessageBox.Show("Сумма должна быть числом");
        return;
      }

      if(!decimal.TryParse(viewPercent.Text, out percent))
      {
        viewProgressPanel.Visibility = Visibility.Collapsed;
        MessageBox.Show("Процент должен быть числом");
        return;
      }

      if(!int.TryParse(viewTerm.Text, out term))
      {
        viewProgressPanel.Visibility = Visibility.Collapsed;
        MessageBox.Show("Срок кредита должен быть числом");
        return;
      }

      Focus();
      viewProgressPanel.Visibility = Visibility.Visible;

      Task.Run(() =>
        {
          try
          {
            var payment = Calculator.CalculatePayment(amount, percent, term);

            Dispatcher.BeginInvoke(() =>
              {
                viewCalculationPanel.Visibility = Visibility.Visible;
                viewPayment.Text = payment.ToString("N2");
                viewTotalPayment.Text = (payment * term).ToString("N2");
              });
          }
          finally
          {
            Dispatcher.BeginInvoke(() =>
              {
                viewProgressPanel.Visibility = Visibility.Collapsed;
              });
          }
        });
    }

    private void DetailsClick(object sender, RoutedEventArgs e)
    {
      var pageUri = string.Format("/DetailsPage.xaml?amount={0}&percent={1}&term={2}", viewAmount.Text, viewPercent.Text, viewTerm.Text);
      NavigationService.Navigate(new Uri(pageUri, UriKind.Relative));
    }
  }
}

Обратите внимание на то, что часть расчетов перенесена в фоновый поток, в данном случае обоснованной необходимости в этом нет. Это сделано специально, Дабы охватить тему синхронизации потоков. Все свойства элементов управления обязаны задаваться из основного потока приложения, если нужно установить качество элемента управления из иного потока, нужно передать управление основному потоку приложения. Для этих целей применяется объект Dispatcher страницы, тот, что неизменно связан с основным потоком приложения.

Передача параметров на страницу подробного изложения кредита, осуществляется через параметры URI страницы.

Страница подробного изложения кредита организована сходственным образом. Обратить внимание стоит на заполнение таблицы расписания платежей, данный блок было проще реализовать применяя ItemsControl. Но такая реализация требует применения связывания данных и не подходит для целей статьи.

Заполнение таблицы расписания платежей в файле DetailsPage.xaml.cs
var style = (Style)Resources["PhoneTextNormalStyle"];

foreach(var record in schedule)
{
  var grid = new Grid();
  grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
  grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
  grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });

  var loanElement = new TextBlock
  {
    Text = record.Loan.ToString("N2"),
    Style = style
  };
  Grid.SetColumn(loanElement, 0);

  var interestElement = new TextBlock
  {
    Text = record.Interest.ToString("N2"),
    Style = style
  };
  Grid.SetColumn(interestElement, 1);

  var balanceElement = new TextBlock
  {
    Text = record.Balance.ToString("N2"),
    Style = style
  };
  Grid.SetColumn(balanceElement, 2);

  grid.Children.Add(loanElement);
  grid.Children.Add(interestElement);
  grid.Children.Add(balanceElement);

  viewRecords.Children.Add(grid);
}

Логика расчета кредита реализована в отдельном статическом классе Calculator. Обратите внимание на задержку в начале способа расчета платежа, ее задача имитировать насыщенные расчеты, на выполнение которых требуется некоторое время. Попытка вызова этого способа в основном потоке приложения приведет к зависанию пользовательского интерфейса. Для предотвращения нужно исполнять все ресурсоемкие задачи в фоновых потоках.

Фрагмент файла Calculator.cs
internal static class Calculator
  {
    public static decimal CalculatePayment(decimal amount, decimal percent, int term)
    {
      Task.Delay(1000).Wait();

      percent /= 1200;
      var common = (decimal) Math.Pow((double) (1   percent), term);
      var multiplier = percent*common/(common - 1);

      var payment = amount*multiplier;
      return payment;
    }

    public static List<PaymentsScheduleRecord> GetPaymentsSchedule(decimal amount, decimal percent, int term)
    {
      var balance = amount;
      var interestRate = percent / 1200;

      var payment = CalculatePayment(amount, percent, term);

      var schedule = new List<PaymentsScheduleRecord>();
      for (var period = 0; period < term; period  )
      {
        var interest = Math.Round(balance * interestRate, 2);
        var loan = payment - interest;
        balance -= loan;

        var record = new PaymentsScheduleRecord
        {
          Interest = interest,
          Loan = loan,
          Balance = balance
        };

        schedule.Add(record);
      }
      return schedule;
    }
  }

Простейшая реализация MVVM

Сейчас реализуем самую примитивную версию MVVM, для этого сотворим для всякой страницы модель представления, которая будет реализовывать интерфейс INotifyPropertyChanged применяемый для уведомления представления об изменениях свойств объекта. Начальный код доступен на GitHub в ветке naivemvvm

Реализация классом интерфейса полагает генерацию события PropertyChanged всякий раз, когда значение свойства объекта изменяется. Такое поведение разрешает привязкам данных отслеживать состояние объекта и обновлять данные пользовательского интерфейса при изменении значения связанного свойства.

Фрагмент файла MainPageViewModel.cs
public class MainPageViewModel
	: INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;

	protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
	{
		var handler = PropertyChanged;
		if (handler != null)
		{
			handler(this, new PropertyChangedEventArgs(propertyName));
		}
	}
 }

Обратите внимание на применение признака [CallerMemberName], данный признак указывает компилятору, что в данный параметр нужно передать имя члена класса из которого был вызван способ. Это разрешает не передавать в способ имя свойства в очевидном виде, если способ вызывается из самого свойства.

Пример реализации свойства модели представления
private string _amount;
public string Amount
{
	get { return _amount; }
	set
	{
		_amount = value;
		OnPropertyChanged();
	}
}

Позже установки значения поля вызывается способ OnPropertyChanged тот, что генерирует событие об изменении значения свойства из которого он был вызван.

Модель представления может предоставлять покупателям команды, которые разрешают исполнять определенные моделью представления действия. Команды представляют собой объекты реализующие интерфейс ICommand, если покупателю нужно исполнить действие заданное командой, он должен вызвать способ Execute команды. Команда предоставляет покупателям информацию, о том может ли она быть исполнена либо нет. Для приобретения информации о доступности команды нужно вызвать способ CanExecute, а так же подписаться на событие CanExecuteChanged которое уведомит покупателей об изменении состояния команды.

Реализация команды для всякого отдельного действия модели представления дюже трудоемкий процесс, для его упрощения сотворим класс DelegateCommand тот, что будет делегировать выполнение способов команды делегатам заданным при создании экземпляра класса

Файл DelegateCommand.cs
public sealed class DelegateCommand
		: ICommand
	{
		private readonly Action<object> _execute;
		private readonly Func<object, bool> _canExecute;

		public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null)
		{
			if(execute == null)
			{
				throw new ArgumentNullException();
			}

			_execute = execute;
			_canExecute = canExecute;
		}

		public bool CanExecute(object parameter)
		{
			return _canExecute == null
				|| _canExecute(parameter);
		}

		public void Execute(object parameter)
		{
			if(!CanExecute(parameter))
			{
				return;
			}

			_execute(parameter);
		}

		public event EventHandler CanExecuteChanged;

		public void RiseCanExecuteChanged()
		{
			var handler = CanExecuteChanged;
			if(handler != null)
			{
				handler(this, EventArgs.Empty);
			}
		}
	}

Объявление команды модели представления с применением класса DelegateCommand

private DelegateCommand _calculateCommand;
		public DelegateCommand CalculateCommand
		{
			get
			{
				if(_calculateCommand == null)
				{
					_calculateCommand = new DelegateCommand(o => Calculate());
				}

				return _calculateCommand;
			}
		}

Позже создания модели представления, внесем метаморфозы в изложение пользовательского интерфейса. Для этого удалим каждый код из файла MainPage.xaml.cs, а в конструкторе страницы установим значение свойства DataContext страницы, позже этого мы сумеем применять привязки данных.

Файл MainPage.xaml.cs позже изменений
using Microsoft.Phone.Controls;
using MVVM_Article.ViewModels;

namespace MVVM_Article
{
	public partial class MainPage
		: PhoneApplicationPage
	{
		public MainPage()
		{
			InitializeComponent();

			DataContext = new MainPageViewModel();
		}
	}
}

Обратите внимание, что code-behind страницы уменьшился до одной строки, в следующих главах эта строка так же будет удалена.

Дальше нужно задать привязки данных в изложении пользовательского интерфейса. Для задания привязок данных применяется конструкция {Binding Path=<Имя свойства>}, в большинстве случаев Path дозволено опустить и сократить запись до вида {Binding <Имя свойства>}.

Пример связывания данных, фрагмент файла MainPage.xaml
<TextBlock Text="Срок кредита" />
<TextBox Text="{Binding Term, Mode=TwoWay}" InputScope="Number"/>
<Button Content="рассчитать" Command="{Binding CalculateCommand}" />

<Border BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}">

Обратите внимание на параметр Mode=TwoWay при задании связывания для текстового поля, данный параметр указывает привязке данных, что при изменении значения свойства элемента управления, нужно его передать в поле модели представления. Таким образом модель представления получает данные пользовательского ввода. Качество Visibility элемента управления и IsLoaded модели представления, не могут быть связанны на прямую, потому что их типы разны. Для решения сходственных задач предуготовлены конвертеры значений.

Для привязки свойства типа Boolean к свойству типа Visibility сотворим конвертер, BoolToVisibilityConverter

public class BoolToVisibilityConverter
		: IValueConverter
	{
		public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
		{
			return (value as bool?) == true
				? Visibility.Visible
				: Visibility.Collapsed;
		}

		public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
		{
			throw new NotImplementedException();
		}
	}

Применяя данный конвертер дозволено объединять между собой поля типа Boolean и Visibility.

Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}"

К сожалению при реализации паттерна MVVM для страницы DeptailsPage не удалось всецело избавиться от code-behind потому, он применяется для инициализации модели представления параметрами переданными из основной страницы.

Завершение

Нынешнее приложение официально соблюдает паттерн MVVM, но реально мы легко перенесли code-behind из класса страницы в обособленный класс. Реализация имеет уйма недостатков и не разрешает пользоваться приемуществами MVVM описанными в начале статьи.

В следующих статьях будут рассмотрены темы: применение DI в MVVM, реализация навигации, взаимодействия с пользователем, суммирование базового класса MVVM и многое другое.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB