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

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

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


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

Комментарии 41

    0
    Я может что то пропустил, но вы считаете что viewPayment.Text = payment.ToString(«N2»); есть MVVM?
    Сначала подумал что мне показалось, открыл исходники… нет не показалось…
      0
      Если не трудно укажите, где именно находится данный код, похоже на ошибку.
        0
        Я извиняюсь, изначально видно ссылка была не на ту ветку (codebehind), не обратил внимания.
          0
          Ок, я тоже мог напутать при оформлении статьи.
      –3
      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();
              }
          }
      

      Это не самый лучший подход. Тесты получаются не так выразительны как могли-бы быть. Гораздо лучше делать свойство типа Visibility во View Model
        +3
        Согласен, что с точки зрения тестирования проверка свойства Visibility может быть более лаконична.

        Однако по моему мнению свойство типа Visibility в View Model это перенос логики уровня представления на уровень модели представления.

        В данном случае модель представления содержит поле, которое описывает ее состояние, например IsBusy. На уровне представления мы можем отобразить надпись «Работаю...», выключить все элементы управления или отобразить/скрыть элемент управления. Используя подход предложенный вами, нам пришлось бы реализовать 6 полей вместо одного, принимая решение о том, что показывать, а что нет за уровень представления.
          –3
          нам пришлось бы реализовать 6 полей вместо одного, принимая решение о том, что показывать, а что нет за уровень представления.

          И я абсолютно за такой подход. ViewModel это модель View. Они неразрывно связанны. Единственная разница заключается в том что уровень ViewModel очень просто тестировать, а вот View слой тестировать достаточно сложно. Ну и separation of concern -> логика View уходит во ViewModel, а вот стиль отображения остается во View. В моей практике — так получалось проще.
            +3
            Получается, что каждый раз, когда нам понадобится значение нового типа которое напрямую коррелирует с базовым полем IsBusy мы будем вынуждены расширять ViewModel новыми полями. Но подход интересный, буду иметь его ввиду, пригодиться.
              –2
              Присоединяюсь к заминусованному выше товарищу. Пару лет назад, когда я программировал на WPF, мы пришли к подходу «ViewModel — конвертер на стероидах». Это позволяет отказаться от уродливых конвертеров типа BoolToVisibilityConverter (которые сильно усложняются по сравнению с синтетическим примером, если они не «чистые» функции, а учитывают внешний контекст и имеют состояние). Косвенным следствием такого подхода является отказ от использования CallerMemberName.

              Предположим, у вас в модели есть дата (DateTimeOffset), а в представлении она отражается в лейбле (string, ISO 8601), прогресс-баре (double, время, прошедшее с начала суток в процентах) и цвете фона (SolidColorBrush, тёмно-синий для ночи и светло-бежевый для дня).

              У меня в модели представления было бы три разных свойства-геттера: string, double и Brush (типы представления), одно свойство- (или метод-) сеттер типа DateTimeOffset (тип фактических данных). Сеттер не бросал бы событие об изменении CallerMemberName, потому что его имя никак не относится к привязываемым свойствам — он бросает события соответствующих свойств, к которым привязаны label, progress bar, etc. Поля, которые лежат за этими четырьмя свойствами можно делать в зависимости от ситуаций: или одно поле (_dateTime) типа данных (и конвертировать в каждом из геттеров), или иметь все поля (_dateTimeString, _dateTimePercentage) представления и единожды рассчитывать их в сеттере, а в геттерах просто возвращать. Можно варьировать в зависимости от того, как чато будут чтения и записи.

              ViewModel идёт в паре со своим представлением, при смене представления меняется и ViewModel. Общим интерфейсом ViewModel'а может служить интерфейс изменения, он оперирует доменными типами, и ничего не знает о способе фактического отображения этих типов.
                +1
                Описанное выше совсем не ViewModel это тот же старый добрый code behind только вынесенный в внешний класс. Сама парадигма такова, что ViewModel не должен ничего знать о представлении, совсем не чего.
                  0
                  Не могли бы вы немного пояснить это мнение? View Model — это модель представления. Я так понял, что она должна содержать абстрактное описания представления без привязки к конкретному фреймворку.

                  В википедии написано, что это вариант PresentationModel см у Фаулера ( martinfowler.com/eaaDev/PresentationModel.html ), где в примере можно, например увидеть isComposerEnabled
                    0
                    Еще немножко подумал, скорее согласен с подходом с Converter, но мне не нравится его название BoolToVisibilityConverter — оно не отражает, что он конвертирует bool в пару Collapsed | Visible, а не Hidden | Visible — надо либо как-то параметризовывать, либо запихивать Collapsed в имя конвертера. Как вы на это cмотрите?
                      0
                      Конвертер является платформозависимым компонентом, в контексте платформы Windows Phone тип Visibility содержит два значения Collapsed и Visible. Нет необходимости загромождать название конвертера и вводить параметры.
                        0
                        Спасибо за объяснение, к тому же я увидел что в WPF существует такой же конвертер и тоже именно с Collapsed.
                      0
                      ViewModel это абстракция, минимально необходимый контракт для взаимодействия пользователя с остальным приложением. Модель представления не должна предоставлять избыточных данных. Если например событие имеет Тип, Дату и Описание, модель представления должна представлять потребителям, только эти данные.
                      А представление должно решать как эти данные использовать, показать как текст в консоли, показать в виде графика или любом другом виде.
                        0
                        ViewModel это абстракция, минимально необходимый контракт для взаимодействия пользователя с остальным приложением.

                        Нет. ViewModel — это не абстракция. Это просто такая штука для DataBinding'а. Переводится «ViewModel» как модель представления, а не представление модели. Можно, конечно, обмазаться IViewModel, но это всё premature abstraction.

                        А представление должно решать как эти данные использовать

                        Что значит «решать»? Иметь какой-то код, который «решает»? То есть иметь, по-сути, code behind?

                        Идею «конвертер на стероидах» предложил не я, а Джош Смит.
                +5
                Вовсе они не связаны. ViewModel не должна ничего знать о View, и уж тем более знать о её устройстве и о том, какие элементы с какими свойствами та содержит.

                Представьте, что у вас есть элемент во View, который становится Collapsed если состояние IsBusy, и вместо использования конвертера вы создали свойство типа Visibility во ViewModel. Завтра вам сказали, что вместо скрытия этого элемента лучше менять его цвет на красный. В случае использования bool свойства вы просто пишете новый конвертер, никак не меняя ViewModel. В вашем подходе же придется лезть во ViewModel и создавать новое свойство типа Brush, а старое свойство типа Visibility удалять.

                И это в случае изменений во View, а если к вашей ViewModel подключаются несколько представлений, которые используют разные подходы к обработке свойства IsBusy? Например, в универсальном Windows 8 / Windows Phone приложении, где в Windows 8 вы скрываете элемент, а в WP — изменяете его цвет.

                Ну и в целом, руководствоваться выразительностью тестов при принятии таких решений — сомнительная практика, на мой взгляд.
                  +1
                  Можно пойти дальше.

                  UI-related свойства у ViewModel автоматически дают нам следующие «преимущества»:
                  * ViewModel получается платформозависимая (без хаков не получится шарить её между SL, WP8, W8 и WP7
                  * Обламываемся с нормальным запуском юнит-тестов.
            0
            Я возможно вызову на себя праведный гнев сообщества, но: DelegateCommand это скорее зло чем добро.

            Идея использования комманд растет из responsibility segregation, т.е. когда мне надо делать расчет платежей я оформляю это в отдельный класс, который реализует помимо алгоритма еще и определение того, когда это действие применимо, уведомления когда действие стало применимо и т.д. И затем использую его либо глобально в приложении, либо создавая инстансы в конкретных ViewModel. Зачем? Затем что, как правило, команды повторно используются и требуют бОльшего покрытия чем ViewModel и имеют четкие зависимости.
              0
              По моему мнению, передача команды из модели на прямую в предсатвление намного большее зло, чем использование комманд в качестве оберток.
              +1
              Проблема MVVM которая меня больше всего заботит: есть например датамодель на EF. Фактически для скучных enterprise приложений ViewModel на 70% ее брат-близнец, только там свойства немножко другие штуки возвращают и реализуют NotifyPropertyChanged. По мне это адский оверхед, как для написания (кто-то будет сидеть, писать туповатые viewmodel-и, отрабатывать change requests, раздувать кодобазу без надобности и ненавидеть свою жизнь), так и для исполнения (чтобы показать список объектов надо создать их близнецов в памяти). Есть ли какие-то рекомендации в этом направлении? LightSwitch не предлагать :-)
                0
                Да проблема более чем актуальна, когда я знаю, что для определенной сущности понадобится «расширенный» близнец, в ход пускаю авто генерацию кода, часть кода которая реализует обертки для общих членов. А всю уникальную функциональность реализую в рядом лежащем partial классе
                +2
                Не сочтите за критику, статья действительно интересная и полезная… для общего образования.
                Я могу понять желание не использовать Prism для WP. Но MVVM Light достаточно легок и прост в освоении, чтобы не заморачиваться написанием своего велосипеда.
                  0
                  Дело наверное в том, что я в общем то люблю велосипедостроительство, с целью достижения понимания происходящего, но обычно к этому моменту свой велосипед уже не хуже готовых, а местами даже и лучше :)
                  0
                  У меня вопрос возник по ходу чтения. В MainPageViewModel метод Calculate делает проверки и выводит сообщение пользователю.
                  private void Calculate()
                  		{
                  			IsCalculated = false;
                  
                  			if (!decimal.TryParse(Amount, out _calculatedAmount))
                  			{
                  				MessageBox.Show("Сумма должна быть числом");
                  				return;
                  			}
                  ...............
                  



                  Было бы не плохо сразу фокусироваться на том элементе, который не прошел проверку. И не выводить сообщение, а подписать нужный элемент. Каким образом Вы бы рекомендовали это сделать?
                    0
                    Это самая простая и наивная реализация валидации. Валидация будет более подробно рассмотрена в слудующих частях статьи. Если в двух словах, то валидация должна осуществляться в момент ввода информации пользователем и сразу отображать ошибку если она есть. Сделать это можно разными способами, я предпочитаю реализацию с помощью присоеденяемых свойств или с помошью создания собственных элементов управления.
                    0
                    Не увидел здесь ничего особенного. Как вы с таким паттерном обработаете события OnNavigatedTo и пр.? Очень похоже на обычный Binding.
                      0
                      Прошу набраться терпения, я решил идти от простого к сложному ну или более простому, это как посмотреть. А обработка событий навигации будет рассмотрена в следующей части.
                        0
                        Хорошо. Тема MVVM для меня крайне болезнена. Я считаю, что использование MVVM в мобильных приложениях не всегда оправдано. Более того, даже в настольных приложениях готов подискутировать на эту тему. То, что показано выше нельзя называть MVVM. Это же только DataContext…
                      0
                      Если честно немного теряюсь от «Очень похоже на обычный Binding» и «Это же только DataContext». Думаю стоит начать с вопроса, а что такое MVVM для вас?
                        0
                        Добрый день! Спасибо за статью!

                        Хотелось бы также поделиться опытом и, может быть, посоветовать что-то полезное…
                        На мой взгляд, применение лямбда-выражений всё же даёт больше гибкости, чем только CallerMemberName-аттрибут.
                        Также их можно использовать совместно для взаимодополнения друг друга.
                        Ещё ваша реализация команд, насколько я понял, в некоторых сценариях может приводить к утечкам памяти из-за подписки контрола на событие CanExecuteChanged. Касательно же WinPhone вообще можно реализовать что-то похожее на RoutedCommands из WPF.

                        Я помню, что вы читали статью WinPhone: пути к совершенству, в которой описаны некоторые фишечки для разработчиков. Очень рекомендую скачать пример из неё… Там как раз используется свой собственный MVVM-фреймворк.

                        И, конечно же, советую прочесть статью с примером MVVM: новый взгляд. Возможно, кому-то покажутся интересными довольно оригинальные апгрейды для MVVM.

                        Все наработки из статей применены на реальных проектах, например, Easel (WinPhone) или Poet (WPF), поэтому можно убедиться, что всё работает отлично.
                          +1
                          Спасибо за комментарий, все описанные статьи я читал. Что касается вопросов, то большая часть их будет раскрыта в следующих частях статьи. Очень надеюсь завершить работу над ними в ближайшее время.
                            0
                            Использование лямбда выражений так же будет описано, хотя я от их использования отошел, по причинам производительности.
                              0
                              Насчёт последнего я бы поспорил :) Мне на практике ещё ни разу не встретился случай, где реально ощущалось бы падение производительности из-за использования лямбд. Конечно, я могу представить гипотетическую ситуацию, например, привязка к значению таймера, где свойство обновляется каждые несколько миллисекунд, но вместе с тем никто не отменяет другие способы для нотификации…
                            0
                            По-моему это обычный MVVM, только со своей RelayCommand (как раз листал книгу), а хочется большего.
                            Я пока только присматриваюсь к фреймворкам в WP8, до этого на as3 проектах использовал puremvc и robotlegs. После них очень хочется автоматический инжектор, сообщения через ивентбас, медиаторы тоже довольно удобны.

                            MVVM Light Toolkit шаг в правильную сторону, но мне кажется он слегка неказист.
                              +1
                              Самый крутой MVVM — это RxUI (ReactiveUI). GitHub, сайт. Очень рекомендую, особенно для тех, кто уважает Rx. Работает под XAML, Silverlight, WP8, WinRT, iOS, Android (Xamarin).
                                0
                                немного смутил их сайт, проект стабильно развивается?
                                  0
                                  Сайт, видимо, давно не обновляли. Видюшка старая лежит. Но проект очень даже развивается. Его основные контрибуторы — это Paul Betts и Phil Haack. Эти чуваки работали в MS, а сейчас работают в GitHub. На RxUI в частности сделан GitHub for Windows.
                                0
                                Похоже, 2-я часть не за горами ))

                                Тут речь заходила о MVVM Light, но, почему-то, никто не сказал о Caliburn.Micro. Очень мощный инструмент.
                                  0
                                  К сожалению реакция на первую часть отбила всякое желание продолжать
                                    0
                                    А вот зря вы так.

                                    А какая реакция вас расстроила?
                                    В комментариях вся адекватная дисскусия сводится к тому, что вы какие-то моменты будете раскрывать в следующих статьях. А следующей статьи как раз и нет ))

                                    Или слив кармы тому виной? Так это дело во-первых наживное, во-вторых не самоцель.

                                    По идее тему можно развивать, тем более, что «свой велосипед» уже написан. А при обсуждении, как правило, можно подчерпнуть и узнать что-то новое или переосмыслть старое. Так что пишите ))

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое