先日、MvvmCrossが6.0にメジャーバージョンアップしました!
.NET Standard 2.0対応や、NuGetパッケージが統合されたりと、色々新しくなっています。
本記事では、新しくなったMvvmCrossを使ってアプリ開発を始めるための、セットアップ方法を紹介します。
できたものはこちらに置いてあります。
追記
もっと簡単な方法を書きました。
MvvmCrossで簡単にアプリ開発を始める準備
プロジェクトの作成
共通ロジックのプロジェクトは、.NET Standard 2.0で作成します。
今回は、プロジェクト名を「MvxStarterApp.Core」、ソリューション名を「MvxStarterApp」にします。
次に、Android、iOSのプロジェクトをソリューションに追加します。
Androidは、テンプレートは[Android アプリ]を選択して、アプリ名を「MvxStarterApp」、プロジェクト名を「MvxStarterApp.Droid」にします。
iOSは、テンプレートは[単一ビューアプリ]を選択して、アプリ名を「MvxStarterApp」、プロジェクト名を「MvxStarterApp.iOS」にします。
参照の追加
プロジェクトが作成できたら、Android、iOSの両プロジェクトに、MvxStarterApp.Coreの参照を追加します。
さらに、Androidには、Mono.Android.Exportも追加します。
MvvmCrossのインストール
NuGetを使って、MvvmCrossをインストールします。
「MvvmCross」と検索をかけて、出てきた[MvvmCross]を選択して、インストールします。これを全てのプロジェクトに対して行います。
以前のバージョンでは、「MvvmCross.StarterPack」というパッケージをインストールすれば、必要なファイルも含めてインストールすることができたのですが、6.0からは使えなくなってしまいました。なので、必要なファイルは、手動で追加していきます。
Coreプロジェクトのセットアップ
ファイルの追加
Coreプロジェクトには、ここにある以下のファイルを追加します。
- App.cs.pp
- MainViewModel.cs.pp
App.cs.ppは、App.csにリネームして、ルートに追加します。ソース内の$rootnamespace$
は、MvxStarterApp.Core
に置き換えます。
using MvvmCross.IoC;
namespace MvxStarterApp.Core
{
public class App : MvvmCross.ViewModels.MvxApplication
{
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
RegisterAppStart<ViewModels.MainViewModel>();
}
}
}
MainViewModel.cs.ppは、ViewModelsフォルダを作って、その中にMainViewModel.csにリネームして追加します。こちらも、ソース内の$rootnamespace$
は、MvxStarterApp.Core
に置き換えます。さらに、using MvvmCross.Commands;
の追加も行います。
using MvvmCross.Commands; //追加
using MvvmCross.ViewModels;
using System.Threading.Tasks;
namespace MvxStarterApp.Core.ViewModels
{
public class MainViewModel : MvxViewModel
{
public MainViewModel()
{
}
public override Task Initialize()
{
//TODO: Add starting logic here
return base.Initialize();
}
public IMvxCommand ResetTextCommand => new MvxCommand(ResetText);
private void ResetText()
{
Text = "Hello MvvmCross";
}
private string _text = "Hello MvvmCross";
public string Text
{
get { return _text; }
set { SetProperty(ref _text, value); }
}
}
}
注意点として、必ずリネームをした後にファイルの追加を行ってください。そうしないと、謎のビルドエラーに悩まされることになりま。
不要ファイルの削除
自動で作られるClass1.csは使わないので、削除しておきます。
Androidプロジェクトのセットアップ
ファイルの追加
Androidプロジェクトには、ここにある以下のファイルを追加します。
- LinkerPleaseInclude.cs
- MainView.axml
- MainView.cs.pp
- SplashScreen.axml
- SplashScreen.cs.pp
- SplashStyle.xml
- splash.png
LinkerPleaseInclude.csは、ルートに追加します。ソース内の$YourNameSpace$
は、MvxStarterApp.Droid
に置き換えます。
using Android.App;
using Android.Views;
using Android.Widget;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using System;
using System.Collections.Specialized;
using System.Windows.Input;
namespace MvxStarterApp.Droid
{
// This class is never actually executed, but when Xamarin linking is enabled it does how to ensure types and properties
// are preserved in the deployed app
[Android.Runtime.Preserve(AllMembers = true)]
public class LinkerPleaseInclude
{
public void Include(Button button)
{
button.Click += (s, e) => button.Text = button.Text + "";
}
public void Include(CheckBox checkBox)
{
checkBox.CheckedChange += (sender, args) => checkBox.Checked = !checkBox.Checked;
}
public void Include(Switch @switch)
{
@switch.CheckedChange += (sender, args) => @switch.Checked = !@switch.Checked;
}
public void Include(View view)
{
view.Click += (s, e) => view.ContentDescription = view.ContentDescription + "";
}
public void Include(TextView text)
{
text.AfterTextChanged += (sender, args) => text.Text = "" + text.Text;
text.Hint = "" + text.Hint;
}
public void Include(CheckedTextView text)
{
text.AfterTextChanged += (sender, args) => text.Text = "" + text.Text;
text.Hint = "" + text.Hint;
}
public void Include(CompoundButton cb)
{
cb.CheckedChange += (sender, args) => cb.Checked = !cb.Checked;
}
public void Include(SeekBar sb)
{
sb.ProgressChanged += (sender, args) => sb.Progress = sb.Progress + 1;
}
public void Include(RadioGroup radioGroup)
{
radioGroup.CheckedChange += (sender, args) => radioGroup.Check(args.CheckedId);
}
public void Include(RadioButton radioButton)
{
radioButton.CheckedChange += (sender, args) => radioButton.Checked = args.IsChecked;
}
public void Include(RatingBar ratingBar)
{
ratingBar.RatingBarChange += (sender, args) => ratingBar.Rating = 0 + ratingBar.Rating;
}
public void Include(Activity act)
{
act.Title = act.Title + "";
}
public void Include(INotifyCollectionChanged changed)
{
changed.CollectionChanged += (s, e) => { var test = $"{e.Action}{e.NewItems}{e.NewStartingIndex}{e.OldItems}{e.OldStartingIndex}"; };
}
public void Include(ICommand command)
{
command.CanExecuteChanged += (s, e) => { if (command.CanExecute(null)) command.Execute(null); };
}
public void Include(MvvmCross.IoC.MvxPropertyInjector injector)
{
injector = new MvvmCross.IoC.MvxPropertyInjector();
}
public void Include(System.ComponentModel.INotifyPropertyChanged changed)
{
changed.PropertyChanged += (sender, e) =>
{
var test = e.PropertyName;
};
}
public void Include(MvxTaskBasedBindingContext context)
{
context.Dispose();
var context2 = new MvxTaskBasedBindingContext();
context2.Dispose();
}
public void Include(MvxNavigationService service, IMvxViewModelLoader loader)
{
service = new MvxNavigationService(null, loader);
}
public void Include(ConsoleColor color)
{
Console.Write("");
Console.WriteLine("");
color = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.ForegroundColor = ConsoleColor.Magenta;
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
Console.ForegroundColor = ConsoleColor.DarkGray;
}
}
}
SplashScreen.cs.ppは、SplashScreen.csにリネームして、こちらもルートに追加します。ソース内の$rootnamespace$
は、同じくMvxStarterApp.Droid
に置き換えます。
using Android.App;
using Android.Content.PM;
using MvvmCross.Platforms.Android.Views;
namespace MvxStartApp.Droid
{
[Activity(
Label = "MvxStarterApp.Droid"
, MainLauncher = true
, Theme = "@style/Theme.Splash"
, NoHistory = true
, ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashScreen : MvxSplashScreenActivity
{
public SplashScreen()
: base(Resource.Layout.SplashScreen)
{
}
}
}
MainView.cs.ppは、Viewsフォルダを作って、その中にMainView.csにリネームして追加します。こちらも$rootnamespace$
は、MvxStarterApp.Droid
に置き換えます。
using Android.App;
using Android.OS;
using MvvmCross.Platforms.Android.Views;
namespace MvxStartApp.Droid
{
[Activity(Label = "View for MainViewModel")]
public class MainView : MvxActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.MainView);
}
}
}
残りのファイルも追加していきます。
MainView.axml、SplashScreen.axmlは、Resources/layoutフォルダに、SplashStyle.xmlは、Resources/valuesフォルダに、splash.pngは、Resources/drawableフォルダにそれぞれ追加します。
不要ファイルの削除
自動で作成されるMainActivity.cs、Resources/layout/Main.axmlを削除します。
MainApplicationの作成
MainApplicationクラスをルートに作成します。
using System;
using MvvmCross.Platforms.Android.Views;
using Android.Runtime;
using Android.App;
using MvvmCross.Platforms.Android.Core;
namespace MvxStartApp.Droid
{
[Application]
public class MainApplication : MvxAndroidApplication<MvxAndroidSetup<Core.App>, Core.App>
{
public MainApplication(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
}
}
}
以前のバージョンでは、初期化処理のために、Setupクラスを作成する必要がありましたが、6.0からは、MvxAndroidApplicationを継承したApplicationクラスを作成するように変更になりました。
iOSプロジェクトのセットアップ
ファイルの追加
iOSプロジェクトには、ここにある以下のファイルを追加します。
- LinkerPleaseInclude.cs
- MainView.cs.pp
- MainView.designer.cs.pp
- MainView.xib
LinkerPleaseInclude.csは、ルートに追加し、以下の修正を行います。
-
$YourNameSpace$
をMvxStarterApp.iOS
に置き換え - 41行目に
textField.EditingDidEnd += (sender, args) => { textField.Text = ""; };
を追加 -
Include (MvvmCross.Plugin.Json.Plugin plugin)
メソッドをコメントアウト
using Foundation;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Navigation;
using MvvmCross.Platforms.Ios.Views;
using MvvmCross.ViewModels;
using System;
using System.Collections.Specialized;
using System.Windows.Input;
using UIKit;
namespace MvxStarterApp.iOS
{
// This class is never actually executed, but when Xamarin linking is enabled it does ensure types and properties
// are preserved in the deployed app
[Foundation.Preserve(AllMembers = true)]
public class LinkerPleaseInclude
{
public void Include(MvxTaskBasedBindingContext c)
{
c.Dispose();
var c2 = new MvxTaskBasedBindingContext();
c2.Dispose();
}
public void Include(UIButton uiButton)
{
uiButton.TouchUpInside += (s, e) =>
uiButton.SetTitle(uiButton.Title(UIControlState.Normal), UIControlState.Normal);
}
public void Include(UIBarButtonItem barButton)
{
barButton.Clicked += (s, e) =>
barButton.Title = barButton.Title + "";
}
public void Include(UITextField textField)
{
textField.Text = textField.Text + "";
textField.EditingChanged += (sender, args) => { textField.Text = ""; };
textField.EditingDidEnd += (sender, args) => { textField.Text = ""; }; //これを追加
}
public void Include(UITextView textView)
{
textView.Text = textView.Text + "";
textView.TextStorage.DidProcessEditing += (sender, e) => textView.Text = "";
textView.Changed += (sender, args) => { textView.Text = ""; };
}
public void Include(UILabel label)
{
label.Text = label.Text + "";
label.AttributedText = new NSAttributedString(label.AttributedText.ToString() + "");
}
public void Include(UIImageView imageView)
{
imageView.Image = new UIImage(imageView.Image.CGImage);
}
public void Include(UIDatePicker date)
{
date.Date = date.Date.AddSeconds(1);
date.ValueChanged += (sender, args) => { date.Date = NSDate.DistantFuture; };
}
public void Include(UISlider slider)
{
slider.Value = slider.Value + 1;
slider.ValueChanged += (sender, args) => { slider.Value = 1; };
}
public void Include(UIProgressView progress)
{
progress.Progress = progress.Progress + 1;
}
public void Include(UISwitch sw)
{
sw.On = !sw.On;
sw.ValueChanged += (sender, args) => { sw.On = false; };
}
public void Include(MvxViewController vc)
{
vc.Title = vc.Title + "";
}
public void Include(UIStepper s)
{
s.Value = s.Value + 1;
s.ValueChanged += (sender, args) => { s.Value = 0; };
}
public void Include(UIPageControl s)
{
s.Pages = s.Pages + 1;
s.ValueChanged += (sender, args) => { s.Pages = 0; };
}
public void Include(INotifyCollectionChanged changed)
{
changed.CollectionChanged += (s, e) => { var test = $"{e.Action}{e.NewItems}{e.NewStartingIndex}{e.OldItems}{e.OldStartingIndex}"; };
}
public void Include(ICommand command)
{
command.CanExecuteChanged += (s, e) => { if (command.CanExecute(null)) command.Execute(null); };
}
public void Include(MvvmCross.IoC.MvxPropertyInjector injector)
{
injector = new MvvmCross.IoC.MvxPropertyInjector();
}
public void Include(System.ComponentModel.INotifyPropertyChanged changed)
{
changed.PropertyChanged += (sender, e) => { var test = e.PropertyName; };
}
public void Include(MvxNavigationService service, IMvxViewModelLoader loader)
{
service = new MvxNavigationService(null, loader);
}
public void Include(ConsoleColor color)
{
Console.Write("");
Console.WriteLine("");
color = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.ForegroundColor = ConsoleColor.Magenta;
Console.ForegroundColor = ConsoleColor.White;
Console.ForegroundColor = ConsoleColor.Gray;
Console.ForegroundColor = ConsoleColor.DarkGray;
}
//public void Include(MvvmCross.Plugin.Json.Plugin plugin)
//{
// plugin.Load();
//}
}
}
Androidでも追加したLinkerPleaseIncludeですが、どこからも呼ばれていないし、一見意味のないクラスのように見えます。このクラスは、バインディングで使用するプロパティが削除されてしまうのを防ぐ為のものです。Xamarinには、アプリサイズ削減のため、使われていないプロパティやメソッドを削除する機能があり、AndroidでReleaseビルドを行なったり、iOSで実機で動かしたりすると、その機能が有効になり、他に使われていないなら、バインドするプロパティに指定していても、削除されてしまいます。それを防ぐために適当にプロパティを使っているこのクラスを追加しているのです。
MainView.cs.ppとMainView.designer.cs.ppは、Viewsフォルダを作って、その中にそれぞれMainView.cs、MainView.designer.csにリネームして追加します。そして、ソース内の$rootnamespace$
をMvxStarterApp.iOS
に置き換えます。
MainView.xibもViewsフォルダに追加します。
using MvvmCross.Binding.BindingContext;
using MvvmCross.Platforms.Ios.Presenters.Attributes;
using MvvmCross.Platforms.Ios.Views;
namespace MvxStarterApp.iOS.Views
{
[MvxRootPresentation(WrapInNavigationController = true)]
public partial class MainView : MvxViewController
{
public MainView() : base("MainView", null)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var set = this.CreateBindingSet<MainView, Core.ViewModels.MainViewModel>();
set.Bind(TextField).To(vm => vm.Text);
set.Bind(Button).To(vm => vm.ResetTextCommand);
set.Apply();
}
}
}
// WARNING
//
// This file has been generated automatically by Visual Studio from the outlets and
// actions declared in your storyboard file.
// Manual changes to this file will not be maintained.
//
using Foundation;
using System;
using System.CodeDom.Compiler;
using UIKit;
namespace MvxStarterApp.iOS.Views
{
[Register ("MainView")]
partial class MainView
{
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIButton Button { get; set; }
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UITextField TextField { get; set; }
void ReleaseDesignerOutlets ()
{
if (Button != null) {
Button.Dispose ();
Button = null;
}
if (TextField != null) {
TextField.Dispose ();
TextField = null;
}
}
}
}
不要ファイルの削除
自動で作成されるMain.storyboard、ViewController.cs、ViewController.designer.csを削除します。
AppDelegateの修正
AppDelegateを以下のように修正します。
using Foundation;
using UIKit;
using MvvmCross.Platforms.Ios.Core;
namespace MvxStarterApp.iOS
{
[Register("AppDelegate")]
public class AppDelegate : MvxApplicationDelegate<MvxIosSetup<Core.App>, Core.App>
{
}
}
iOSも以前は、MvxIosSetupを継承したSetupクラスを作る必要がありましたが、デフォルトのままななら、AppDelegateの修正だけで済むようになりました。
info.plistの修正
info.plistを修正します。「メインインターフェース」に「Main」と入っていますが、ここを空にしておきます。
実行
以上でセットアップは完了です。実際に動かしてみましょう。
始めるまでちょっと面倒ですが、MvvmCrossは使いこなせればとても強力なフレームワークなので、ぜひ使ってみてはいかがでしょうか。