В недрах нашей компании 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/
Добавить комментарий