Когда мы начали работать над приложениями под 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 побольше приложений!
Автор: eastbanctech