Pull to refresh
0
True Engineering
Лаборатория технологических инноваций

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

Reading time 11 min
Views 8.1K
Когда мы начали работать над приложениями под 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 побольше приложений!
Tags:
Hubs:
+8
Comments 30
Comments Comments 30

Articles

Information

Website
www.trueengineering.ru
Registered
Founded
Employees
101–200 employees
Location
Россия