Xamarin.Formsでの共有部分をPCLから.NET Standardに変える

去年の6月の時点では、.NET Standard化されていなかったので、第6章の「共通ロジックを作成する」で PCL を使っていたのだけど、プロジェクト作成時に「共有プロジェクト」と「.NET Standard」になっているので、そのあたりのサンプルを書き替る。

Xamarinプログラミング入門 C#によるiOS、Androidアプリケーション開発の基本
https://www.amazon.co.jp/dp/4822253503/

コード共有方法で「.NET Standard」を選ぶ

「共有プロジェクト」のほうは、従来通りのコードを共有する方式なので #if を使って iOS/Android/UWP と書き分けられる。これでも、まあ十分なのだけど、ある程度コードが多くなってくるとロジック部分を別途テストしたいときがあるので、ライブラリ化しておくほうが良かったりする。

でもって、PCL(Portable Class Library)で作るときと .NET Standard で作るときとどう違うのかというと端的に言えば、PCLの場合は最小公約数になていて、.NET Standard の場合が最大公約数(というほど最大でもないが、いちおうな標準化)な意味合いで使える。
書籍の説明上、第6章でPCLで共通化させておいて、第8章で個別の動作を DependencyService を使うことになるのだが、.NET Standard の範囲内であればここの DependencyService が要らなくなる。

特に .NET Standard 2.0 の場合は、

  • ファイルまわり
  • HTTPまわり

が共通コードで書ける(System.IO.Fileとか、HttpListenerとか)ので、場合のよっては従来 NuGet でとってきた PCL なライブラリの組み込みが必要なくなる。あと、PCL の場合は、対応するプラットフォームによって条件がかなり厳しくなっていて Profile の番号体系も混乱しているので、さっくりと、.NET Standard 2.0 に対応させてしまったほうがよかったりする。

SampleTodo.XForms.Std

.NET Standard 版の SampleTodo.XForms.Std を新しく作成する。

moonmile/xamarin-samples
https://github.com/moonmile/xamarin-samples

プロジェクトを新しく作成しなくても、既存の PCL のプロジェクトを、

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Xamarin.Forms" Version="3.0.0.482510" />
  </ItemGroup>
</Project>

な風に書き替えればよいのだが、Xamarin.Forms のバージョンが「3」になったり(つい5月にver3になっている)していたので、作り直してみた。

小さいサンプルプロジェクトなので、あとは PCL 版の SampleTodo.XForms からコードを移していくだけでよい。

ちなみに、本格的な ToDo アプリを作りたい場合は、テンプレートの Master-Detail から書き起こしていったほうがよい。ViewModel 間のメッセージのやりとりが MessagingCenter.Subscribe になっているのはこれに準じているため。

PCLのコードでは、DependencyServiceを使って、Android/iOSで別のコードを共通化することになっていた。

IToDoStorage storage = DependencyService.Get<IToDoStorage>();
/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    using (var st = storage.OpenWriter("save.xml"))
    {
        viewModel.Items.Save(st);
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    using (var st = storage.OpenReader("save.xml"))
    {
        if (st == null || items.Load(st) == false)
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    viewModel.Items = items;
}

これを、.NET Standard 版で書き替えると、素直に System.IO.File
が使えるようになる。

/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    using (var st = System.IO.File.OpenWrite(path))
    {
        viewModel.Items.Save(st);
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    // .NET Standard 版では、
    // Android/iOSのコードも、共有プロジェクトに書ける
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    if (System.IO.File.Exists(path))
    {
        var st = System.IO.File.OpenRead(path);
        if (items.Load(st) == false)
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    else
    {
        // 初期データを作成する
        items = ToDoFiltableCollection.MakeSampleData();
    }
    viewModel.Items = items;
}

だが、UWPを付け加えるとストレージ絡みはやっぱり、DependencyService を使うという羽目に陥る。

IToDoStorage storage = DependencyService.Get<IToDoStorage>();

/// <summary>
/// 内部ストレージに保存
/// </summary>

void Save()
{
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    try
    {
        using (var st = System.IO.File.OpenWrite(path))
        {
            viewModel.Items.Save(st);
        }
    }
    catch
    {
        // UWPを含める場合は従来通り DependencyService を使う
        using (var st = storage.OpenWriter("save.xml"))
        {
            viewModel.Items.Save(st);
        }
    }
}
/// <summary>
/// 内部ストレージから読み込み
/// </summary>

void Load()
{
    var items = new ToDoFiltableCollection();
    // .NET Standard 版では、
    // Android/iOSのコードも、共有プロジェクトに書ける
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var path = System.IO.Path.Combine(docs, "save.xml");
    try
    {
        if (System.IO.File.Exists(path))
        {
            var st = System.IO.File.OpenRead(path);
            if (items.Load(st) == false)
            {
                // 初期データを作成する
                items = ToDoFiltableCollection.MakeSampleData();
            }
        }
        else
        {
            // 初期データを作成する
            items = ToDoFiltableCollection.MakeSampleData();
        }
    }
    catch
    {
        // UWPを含める場合は従来通り DependencyService を使う
        using (var st = storage.OpenReader("save.xml"))
        {
            if (st == null || items.Load(st) == false)
            {
                // 初期データを作成する
                items = ToDoFiltableCollection.MakeSampleData();
            }
        }
    }
    viewModel.Items = items;
}

これは、UWPの場合だけファイルアクセス方法が違っていて、↓な風に Windows.Storage.ApplicationData にアクセスするという制限からきている。これって、System.Environment.GetFolderPath にエイリアスしてくれないですかね?

public Stream OpenWriter(string file)
{
    var folder = Windows.Storage.ApplicationData.Current.LocalFolder;
    var t = folder.OpenStreamForWriteAsync(file, Windows.Storage.CreationCollisionOption.ReplaceExisting);
    t.Wait();
    var st = t.Result;
    return st;
}

既存のPCLプロジェクトを.NET Standard化するべきか?

仕事の場合、iOSとAndroidの共通化に特化しているだろうから(UWPプロジェクトは使っていないだろう)、.NET Standard にしても共通で使える部分が多いと思うのだけど、それぞれのプロジェクトに Xamarin.Forms.Dependency で書いていたものを、共通の .NET Standard プロジェクトに移し替えるか?というとかなりリスキーな気がする。

というのも、

  • そもそも、その Xamarin.Forms.Dependency は .NET Standard に入っているのか?
  • NuGet 等で旧来の PCL を使っていないか?

が問題になってくる。NuGet で提供される PCL プロジェクトが .NET Standard 化されていればよいのだが(去年の夏あたりから順次 .NET Standard 対応になっていたけど)、iOS/Android 個別の動作に特化してる部分は、なかなか難しいのではないかな?と。
なので、サンプル的には「共有プロジェクトは PCL から .NET Standardへ」という形で新規プロジェクトとして作ればよいのだが、既存の巨大となってしまった&現在動いているアプリのコードを PCL から .NET Standard に完全移行するかどうかは微妙な感じになる。

ただし、SampleTodo.XForms から SampleTodo.XForms.Std にプロジェクトのコードを引き継いだように、

  1. PCL プロジェクトの *.csproj を .NET Standard にしてビルドして動作確認。DependencyService 部分はそのまま。
  2. 冗長と思われる iOS/Android 個別の Xamarin.Forms.Dependency なところを、.NET Standard に引っ越しさせる。

という2段階で移行させるのがベターだろう。引っ越しさせなくてもよいのだが、iOSとAndroidでコードが二重化されているよりもひとつにまとまっていたほうが今後のメンテナンスが楽だろうし、コード共有で一生懸命 #if しているよりは、.NET Standard で適宜ライブラリ化しておいたほうがビルド時間も減るというものだ。

カテゴリー: 開発, Xamarin パーマリンク