Здравствуйте. Разработчики кроссплатформенных приложений под .NET (далее все про C#) наверно знают о существовании MvvmCross. Отличный продукт, главным недостатком которого является весьма скудная документация. А на русском языке и того почти нет. Здесь я хочу в общих чертах рассказать о структуре простого приложения с меню для iOS на базе MvvmCross.
Следует сразу заметить, что если у вас нет настоящего железа от Apple, то вы не сможете ничего сделать, т.к. сборка и отладка проектов возможна только на реальных устройствах Apple. Хотя работать вы можете и под Windows. Т.е. примерно как запускать Android-приложения через отладку по USB. Такова политика этой конторы.
На момент написания этого текста MvvmCross не поддерживает .NETStandard, поэтому создавать какой-либо проект придется на VisualStudio 2015, но не 2017. Хотя готовый проект можно редактировать в 2017. Мне тут подсказали, что можно обойти эту проблему. Инструкция. Кроме того, в студии должен быть установлен Xamarin.
Все примеры кода максимально упрощены и наверно местами не совсем идеологически верны, но работают. Так например меню лучше создавать в виде таблицы и подключать к ней источник данных.
Подключаем MvvmCross: Через «Управление пакетами NuGet» ставим MvvmCross, MvvmCross.iOS.Support, MvvmCross.iOS.Support.XamarinSidebar, SidebarNavigation. Набор пакетов завит от версии, здесь про MvvmCross 5.12.
Итак, прежде всего следует создать новый проект типа «iOS-Universal-пустое приложение». Назовем его iOSTest. В папке проекта появятся разные файлы. Главным тут будет файл Main.cs. Именно этот файл будет запускать ваше приложение. Структура и роль его весьма проста. Он будет запускать AppDelegate.cs, который находится тут же рядом с Main.cs.
AppDelegate.cs создает главное окно и передает управление дальше. Для этого его перегруженную функцию FinishedLaunching следует переписать так:
Кроме того, следует поменять класс, от которого наследуется AppDelegate, т.е. объявление класса должно выглядеть так:
Остальное оставить как есть.
Чтобы все это работало, в проект необходимо добавить файл Setup.cs.
Код примерно такой:
Главным здесь является CreateApp(). Сейчас студия показывает на ошибку, потому что App() пока не существует.
Теперь нам необходимо создать новый проект в решении, где в соответствии с парадигмой MVVM будут лежать ViewModel’и. Этот проект должен быть типа «переносимый» или .NETStandard, но пока для MvvmCross это только проект типа PCL. Если у вас есть «заготовка» того проекта, то можно просто скопировать его в 2017-ю студию. Работать он будет без проблем. Назовем его «Core», подключим через NuGet MvvmCross и создадим в его корне файл App.cs. Подключим новый проект с первому как ссылку и ошибка в Setup.cs исчезнет.
Содержание App.cs весьма примитивно:
В строке RegisterAppStart(); мы наконец добираемся до первого реального работающего кода. Как не трудно догадаться здесь запускается «StartViewModel» Все предыдущее было подготовкой к запуску. Создадим в проекте Core папку ViewModels и в ней файл StartViewModel.cs:
Эта ViewModel умеет только одно: запускать MainViewModel. Тут следует сделать замечание, что такая прокладка кажется избыточной, почему бы сразу не запустить MainViewModel? До какой-то версии MvvmCross так и было, но сейчас что-то изменилось и мы не увидим меню, если запустить MainViewModel сразу. Возможно это баг и его поправят. Но если сейчас не использовать промежуточный класс, то меню будет справа, а не слева, как в его настройках, увидеть его можно только потянув за край экрана, кнопки вызова меню сверху не будет (подозреваю, что она за пределами экрана справа).
MainViewModel.cs:
Здесь все просто. Модель по сути пустая, единственное, что она может — это показать меню. В реальном проекте здесь может быть много разных функций по необходимости.
Рядом с MainViewModel.cs создадим MenuViewModel.cs для описания поведения меню:
Как видите, в меню будет всего две строки.
Теперь создадим модель для меню. Т.е. шаблон для строк. В проекте Core файл Models/MenuModel.cs:
Здесь описывается, что представляют из себя отдельные строки меню. Текст для строки и команда для исполнения.
Для перехода из пунктов меню создадим еще две почти одинаковые ViewModel’и. Разница будет только в имени. AboutViewModel.cs и SettingsViewModel.cs
В реальном коде тут следует размещать функции, которые будут исполняться при обращении к этой ViewModel.
Теперь возвращаемся в проект iOSTest и начинаем заниматься магией MvvmCross. Дело в том что связи между View и ViewModel происходят где-то внутри MvvmCross и в явном виде не представлены. Код вызывает ViewModel, а мы видим View, при этом указания какую View показывать в коде нет.
Как я понял, каждая View связана со своей ViewModel исключительно через название. Обратная связь как раз существует в объявлении View. В общем, все как-то неоднозначно.
Создадим в проекте iOSTest папку Views и в ней представления для всех ViewModel’ей: MainView, MenuView, AboutView, SettingsView. К сожалению, VisualStudio не лучшее средство для создания View. Следует воспользоваться «Xamarin iOS Designer» или «Xcode Interface Builder». Подробности можно прочитать тут: ссылка Это позволит вам создать красивые представления.
Но мы пойдем таким путем: Создадим файлы MainView.cs, MenuView.sc, AboutView.cs, SettingsView.cs как обычные классы и опишем представление в них программно. Будет не очень красиво, но просто.
Обратите внимание на атрибуты перед объявлением класса. Это привязка меню к этому View.
AboutView.cs (SettingsView аналогично)
Здесь атрибуты выглядят несколько иначе. «PushPanel» вместо «ResetRoot» приводит к тому, что вместо кнопки вызова меню появится кнопка «< Back», которая вернет вас в предыдущее окно. Т.е., если вы хотите иметь кнопку меню во всех окнах, то пишите «ResetRoot»
И, наконец, MenuView.cs:
Атрибуты опять слегка поменялись, видно, что меню будет слева. Значок меню лежит в файле «menu.png», который по всем правилам iOS должен находиться в папке Resources, желательно в трех разрешениях (размерах).
Если отказаться от реализации интерфейса IMvxSidebarMenu, то код можно сократить почти в два раза, но значка меню не будет. Будет надпись «Menu».
Вот собственно и все.

Весь проект можно посмотреть здесь: github
Следует сразу заметить, что если у вас нет настоящего железа от Apple, то вы не сможете ничего сделать, т.к. сборка и отладка проектов возможна только на реальных устройствах Apple. Хотя работать вы можете и под Windows. Т.е. примерно как запускать Android-приложения через отладку по USB. Такова политика этой конторы.
Все примеры кода максимально упрощены и наверно местами не совсем идеологически верны, но работают. Так например меню лучше создавать в виде таблицы и подключать к ней источник данных.
Подключаем MvvmCross: Через «Управление пакетами NuGet» ставим MvvmCross, MvvmCross.iOS.Support, MvvmCross.iOS.Support.XamarinSidebar, SidebarNavigation. Набор пакетов завит от версии, здесь про MvvmCross 5.12.
Итак, прежде всего следует создать новый проект типа «iOS-Universal-пустое приложение». Назовем его iOSTest. В папке проекта появятся разные файлы. Главным тут будет файл Main.cs. Именно этот файл будет запускать ваше приложение. Структура и роль его весьма проста. Он будет запускать AppDelegate.cs, который находится тут же рядом с Main.cs.
AppDelegate.cs создает главное окно и передает управление дальше. Для этого его перегруженную функцию FinishedLaunching следует переписать так:
AppDelegate.cs
public override bool FinishedLaunching(UIApplication app, NSDictionary options) { Window = new UIWindow(UIScreen.MainScreen.Bounds); var setup = new Setup(this, Window); setup.Initialize(); var startup = Mvx.Resolve<IMvxAppStart>(); startup.Start(); Window.MakeKeyAndVisible(); return true; }
Кроме того, следует поменять класс, от которого наследуется AppDelegate, т.е. объявление класса должно выглядеть так:
public class AppDelegate : MvxApplicationDelegate
Остальное оставить как есть.
Чтобы все это работало, в проект необходимо добавить файл Setup.cs.
Код примерно такой:
Setup.cs
using MvvmCross.Core.ViewModels; using MvvmCross.iOS.Platform; using MvvmCross.iOS.Views.Presenters; using MvvmCross.iOS.Support.XamarinSidebar; using MvvmCross.Platform.Platform; using UIKit; namespace iOSTest { public class Setup : MvxIosSetup { public Setup(MvxApplicationDelegate applicationDelegate, UIWindow window) : base(applicationDelegate, window) { } public Setup(MvxApplicationDelegate applicationDelegate, IMvxIosViewPresenter presenter) : base(applicationDelegate, presenter) { } protected override IMvxApplication CreateApp() { return new App(); } protected override IMvxIosViewPresenter CreatePresenter() { return new MvxSidebarPresenter((MvxApplicationDelegate)ApplicationDelegate, Window); } } }
Главным здесь является CreateApp(). Сейчас студия показывает на ошибку, потому что App() пока не существует.
Теперь нам необходимо создать новый проект в решении, где в соответствии с парадигмой MVVM будут лежать ViewModel’и. Этот проект должен быть типа «переносимый» или .NETStandard, но пока для MvvmCross это только проект типа PCL. Если у вас есть «заготовка» того проекта, то можно просто скопировать его в 2017-ю студию. Работать он будет без проблем. Назовем его «Core», подключим через NuGet MvvmCross и создадим в его корне файл App.cs. Подключим новый проект с первому как ссылку и ошибка в Setup.cs исчезнет.
Содержание App.cs весьма примитивно:
App.cs
using Core.ViewModels; using MvvmCross.Platform.IoC; namespace Core { public class App : MvvmCross.Core.ViewModels.MvxApplication { public override void Initialize() { CreatableTypes() .EndingWith("Service") .AsInterfaces() .RegisterAsLazySingleton(); RegisterAppStart<StartViewModel>(); } } }
В строке RegisterAppStart(); мы наконец добираемся до первого реального работающего кода. Как не трудно догадаться здесь запускается «StartViewModel» Все предыдущее было подготовкой к запуску. Создадим в проекте Core папку ViewModels и в ней файл StartViewModel.cs:
StartViewModel.cs
using MvvmCross.Core.ViewModels; namespace Core.ViewModels { public class StartViewModel: MvxViewModel { public void ShowMainView() { ShowViewModel<MainViewModel>(); } } }
Эта ViewModel умеет только одно: запускать MainViewModel. Тут следует сделать замечание, что такая прокладка кажется избыточной, почему бы сразу не запустить MainViewModel? До какой-то версии MvvmCross так и было, но сейчас что-то изменилось и мы не увидим меню, если запустить MainViewModel сразу. Возможно это баг и его поправят. Но если сейчас не использовать промежуточный класс, то меню будет справа, а не слева, как в его настройках, увидеть его можно только потянув за край экрана, кнопки вызова меню сверху не будет (подозреваю, что она за пределами экрана справа).
MainViewModel.cs:
MainViewModel.cs
using MvvmCross.Core.ViewModels; namespace Core.ViewModels { public class MainViewModel : MvxViewModel { public void ShowMenu() { ShowViewModel<MenuViewModel>(); } } }
Здесь все просто. Модель по сути пустая, единственное, что она может — это показать меню. В реальном проекте здесь может быть много разных функций по необходимости.
Рядом с MainViewModel.cs создадим MenuViewModel.cs для описания поведения меню:
MenuViewModel.cs
using System; using System.Collections.Generic; using Core.Models; using MvvmCross.Core.ViewModels; namespace Core.ViewModels { public class MenuViewModel : MvxViewModel { public List<MenuModel> MenuItems { get; } public MenuViewModel() { MenuItems = new List<MenuModel> { new MenuModel {Title = "Settings", Navigate = NavigateCommandSettings}, new MenuModel {Title = "About", Navigate = NavigateCommandAbout} }; } private MvxCommand<Type> _navigateCommandSettings; public MvxCommand<Type> NavigateCommandSettings { get { _navigateCommandSettings = _navigateCommandSettings ?? new MvxCommand<Type>((vm) => { ShowViewModel<SettingsViewModel>(); }); return _navigateCommandSettings; } } private MvxCommand<Type> _navigateCommandAbout; public MvxCommand<Type> NavigateCommandAbout { get { _navigateCommandAbout = _navigateCommandAbout ?? new MvxCommand<Type>((vm) => { ShowViewModel<AboutViewModel>(); }); return _navigateCommandAbout; } } } }
Как видите, в меню будет всего две строки.
Теперь создадим модель для меню. Т.е. шаблон для строк. В проекте Core файл Models/MenuModel.cs:
MenuModel.cs
using System; using MvvmCross.Core.ViewModels; namespace Core.Models { public class MenuModel { public String Title { get; set; } public MvxCommand<Type> Navigate { get; set; } } }
Здесь описывается, что представляют из себя отдельные строки меню. Текст для строки и команда для исполнения.
Для перехода из пунктов меню создадим еще две почти одинаковые ViewModel’и. Разница будет только в имени. AboutViewModel.cs и SettingsViewModel.cs
AboutViewModel.cs
using MvvmCross.Core.ViewModels; namespace Core.ViewModels { public class AboutViewModel : MvxViewModel { } }
В реальном коде тут следует размещать функции, которые будут исполняться при обращении к этой ViewModel.
Теперь возвращаемся в проект iOSTest и начинаем заниматься магией MvvmCross. Дело в том что связи между View и ViewModel происходят где-то внутри MvvmCross и в явном виде не представлены. Код вызывает ViewModel, а мы видим View, при этом указания какую View показывать в коде нет.
Как я понял, каждая View связана со своей ViewModel исключительно через название. Обратная связь как раз существует в объявлении View. В общем, все как-то неоднозначно.
Создадим в проекте iOSTest папку Views и в ней представления для всех ViewModel’ей: MainView, MenuView, AboutView, SettingsView. К сожалению, VisualStudio не лучшее средство для создания View. Следует воспользоваться «Xamarin iOS Designer» или «Xcode Interface Builder». Подробности можно прочитать тут: ссылка Это позволит вам создать красивые представления.
Но мы пойдем таким путем: Создадим файлы MainView.cs, MenuView.sc, AboutView.cs, SettingsView.cs как обычные классы и опишем представление в них программно. Будет не очень красиво, но просто.
MainView.cs
using Core.ViewModels; using CoreGraphics; using MvvmCross.iOS.Support.XamarinSidebar; using MvvmCross.iOS.Views; using UIKit; namespace iOSTest.Views { [MvxSidebarPresentation(MvxPanelEnum.Center, MvxPanelHintType.ResetRoot, true)] public class MainView : MvxViewController<MainViewModel> { public override void ViewDidLoad() { base.ViewDidLoad(); View.BackgroundColor = UIColor.LightGray; var label = new UILabel { Frame = new CGRect(10, 60, 200, 50), TextColor = UIColor.Magenta, Font = UIFont.FromName("Helvetica-Bold", 20f), Text = "MainWiew" }; Add(label); } } }
Обратите внимание на атрибуты перед объявлением класса. Это привязка меню к этому View.
AboutView.cs (SettingsView аналогично)
AboutView.cs
using Core.ViewModels; using CoreGraphics; using MvvmCross.iOS.Support.XamarinSidebar; using MvvmCross.iOS.Views; using UIKit; namespace iOSTest.Views { [MvxSidebarPresentation(MvxPanelEnum.Center, MvxPanelHintType.PushPanel, true)] public class AboutView : MvxViewController<AboutViewModel> { public override void ViewDidLoad() { base.ViewDidLoad(); View.BackgroundColor = UIColor.LightGray; var label = new UILabel { Frame = new CGRect(10, 60, 200, 50), TextColor = UIColor.Magenta, Font = UIFont.FromName("Helvetica-Bold", 20f), Text = "AboutView" }; Add(label); } } }
Здесь атрибуты выглядят несколько иначе. «PushPanel» вместо «ResetRoot» приводит к тому, что вместо кнопки вызова меню появится кнопка «< Back», которая вернет вас в предыдущее окно. Т.е., если вы хотите иметь кнопку меню во всех окнах, то пишите «ResetRoot»
И, наконец, MenuView.cs:
MenuView.cs
using System.Globalization; using Core.ViewModels; using CoreGraphics; using MvvmCross.iOS.Support.XamarinSidebar; using MvvmCross.iOS.Support.XamarinSidebar.Views; using MvvmCross.iOS.Views; using MvvmCross.Platform; using UIKit; namespace iOSTest.Views { [MvxSidebarPresentation(MvxPanelEnum.Left, MvxPanelHintType.PushPanel, false)] public class MenuView : MvxViewController<MenuViewModel>, IMvxSidebarMenu { private CGColor _borderColor = new CGColor(45, 177, 128); private readonly UIColor _backgroundColor = UIColor.FromRGB(140, 176, 116); private readonly UIColor _textColor = UIColor.Black; public override void ViewDidLoad() { base.ViewDidLoad(); View.BackgroundColor = _backgroundColor; EdgesForExtendedLayout = UIRectEdge.None; var label = new UILabel { Frame = new CGRect(10f, 30f, MenuWidth, 20f), TextColor = _textColor, Font = UIFont.FromName("Helvetica", 20f), Text = "Меню", TextAlignment = UITextAlignment.Center }; Add(label); var i = 0; foreach (var item in ViewModel.MenuItems) { var itemButton = new UIButton(); itemButton.Frame = new CGRect(10, 100+i, MenuWidth, 20); itemButton.SetTitle(item.Title, UIControlState.Normal); itemButton.TitleLabel.Font = UIFont.FromName("Helvetica", 20f); itemButton.TitleLabel.TextColor = _textColor; itemButton.BackgroundColor = _backgroundColor; itemButton.TouchUpInside += delegate { item.Navigate.Execute(); Mvx.Resolve<IMvxSidebarViewController>().CloseMenu(); }; i += 30; Add(itemButton); } } public void MenuWillOpen(){} public void MenuDidOpen(){} public void MenuWillClose(){} public void MenuDidClose(){} private int MaxMenuWidth = 300; private int MinSpaceRightOfTheMenu = 55; public bool AnimateMenu => true; public bool DisablePanGesture => false; public float DarkOverlayAlpha => 0; public bool HasDarkOverlay => false; public bool HasShadowing => true; public UIImage MenuButtonImage => new UIImage("menu.png"); public int MenuWidth => UserInterfaceIdiomIsPhone ? int.Parse(UIScreen.MainScreen.Bounds.Width.ToString(CultureInfo.InvariantCulture)) - MinSpaceRightOfTheMenu : MaxMenuWidth; public bool ReopenOnRotate => true; private bool UserInterfaceIdiomIsPhone => UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone; } }
Атрибуты опять слегка поменялись, видно, что меню будет слева. Значок меню лежит в файле «menu.png», который по всем правилам iOS должен находиться в папке Resources, желательно в трех разрешениях (размерах).
Если отказаться от реализации интерфейса IMvxSidebarMenu, то код можно сократить почти в два раза, но значка меню не будет. Будет надпись «Menu».
Вот собственно и все.

Весь проект можно посмотреть здесь: github