Xamarin .Net Standard

Xamarin.Forms mit .Net Standard 2.0

Versionsgeschichte
Version Aktualisierungsdatum Änderungen
0.2 08.12.2017 VS 2017 15.5 Anpassungen
0.1 03.11.2017 (Erste Fassung)

Mit Xamarin.Forms 2.4 ist Xamarin nun .Net Core kompatibel (sogar zu Version 2.0).

Gehen Sie nun Schritt für Schritt von der Standard-Vorlage für Xamarin.Forms Projekte bis zu einer funktionierenden Umsetzung mit .Net Standard 2.0 und Prism als Navigationshilfe und Dependency Injection Container.

1. Schritt - Neues Projekt

Legen Sie als erstes ein neuen Xamarin.Forms Projekt aus der Visual Studio Vorlage. Aktualisieren Sie alle Bibliotheken auf den aktuellsten Stand. Zum Zeitpunkt des Schreibens sind es folgende:

  • Xamarin.Forms in Version 2.5.0.121934
  • Microsoft.NetCore.UniversalWindowsPlatform in Version 6.0.4
  • Xamarin.Android.Support.XXX in Version 26.1.0.1
  • Prism.Unity.Forms in Version 7.0.0.336-pre

Prüfen Sie, dass die Apps noch alle laufen, bevor wir mit dem nächsten Schritt fortfahren.

Seit VS 2017 15.5.1 erlaubt die Projektvorlage für Xamarin.Forms Apps die direkte Anlage eines Projektes, das .Net Standard 2.0 als gemeinsames Projekt nutzt. Auch die OS-Projekte nutzen nun Nuget-Reference-Abhängigkeiten, statt der packages.config-Datei. Das macht die Erstellung neuer projekte und die Aktualisierung der NuGet-Pakete deutlich schneller und einfacher.

2. Schritt - .Net Standard Projekte

Legen wir nun mehrere .Net Standard Projekte an, da dies eher der realen Struktur entspricht, statt alles in ein Projekt zu stecken.

  1. XamarinWithEntityFramework.Core: .Net Standard 2.0 Projekt für Service-Interfaces (wie Repositories) und Entities.
  2. XamarinWithEntityFramework.Repos: .Net Standard 2.0 Projekt mit Entity Framework Core 2.0 Implementierung der Repositories.
  3. XamarinWithEntityFramework.UI: .Net Standard 2.0 Projekt, unser Xamarin.Forms Basisprojekt mit Views und ViewModels.

 

Einsatz von Entity Framework Core 2.0 unter Xamarin.Forms wird in einen späteren Tutorial beschrieben.

3. Schritt - Prism.Forms installieren

Für Navigation und bessere Dependency Injection, werden wir die aktuelle Version (zum Zeitpunkt des Schreibens ist es noch eine Prerelease-Version, die .Net Standard unterstützt, 7.0.0.336-pre) von Prism.Unity.Forms installieren. Die Bibliothek wird in den UI und allen App-Projekten installiert.

Nun passen wir das UI und App-Projekte an, damit diese Prism nutzen können.

UI

App.xaml.cs

using Prism;
using Prism.Ioc;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace XamarinFormsNetStandard.UI
{
    public partial class XfNetStandardApp
	{
		public XfNetStandardApp (IPlatformInitializer initializer) : base(initializer)
		{
		}

        protected override void OnInitialized()
        {
            InitializeComponent();
            
            NavigationService.NavigateAsync(nameof(Views.MainPage));
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            // Register services

            // Register for navigation
            containerRegistry.RegisterForNavigation<Views.MainPage>();
        }

        protected override void OnStart ()
		{
			// Handle when your app starts
		}

		protected override void OnSleep ()
		{
			// Handle when your app sleeps
		}

		protected override void OnResume ()
		{
			// Handle when your app resumes
		}
    }
}

App.xaml

<?xml version="1.0" encoding="utf-8" ?>
<prism:PrismApplication xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Unity;assembly=Prism.Unity.Forms"
             x:Class="XamarinFormsNetStandard.UI.XfNetStandardApp">
    <Application.Resources>

        <!-- Application resource dictionary -->

    </Application.Resources>
</prism:PrismApplication>

Android

MainActivity.cs

using Android.App;
using Android.Content.PM;
using Android.OS;

namespace XamarinFormsNetStandard.Droid
{
    [Activity(Label = "XamarinFormsNetStandard", Icon = "@drawable/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(bundle);

            global::Xamarin.Forms.Forms.Init(this, bundle);
            LoadApplication(new UI.XfNetStandardApp(new AndroidInitializer()));
        }
    }
}

AndroidInitializer.cs

using Prism;
using Prism.Ioc;

namespace XamarinFormsNetStandard.Droid
{
    class AndroidInitializer : IPlatformInitializer
    {
        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            // Register OS spezific services here
        }
    }
}

iOS

AppDelegate.cs

using Foundation;
using UIKit;

namespace XamarinFormsNetStandard.iOS
{
    // The UIApplicationDelegate for the application. This class is responsible for launching the 
    // User Interface of the application, as well as listening (and optionally responding) to 
    // application events from iOS.
    [Register("AppDelegate")]
    public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
    {
        //
        // This method is invoked when the application has loaded and is ready to run. In this 
        // method you should instantiate the window, load the UI into it and then make the window
        // visible.
        //
        // You have 17 seconds to return from this method, or iOS will terminate your application.
        //
        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new UI.XfNetStandardApp(new IosInitializer()));

            return base.FinishedLaunching(app, options);
        }
    }
}

IosInitializer.cs

using Prism;
using Prism.Ioc;

namespace XamarinFormsNetStandard.iOS
{
    class IosInitializer : IPlatformInitializer
    {
        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            // Register OS spezific services here
        }
    }
}

UWP / UAP

MainPage.cs

namespace XamarinFormsNetStandard.UWP
{
    public sealed partial class MainPage
    {
        public MainPage()
        {
            InitializeComponent();

            LoadApplication(new UI.XfNetStandardApp(new UwpInitializer()));
        }
    }
}

UwpInitializer.cs

using Prism;
using Prism.Ioc;

namespace XamarinFormsNetStandard.UWP
{
    class UwpInitializer : IPlatformInitializer
    {
        public void RegisterTypes(IContainerRegistry containerRegistry)
        {
            // Register OS spezific services here
        }
    }
}

View und ViewModel

Verschieben Sie nun MainPage in den Ordner Views und passen Sie den Namespace an. Das ist notwendig, da Prism die Seite im Namespace .Views und ViewModels in Namespace .ViewModels erwartet. Dieses Verhalten kann aber über einen eigenen Resolver angepasst werden.

Das machen wir jetzt auch. Standardverhalten ist, dass Prism ein ViewModel im Namespace ViewModels sucht und bei dem Klassennamen ViewModel angehängt. Für das View Views.MainPage sucht Prism also ein ViewModels.MainPageViewModel. Mit unserem Resolver werden wir Page weglassen, so das das ViewModel dann ViewModels.MainViewModel heißen muss.

Dazu legen wir eine statische Klasse, die die Methode zur Auflösung einer Page zu einem ViewModel übernimmt.

ViewModelResolver.cs

using System;
using System.Reflection;

namespace XamarinFormsNetStandard.UI
{
    public static class ViewModelResolver
    {
        public static Type PageToViewModel(Type viewType)
        {
            var viewName = viewType.FullName;
            viewName = viewName.Replace(".Views.", ".ViewModels.");
            viewName = viewName.EndsWith("Page", StringComparison.Ordinal)
                ? viewName.Substring(0, viewName.LastIndexOf("Page", StringComparison.Ordinal)) + "View"
                : viewName;
            var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
            var suffix = viewName.EndsWith("View", StringComparison.Ordinal)
                ? "Model"
                : "ViewModel";
            var viewModelName = $"{viewName}{suffix}, {viewAssemblyName}";
            return Type.GetType(viewModelName);
        }

    }
}

MainPage.cs

using Xamarin.Forms;

namespace XamarinFormsNetStandard.UI.Views
{
    public partial class MainPage : ContentPage
	{
		public MainPage()
		{
			InitializeComponent();
		}
	}
}

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamarinFormsNetStandard.UI.Views.MainPage">

    <StackLayout>
        <Label Text="{Binding Title}"
                VerticalOptions="CenterAndExpand" 
                HorizontalOptions="CenterAndExpand" />
    </StackLayout>

</ContentPage>

Legen Sie eine neue Klasse MainViewModel im Ordner ViewModels ab.

MainViewModel.cs

using Prism.Mvvm;
using XamarinFormsNetStandard.UI.Views;

namespace XamarinFormsNetStandard.UI.ViewModels
{
    public class MainViewModel : BindableBase
    {
        private string _title = "Hallo from Prism.Forms!";
        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }
    }
}

Nun sollte Prism mit automatischen Zuordnung des ViewModels funktionieren. Für die Navigation fehlt noch eine zweite Seite mit einem ViewModel.

5. Schritt - Navigation mit Prism

Legen wir eine neue Page und ein neues ViewModel an.

SecondViewModel.cs

using Prism.Mvvm;

namespace XamarinFormsNetStandard.UI.ViewModels
{
    public class SecondViewModel : BindableBase
    {
        public string Title { get; set; } = "Second Page";
    }
}

SecondPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamarinFormsNetStandard.UI.Views.SecondPage">
    <ContentPage.Content>
        <StackLayout>
            <Label Text="{Binding Title}"
                VerticalOptions="CenterAndExpand" 
                HorizontalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Für die Navigation bekommt das erste ViewModel das INavigationService als Constructor-Injection.

Vorsicht, der Parameter muss wirklich EXACT navigationService heißen, sonst funktioniert es nicht. Diese Merkwürdigkeit ist Xamarin.Forms geschuldet und muss aktuell so hingenommen werden.

Dazu kommt noch ein Command-Property, der zur zweiten Seite durch den Service navigiert.

MainPageViewModel.cs

using Prism.Commands;
using Prism.Mvvm;
using Prism.Navigation;
using System.Threading.Tasks;
using System.Windows.Input;
using XamarinFormsNetStandard.UI.Views;

namespace XamarinFormsNetStandard.UI.ViewModels
{
    public class MainViewModel : BindableBase
    {
        private readonly INavigationService _navService;

        public MainViewModel(INavigationService navigationService)
        {
            _navService = navigationService;

            // Commands
            NextPageCommand = new DelegateCommand(async () => await NextPage());
        }

        private string _title = "Hallo from Prism.Forms!";
        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }

        public ICommand NextPageCommand { get; }
        private async Task NextPage()
        {
            await _navService.NavigateAsync(nameof(SecondPage));
        }
    }
}

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XamarinFormsNetStandard.UI.Views.MainPage">

    <StackLayout>
        <Label Text="{Binding Title}"
                VerticalOptions="CenterAndExpand" 
                HorizontalOptions="CenterAndExpand" />

        <Button Text="Next Page" Command="{Binding NextPageCommand}" />
    </StackLayout>

</ContentPage>

Dazu noch die Registrierung der neuen Navigationsseite, und fertig ist die Navigation über den Prism-Navigationsservice, der Pages mit ViewModels verbindet und Abhängigkeiten automatisch auflöst.

App.xaml.cs

protected override void OnInitialized()
{
    InitializeComponent();

    // Register View Model resolver
    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(ViewModelResolver
            .PageToViewModel);

    // Navigate to first page
    ToEntryPoint();
}

private void ToEntryPoint()
{
    NavigationService.NavigateAsync($"{nameof(NavigationPage)}/{nameof(Views.MainPage)}")
        .ContinueWith(t => Debug.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted); // Nur für Log im Fehlerfall
}

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // Register services

    // Register for navigation
    containerRegistry.RegisterForNavigation<NavigationPage>();
    containerRegistry.RegisterForNavigation<Views.MainPage>();
    containerRegistry.RegisterForNavigation<Views.SecondPage>();
}