MvvmCross for a simple iOS application in C #
Hello. Developers of cross-platform applications for .NET (hereinafter referred to as C #) probably know about the existence of MvvmCross. An excellent product, the main drawback of which is the very scarce documentation. And in Russian, even that is almost nonexistent. Here I want to outline in general terms the structure of a simple application with a menu for iOS based on MvvmCross.
It should be noted immediately that if you do not have real hardware from Apple, then you can not do anything, because assembly and debugging of projects is possible only on real Apple devices. Although you can work under Windows. Those. about how to run Android applications via USB debugging. This is the policy of this office.
At the time of writing this text, MvvmCross does not support .NETStandard, so it will be necessary to create any project on VisualStudio 2015, but not 2017. Although the finished project can be edited in 2017. I was prompted here that I can circumvent this problem. Instruction In addition, Xamarin must be installed in the studio.
All code examples are maximally simplified and probably in some places are not quite ideologically correct, but they work. So for example, it is better to create a menu in the form of a table and connect a data source to it.
We connect MvvmCross: Through “Managing NuGet packages” we put MvvmCross, MvvmCross.iOS.Support, MvvmCross.iOS.Support.XamarinSidebar, SidebarNavigation. The set of packages depends on the version, here about MvvmCross 5.12.
So, first of all, you need to create a new project like "iOS-Universal-empty application." Let's call it iOSTest. Different files will appear in the project folder. The main thing here will be the Main.cs file. It is this file that will run your application. Its structure and role is very simple. He will launch AppDelegate.cs, which is right next to Main.cs.
AppDelegate.cs creates the main window and transfers control further. To do this, its overloaded FinishedLaunching function should be rewritten as follows:
In addition, you should change the class from which AppDelegate is inherited, i.e. The class declaration should look like this:
Leave the rest as is.
For all this to work, you must add the Setup.cs file to the project.
The code is something like this:
The main thing here is CreateApp (). Now the studio is showing an error, because App () does not exist yet.
Now we need to create a new project in the solution, where ViewModels will lie in accordance with the MVVM paradigm. This project should be of the “portable” type or .NETStandard, but so far for MvvmCross it is only a PCL type project. If you have a “draft” of that project, then you can simply copy it to the 2017th studio. He will work without problems. We call it “Core”, connect via NuGet MvvmCross and create the App.cs file in its root. We will connect the new project with the first as a link and the error in Setup.cs will disappear.
App.cs content is very primitive:
In the line RegisterAppStart (); we finally get to the first real working code. As it’s not hard to guess, “StartViewModel” is launched here. All the previous ones were preparations for launching. Create the ViewModels folder in the Core project and the StartViewModel.cs file in it:
This ViewModel can only do one thing: launch the MainViewModel. Here it should be noted that such a laying seems redundant, why not immediately launch MainViewModel? Until some version of MvvmCross it was, but now something has changed and we will not see the menu if you start MainViewModel right away. Perhaps this is a bug and it will be fixed. But if you don’t use the intermediate class now, then the menu will be on the right, and not on the left, as in its settings, you can see it only by pulling the edge of the screen, there will be no button to call up the menu (I suspect that it is outside the screen on the right).
MainViewModel.cs:
Everything is simple here. The model is essentially empty, the only thing it can do is show the menu. In a real project, there can be many different functions as needed.
Next to MainViewModel.cs, create MenuViewModel.cs to describe the behavior of the menu:
As you can see, the menu will have only two lines.
Now create a model for the menu. Those. template for strings. In the Core project, the Models / MenuModel.cs file:
It describes what the individual menu lines are. The text for the line and the command to execute.
To move from the menu items, we will create two more almost identical ViewModels. The difference will only be in the name. AboutViewModel.cs and SettingsViewModel.cs
In real code, you should place functions that will be executed when accessing this ViewModel.
Now we return to the iOSTest project and begin to engage in the magic of MvvmCross. The fact is that the connections between the View and ViewModel occur somewhere inside MvvmCross and are not explicitly presented. The code calls ViewModel, and we see View, while there is no indication of which View to show in the code.
As I understand it, each View is associated with its ViewModel exclusively through the name. Feedback just exists in the View declaration. In general, everything is somehow ambiguous.
Create the Views folder in the iOSTest project and in it the views for all ViewModels: MainView, MenuView, AboutView, SettingsView. Unfortunately, VisualStudio is not the best way to create a View. You should use Xamarin iOS Designer or Xcode Interface Builder. Details can be read here: link This will allow you to create beautiful views.
But we will go this way: Create the files MainView.cs, MenuView.sc, AboutView.cs, SettingsView.cs as ordinary classes and describe the representation in them programmatically. It will not be very beautiful, but simple.
Pay attention to the attributes before declaring the class. This is the menu binding to this View.
AboutView.cs (SettingsView similar)
Here the attributes look a little different. “PushPanel” instead of “ResetRoot” causes the “<Back” button to appear instead of the menu call button, which will return you to the previous window. That is, if you want to have a menu button in all windows, then write "ResetRoot"
And, finally, MenuView.cs:
The attributes again changed slightly, it is clear that the menu will be on the left. The menu icon lies in the “menu.png” file, which, according to all iOS rules, should be located in the Resources folder, preferably in three resolutions (sizes).
If you abandon the implementation of the IMvxSidebarMenu interface, the code can be reduced almost twice, but there will be no menu icon. There will be an inscription “Menu”.
That's all.
The whole project can be viewed here: github
It should be noted immediately that if you do not have real hardware from Apple, then you can not do anything, because assembly and debugging of projects is possible only on real Apple devices. Although you can work under Windows. Those. about how to run Android applications via USB debugging. This is the policy of this office.
All code examples are maximally simplified and probably in some places are not quite ideologically correct, but they work. So for example, it is better to create a menu in the form of a table and connect a data source to it.
We connect MvvmCross: Through “Managing NuGet packages” we put MvvmCross, MvvmCross.iOS.Support, MvvmCross.iOS.Support.XamarinSidebar, SidebarNavigation. The set of packages depends on the version, here about MvvmCross 5.12.
So, first of all, you need to create a new project like "iOS-Universal-empty application." Let's call it iOSTest. Different files will appear in the project folder. The main thing here will be the Main.cs file. It is this file that will run your application. Its structure and role is very simple. He will launch AppDelegate.cs, which is right next to Main.cs.
AppDelegate.cs creates the main window and transfers control further. To do this, its overloaded FinishedLaunching function should be rewritten as follows:
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();
startup.Start();
Window.MakeKeyAndVisible();
return true;
}
In addition, you should change the class from which AppDelegate is inherited, i.e. The class declaration should look like this:
public class AppDelegate : MvxApplicationDelegate
Leave the rest as is.
For all this to work, you must add the Setup.cs file to the project.
The code is something like this:
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);
}
}
}
The main thing here is CreateApp (). Now the studio is showing an error, because App () does not exist yet.
Now we need to create a new project in the solution, where ViewModels will lie in accordance with the MVVM paradigm. This project should be of the “portable” type or .NETStandard, but so far for MvvmCross it is only a PCL type project. If you have a “draft” of that project, then you can simply copy it to the 2017th studio. He will work without problems. We call it “Core”, connect via NuGet MvvmCross and create the App.cs file in its root. We will connect the new project with the first as a link and the error in Setup.cs will disappear.
App.cs content is very primitive:
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();
}
}
}
In the line RegisterAppStart (); we finally get to the first real working code. As it’s not hard to guess, “StartViewModel” is launched here. All the previous ones were preparations for launching. Create the ViewModels folder in the Core project and the StartViewModel.cs file in it:
StartViewModel.cs
using MvvmCross.Core.ViewModels;
namespace Core.ViewModels
{
public class StartViewModel: MvxViewModel
{
public void ShowMainView()
{
ShowViewModel();
}
}
}
This ViewModel can only do one thing: launch the MainViewModel. Here it should be noted that such a laying seems redundant, why not immediately launch MainViewModel? Until some version of MvvmCross it was, but now something has changed and we will not see the menu if you start MainViewModel right away. Perhaps this is a bug and it will be fixed. But if you don’t use the intermediate class now, then the menu will be on the right, and not on the left, as in its settings, you can see it only by pulling the edge of the screen, there will be no button to call up the menu (I suspect that it is outside the screen on the right).
MainViewModel.cs:
MainViewModel.cs
using MvvmCross.Core.ViewModels;
namespace Core.ViewModels
{
public class MainViewModel : MvxViewModel
{
public void ShowMenu()
{
ShowViewModel();
}
}
}
Everything is simple here. The model is essentially empty, the only thing it can do is show the menu. In a real project, there can be many different functions as needed.
Next to MainViewModel.cs, create MenuViewModel.cs to describe the behavior of the menu:
MenuViewModel.cs
using System;
using System.Collections.Generic;
using Core.Models;
using MvvmCross.Core.ViewModels;
namespace Core.ViewModels
{
public class MenuViewModel : MvxViewModel
{
public List MenuItems
{
get;
}
public MenuViewModel()
{
MenuItems = new List
{
new MenuModel {Title = "Settings", Navigate = NavigateCommandSettings},
new MenuModel {Title = "About", Navigate = NavigateCommandAbout}
};
}
private MvxCommand _navigateCommandSettings;
public MvxCommand NavigateCommandSettings
{
get
{
_navigateCommandSettings = _navigateCommandSettings ?? new MvxCommand((vm) =>
{
ShowViewModel();
});
return _navigateCommandSettings;
}
}
private MvxCommand _navigateCommandAbout;
public MvxCommand NavigateCommandAbout
{
get
{
_navigateCommandAbout = _navigateCommandAbout ?? new MvxCommand((vm) =>
{
ShowViewModel();
});
return _navigateCommandAbout;
}
}
}
}
As you can see, the menu will have only two lines.
Now create a model for the menu. Those. template for strings. In the Core project, the Models / MenuModel.cs file:
MenuModel.cs
using System;
using MvvmCross.Core.ViewModels;
namespace Core.Models
{
public class MenuModel
{
public String Title {
get;
set;
}
public MvxCommand Navigate {
get;
set;
}
}
}
It describes what the individual menu lines are. The text for the line and the command to execute.
To move from the menu items, we will create two more almost identical ViewModels. The difference will only be in the name. AboutViewModel.cs and SettingsViewModel.cs
AboutViewModel.cs
using MvvmCross.Core.ViewModels;
namespace Core.ViewModels
{
public class AboutViewModel : MvxViewModel
{
}
}
In real code, you should place functions that will be executed when accessing this ViewModel.
Now we return to the iOSTest project and begin to engage in the magic of MvvmCross. The fact is that the connections between the View and ViewModel occur somewhere inside MvvmCross and are not explicitly presented. The code calls ViewModel, and we see View, while there is no indication of which View to show in the code.
As I understand it, each View is associated with its ViewModel exclusively through the name. Feedback just exists in the View declaration. In general, everything is somehow ambiguous.
Create the Views folder in the iOSTest project and in it the views for all ViewModels: MainView, MenuView, AboutView, SettingsView. Unfortunately, VisualStudio is not the best way to create a View. You should use Xamarin iOS Designer or Xcode Interface Builder. Details can be read here: link This will allow you to create beautiful views.
But we will go this way: Create the files MainView.cs, MenuView.sc, AboutView.cs, SettingsView.cs as ordinary classes and describe the representation in them programmatically. It will not be very beautiful, but simple.
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
{
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);
}
}
}
Pay attention to the attributes before declaring the class. This is the menu binding to this View.
AboutView.cs (SettingsView similar)
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
{
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);
}
}
}
Here the attributes look a little different. “PushPanel” instead of “ResetRoot” causes the “<Back” button to appear instead of the menu call button, which will return you to the previous window. That is, if you want to have a menu button in all windows, then write "ResetRoot"
And, finally, 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, 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().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;
}
}
The attributes again changed slightly, it is clear that the menu will be on the left. The menu icon lies in the “menu.png” file, which, according to all iOS rules, should be located in the Resources folder, preferably in three resolutions (sizes).
If you abandon the implementation of the IMvxSidebarMenu interface, the code can be reduced almost twice, but there will be no menu icon. There will be an inscription “Menu”.
That's all.
The whole project can be viewed here: github