MvvmCross для простого приложения iOS на C#

в 13:37, , рубрики: C#, iOS, mvvmcross, Visual Studio, xamarin, разработка под iOS

Здравствуйте.

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

Вот собственно и все.

MvvmCross для простого приложения iOS на C# - 1

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

Автор: Viktand

Источник


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


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