Варим MVVM для Windows Store-приложений

от автора

Когда мы начали работать над приложениями под Windows 8, мы искали библиотеку поддержки шаблона Model-View-ViewModel (MVVM) для этой платформы. Некоторое время провели в интернете в поиске таковой, но в итоге приняли факт, что таких библиотек в природе пока не существует (возможно, мы плохо искали, но теперь это уже не так важно). Ответ на вопрос «что делать?» напрашивался сам…

В недрах нашей компании EastBanc Technologies была создана специальная библиотека (кодовое название EBT.Mvvm). Цель создания — экономия времени в будущем при разработке сложных приложений для Windows 8. В библиотеку вошли как наши собственные наработки, так и некоторые идеи и примеры, которые встречались нам во время наших поисков.

Итак, что мы имеем: все помнят, что основная идея шаблона — это ослабление связи между ViewModel (будем называть вью-модель) и непосредственно View (представление). Идеальное состояние — это когда code-behind представления содержит только конструктор с InitializeComponent и, возможно, код поддержки визуального поведения, которое нельзя определить через XAML. Таким образом, разработчик отдает представление дизайнеру, а сам сосредотачивается на работе и тестировании логики приложения.

Данная статья ориентирована на разработчиков, уже знакомых с программированием на C# и XAML под Windows 8. Ниже мы приводим описания основных фич нашей библиотеки в виде примеров кода их использования и комментариев. Итак, поехали:

1. Базовый класс ViewModel

Первое, с чего нужно начинать, говоря о MVVM шаблоне, это базовый класс для наших вью-моделей. Основное предназначение — поддержка интерфейса INotifyPropertyChanged и удобные функции для автоматической нотификации при изменении свойств. Пример использования:

public class SimpleViewModel : ViewModel   {       private int _number;          public int Number       {           get { return _number; }           set { OnPropertyChange(ref _number, value); }       }   }   

Тут всё должно быть понятно без комментариев. Следует добавить, что есть набор перегруженных функций для автоматической нотификации при изменении свойства. Также имеется способ избежать написания поля вообще. Имеется в виду так называемый backing field. Пример — поле _number в примере кода выше. При этом свойства можно продолжать создавать с поддержкой автоматической нотификации. Это достаточно удобно, если во вью модели у нас имеется множество свойств для связывания. Пример ниже показывает, как можно сделать свойство с учётом этой фичи (поле не требуется).

public string Text { 	get { return GetPropertyValue(() => Text); } 	set { SetPropertyValue(() => Text, value); } }  
2. Команды

Привычный и необходимый обработчик команд RelayCommand. Привязывается к свойству Command базового класса ButtonBase (кнопки, пункты меню, гиперссылки) и поддерживает ICommand интерфейс. Вещь незаменимая и реализована уже давно. Тем не менее, должна быть упомянута:

public class SimpleViewModel : ViewModel {   	public SimpleViewModel()   	{   		SampleCommand = new RelayCommand(OnSample);   	}   	public RelayCommand SampleCommand { get; private set; }      	private void OnSample()   	{ 		// TODO Do something here.   	}   }   
<Button Command="{Binding SampleCommand}" Content="Button Text" /> 
3. Связывание обработчиков событий

Мы добавили возможность удобно связывать обработчики событий. MVVM подразумевает, что обработка событий пользовательского интерфейса должна происходить на стороне вью-модели. Без небольшого трюка сделать это невозможно. Он состоит в связывании присоединённого свойства элемента пользовательского интерфейса. На текущий момент библиотека поддерживает обработку большого количества событий. Список при необходимости может расширить сам разработчик. В качестве примера приведём обработку события Tapped элемента TextBlock:

public class SimpleViewModel   {   	public SimpleViewModel()   	{   		TappedCommand = new EventCommand<Point>(OnTapped);   	}   	public IEventCommand TappedCommand { get; private set; }    	private void OnTapped(Point point)   	{   		TappedCommand.PreventBubbling = point.X < 100;   	}   }   
<TextBlock Mvvm:EventBinding.Tapped="{Binding TappedCommand}" Text="Tap me"/> 

Тут стоит обратить внимание на строку с TappedCommand.PreventBubbling = point.X < 100. Дело в том, что мы предусмотрели возможность отменить дальнейшую обработку событий (Handled) выставив соответствующий флаг.

На текущий момент есть поддержка событий: SelectionChanged, Click, ItemClick, KeyDown, KeyUp, PointerReleased, PointerPressed, PointerMoved, PointerCanceled, PointerEntered, PointerExited, PointerCaptureLost, Tapped, RightTapped, PointerWheelChanged, ManipulationStarting, ManipulationStarted, ManipulationDelta, ManipulationInertiaStarting, ManipulationCompleted, LostFocus, Unloaded, Loaded.

4. Поддержка различных режимов экрана

На наш взгляд, это самая интересная фича библиотеки. Для целевых приложений, ориентированных на планшеты прямо-таки незаменимая! Помним, что есть четыре режима экрана и что поддерживать их все — хороший тон. У нас есть два механизма для изменения отображения элементов пользовательского интерфейса в зависимости от текущего режима экрана.

  • Управление видимостью. Основан на изменении видимости каждого конкретного элемента и удобен для простых сценариев.
  • Изменение стиля. Иногда с точки зрения производительности это более эффективный метод для сложных сценариев пользовательского интерфейса.
<TextBlock behaviors:OrientationBehavior.Orientations="Landscape,Filled,Portrait" Text="Not snapped"/>     <TextBlock behaviors:OrientationBehavior.Orientations="Snapped" Text="Snapped"/>    

В следующем примере показано изменение ориентации списка в зависимости от режима экрана.

<GridView ItemsSource="{Binding YourItems}">          <behaviors:OrientationBehavior.LandscapeStyle>           <!-- This style will be applied in landscape, filled and portrait modes. -->           <Style TargetType="ListViewBase"/>       </behaviors:OrientationBehavior.LandscapeStyle>          <behaviors:OrientationBehavior.SnappedStyle>           <!-- This style will be applied in the snapped mode. -->           <Style TargetType="ListViewBase">               <Style.Setters>                   <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>                   <Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled"/>                   <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>                   <Setter Property="ScrollViewer.VerticalScrollMode" Value="Auto"/>                   <Setter Property="ItemsPanel">                       <Setter.Value>                           <ItemsPanelTemplate>                               <VirtualizingStackPanel Orientation="Vertical"/>                           </ItemsPanelTemplate>                       </Setter.Value>                   </Setter>               </Style.Setters>           </Style>       </behaviors:OrientationBehavior.SnappedStyle>   </GridView> 

Метод изменения стиля элемента — это очень удобная и мощная фича. При её использовании необходимо помнить про следующее:

  • При использовании этой фичи мы не можем использовать свойство Style для элементов.
  • Если применён для одного из режимов экрана, то как минимум этот стиль будет применяться во всех режимах если не указаны другие.
  • Для каждого из режимов экрана каждый из этих стилей имеет приоритет. Например, если есть стиль для портретной ориентации и для snapped, то портретный стиль будет применяться для ландшафтного и заполненного режима. Если указан только один стиль — он будет применяться во всех режимах.

А приятное следствие в использовании метода изменения стиля состоит в том, что при таком подходе, используя ContentControl/ContentPresenter, можно изменять view template полностью! Ниже показано как это делается:

<Grid Name="main">       <ContentControl>           <behaviors:OrientationBehavior.LandscapeStyle>               <Style TargetType="ContentControl">                   <Setter Property="ContentTemplate">                       <Setter.Value>                           <DataTemplate>                               <Grid>                                   <TextBlock Text="Landscape"/>                                   <!-- Something in landscape mode -->                               </Grid>                           </DataTemplate>                       </Setter.Value>                   </Setter>               </Style>           </behaviors:OrientationBehavior.LandscapeStyle>           <behaviors:OrientationBehavior.PortraitStyle>               <Style TargetType="ContentControl">                   <Setter Property="ContentTemplate">                       <Setter.Value>                           <DataTemplate>                               <Grid>                                   <TextBlock Text="Portrait"/>                                   <!-- Something in portrait mode -->                               </Grid>                           </DataTemplate>                       </Setter.Value>                   </Setter>               </Style>           </behaviors:OrientationBehavior.PortraitStyle>           <behaviors:OrientationBehavior.SnappedStyle>               <Style TargetType="ContentControl">                   <Setter Property="ContentTemplate">                       <Setter.Value>                           <DataTemplate>                               <Grid>                                   <TextBlock Text="Snapped. Only text here"/>                               </Grid>                           </DataTemplate>                       </Setter.Value>                   </Setter>               </Style>           </behaviors:OrientationBehavior.SnappedStyle>       </ContentControl>   </Grid>   

Например, таким образом можно без лишних проблем сделать переход в snapped режим.

5. Вызов методов View из ViewModel

Иногда бывает необходимо вызвать методы пользовательского интерфейса из вью модели. В качестве примера можно привести необходимость установить фокус ввода на заданное поле. Это можно сделать с помощью нашего ControlWrapper:

public class SimpleViewModel : ViewModel   {       public SimpleViewModel()       {           TextBoxWrapper = new ControlWrapper();       }       public ControlWrapper TextBoxWrapper { get; private set; }          public void GotoField()       {           TextBoxWrapper.Focus();       }   }   
<TextBox Mvvm:ElementBinder.Wrapper="{Binding TextBoxWrapper}"/> 
6. Триггеры событий для анимации

Этот механизм позволяет вам стартовать анимацию, когда происходит событие в элементе представления. И опять ни строчки кода в code-behind! Метод основан на привязывании обработчиков событий. В XAML нужно определить специальную команду TriggerCommand:

<Grid>       <FrameworkElement.Resources>           <Storyboard x:Key="FadeOut">               <PointerDownThemeAnimation Storyboard.TargetName="MyElement"/>           </Storyboard>           <Storyboard x:Key="FadeIn">               <PointerUpThemeAnimation Storyboard.TargetName="MyElement"/>           </Storyboard>       </FrameworkElement.Resources>              <Border x:Name="MyElement" Width="100" Height="100" Background="Red">              <mvvm:EventBinding.PointerPressed>               <mvvm:TriggerCommand Storyboard="{StaticResource FadeOut}"/>           </mvvm:EventBinding.PointerPressed>              <mvvm:EventBinding.PointerReleased>               <mvvm:TriggerCommand Storyboard="{StaticResource FadeIn}"/>           </mvvm:EventBinding.PointerReleased>          </Border>   </Grid> 
7. Привязывание контекстного меню

ContextMenuBehavior позволяет быстро и удобно отображать контекстное меню на нажатие правой клавиши мыши или tap на тачскрине. Во вью необходимо только сделать связывание на элементе, для которого будет вызвано контекстное меню. А в модели определить список команд и обработчики:

public class MyViewModel : ViewModel   {       private IList<UICommand> _contextMenuCommands;       private string _text;          public string Text       {           get { return _text; }           set { OnPropertyChange(ref _text, value); }       }          public IList<UICommand> ContextMenuCommands       {           get           {               return _contextMenuCommands ?? (_contextMenuCommands = new List<UICommand>              {                   new UICommand("Copy", OnCopy),                   new UICommand("Paste", OnPaste),               });           }       }          private void OnCopy(IUICommand command)       {           var content = new DataPackage();           content.SetText(Text);           Clipboard.SetContent(content);       }          private async void OnPaste(IUICommand command)       {           var content = Clipboard.GetContent();           Text = await content.GetTextAsync();       }   }   
<TextBlock behaviors:ContextMenuBehavior.Commands="{Binding ContextMenuCommands}" Text="{Binding Text}" MinWidth="300" Height="40"/> 

8. Привязывание popup

PopupBehavior позволяет создать функционал показа popup при нажатии на правую кнопку мыши или tap на тачскрине. Всё должно быть ясно из примера кода ниже:

<TextBlock Text="Tap or right click here for more information" behaviors:PopupBehavior.Placement="Above">       <behaviors:PopupBehavior.Content>           <DataTemplate>               <TextBlock Text="More information..."/>           </DataTemplate>       </behaviors:PopupBehavior.Content>   </TextBlock>   


9. Межстраничная навигация

Одной из проблем для разработчика является страничная навигация — не очень удобно поддерживать чистоту code-behind, если переходы осуществляются через обращения к Frame из представления. И практически всегда возникает потребность обработки событий Navigating и Navigated во вью-модели.

Для достижения целей создаем основную модель нашего приложения:

public class RootModel {     public RootModel()     {         NavigationState = new NavigationState();         HomePageModel = new HomePageModel(this);     }     public NavigationState NavigationState { get; set; }     public HomePageModel HomePageModel { get; set; }      public bool CanGoBack { get { return NavigationState.CanGoBack; } }     public void GoBack()     {         NavigationState.GoBack();                 }     public void GoToHomePage()     {         NavigationState.Navigate(typeof (HomePage));     } } 

При запуске приложения устанавливаем основную модель как контекст верхнеуровнего элемента визуального дерева объектов и связываем класс-обёртку NavigationState с frame.

sealed partial class App : Application {     ...     public RootModel RootModel { get; private set; }      protected override void OnLaunched(LaunchActivatedEventArgs args)     {         RootModel = new RootModel();         var frame = new Frame { DataContext = RootModel };          // Bind the NavigationState and the frame using the ElementBinder class.         // You can also do this in XAML.         ElementBinder.SetWrapper(frame, RootModel.NavigationState);          Window.Current.Content = frame;         Window.Current.Activate();          RootModel.GoToHomePage();     } } 

Теперь наша вью-модель HomePageModel может обрабатывать события OnNavigating и OnNavigated. А также осуществлять навигацию на другие страницы через сохраненную ссылку на _rootModel. Обратите внимание, что OnNavigating поддерживает отмену перехода (параметр ref bool cancel).

public class HomePageModel : PageModel // Or implement IPageModel. {     private RootModel _rootModel;   // You can call _rootModel.NavigationState.Navigate(…)     public HomePageModel(RootModel rootModel)     {         _rootModel = rootModel;     }     public override void OnNavigated()     {         // TODO Do something here to initialize/update your page.     }     public override void OnNavigating(ref bool cancel)     {         // TODO Do something here to clean up your page.     } } 

В XAML выставляем правильный DataContext страницы для корректной работы связывания.

<Page x:Class="YourNamespace.HomePage" ... DataContext="{Binding HomePageModel}">     <!-- Your page content goes here --> </Page> 

Всё, результат достигнут. Теперь можно создавать страницы и связывать их c вью-моделями. Последние будут обрабатывать события OnNavigating и OnNavigated и управлять навигацией.

10. Шаблон для генерации скелетного проекта

Мы предусмотрели возможность быстро создать каркас для проекта с использованием нашей библиотеки. Шаблон проекта встраивается в Visual Studio и появляется в проектах Windows Store. Также шаблон доступен в библиотеке онлайн шаблонов проектов Visual Studio.

Пока всё

Ну вот, кажется, этого для одной статьи достаточно. На самом деле были перечислены хоть и большинство, но не все фичи нашей библиотеки. Есть ещё конвертеры, сохранение и восстановления состояния, помощник для charm-панели. Остальное хабрачитатели смогут самостоятельно узнать, непосредственно установив и использовав этот проект. Так что плавно переходим к следующему пункту:

Где можно скачать?

Заинтересованные хабрачитатели захотят посмотреть описанную бибилиотеку в действии. Сделать это очень просто. Наша библиотека доступна для скачивания в виде Nuget Package. Также наш проект заведён на CodePlex.

Самый быстрый способ установить её в студию — воспользоваться поиском в 12 студии через Tools-> Extensions and Updates. Выберите Online и в поисковой строке наберите ключевые слова Windows 8 MVVM.

Напоследок

«Библиотека EBT.Mvvm распространяется по принципу «как есть», разработчик не несет ответственности за возможные последствия…»

А если серьёзно, то мы будем рады, если наша библиотека поможет разработчикам приложений под молодую платформу Windows 8 сэкономить время на преодоление проблем, с которыми пришлось столкнуться нам самим. По мере сил и возможностей мы постоянно исправляем и улучшаем этот программный проект. Ваши предложения и замечания могут нам в этом помочь.

Хочется пожелать всем хабрачитателям, занимающимся разработкой, удачи. Создадим для Windows Store побольше приложений!

ссылка на оригинал статьи http://habrahabr.ru/company/eastbanctech/blog/172839/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *