Здравствуйте. Разработчики кроссплатформенных приложений под .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