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

в 5:37, , рубрики: windows, Windows 8, XAML, библиотека, Блог компании EastBanc Technologies, разработка, разработка приложений, метки: , , , ,

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

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

В недрах нашей компании 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"/>

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

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>  

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

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.

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

Пока всё

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

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

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

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

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

Напоследок

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

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

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

Автор: eastbanctech

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js