【Xamarin.Mac】プロジェクト内のリソースファイルを実行時にアプリから読み込む

2021/01/18に公開

やりたいこと

Xamarin.Macで作ったデスクトップアプリで、プロジェクト内に用意した設定やテンプレートなどのファイルを実行時に読み込ませたい。

背景・利用シーン

Xamarin.Macでlog4netを使うため、起動時にXMLで書かれたファイルを読みこんでlog4netに渡したい。
また、アプリに簡易的なWebサーバを持たせたい時にテンプレートをファイルを読み込みたいという場面があり、その際にも同様の方法で実現した。

手順

今回はファイルを読み込む例としてプロジェクト内のJSONファイルを読み込み、前回作成した表に表示することにする。
前回の記事:DelegateとDataSourceを使ったTableViewにインクリメンタルサーチを実装する

ファイルの追加

読み込むJSONファイルを作成する。
プロジェクト下にResourcesというフォルダがあるのでここにファイルを追加する。ファイル名はmembers.jsonとした。

members.json

{
  "members": [
    {
      "Name": "a2see",
      "TwitterId": "a2see"
    },
    {
      "Name": "うぇるち",
      "TwitterId": "welchi_"
    },
    {
      "Name": "想間ミレイ",
      "TwitterId": "zerovabi"
    },
    {
      "Name": "月見ねぎとろ",
      "TwitterId": "tukimi_negitoro"
    },
    {
      "Name": "創好リナ",
      "TwitterId": "TsukusuLina"
    },
    {
      "Name": "でんじぃ",
      "TwitterId": "den_oldman"
    },
    {
      "Name": "とりえ",
      "TwitterId": "trpla226"
    },
    {
      "Name": "なもなき",
      "TwitterId": "nam0naki"
    }
  ]
}

ビルドアクションの設定

ソリューションウィンドウからmembers.jsonファイルのコンテキストメニューを表示し、「プロパティ」をクリックする。

ビルド > ビルドアクションドロップダウンで「EmbeddedResource」を選択する。
EmbeddedResourceを選ぶと、「リソースID」にデフォルトのものが設定される。これをクリップボードにコピーしておく。
今回は「tablesearch.Resources.members.json」というIDになった。

ファイルを読み込みの処理を記述する

今回はViewControllerの生成時にファイルを読みこむようにする。
Assembly#GetManifestResourceStream()にリソースIDを渡すことで、Streamとして取得することができる。
あとはStreamを文字列として読み込み、欲しい形に変換して前回作った表示処理に食わせる。

なお、JSONのパースのにあたってNugetでNewtonsoft.Jsonを導入した。

ViewController.cs(該当部分)

// Resource内のファイルを取得
var assm = Assembly.GetExecutingAssembly();
var membersStream = assm.GetManifestResourceStream("tablesearch.Resources.members.json");

// string型としてファイルの最後まで読み込む
var membersJson = new StreamReader(membersStream).ReadToEnd();

// 取得したJSONをList<Member>に変換
var membersJObj = JObject.Parse(membersJson);
var membersJArray = (JArray)membersJObj["members"];
Members = membersJArray
    .Select(jt => jt.ToObject<Member>())
    .ToList().AsReadOnly();

JSONファイルの内容をテーブルに表示することができた。

環境

Mac OS X 10.15.6
Visual Studio for Mac Community 8.8.1(build 37)
Xamarin Mac 7.0.0.15
XCode Version 12.2

付録

前回のコードに変更を加えて作成した今回のViewController.csファイルの全容を公開しておく。

ViewController.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using AppKit;
using Foundation;
using Newtonsoft.Json.Linq;
namespace tablesearch
{
    public partial class ViewController : NSViewController
    {
        readonly MemberTableViewDataSource dataSource = new MemberTableViewDataSource();
        IReadOnlyCollection<Member> Members;
        public ViewController(IntPtr handle) : base(handle)
        {
            // Resource内のファイルを取得
            var assm = Assembly.GetExecutingAssembly();
            var membersStream = assm.GetManifestResourceStream("tablesearch.Resources.members.json");
            // string型としてファイルの最後まで読み込む
            var membersJson = new StreamReader(membersStream).ReadToEnd();
            // 取得したJSONをList<Member>に変換
            var membersJObj = JObject.Parse(membersJson);
            var membersJArray = (JArray)membersJObj["members"];
            Members = membersJArray
                .ToList()
                .Select(jt => jt.ToObject<Member>())
                .ToList()
                .AsReadOnly();
            dataSource.Members = new List<Member>(Members);
        }
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            MemberTableView.DataSource = dataSource;
            MemberTableView.Delegate = new MemberTableViewDelegate(dataSource);
        }
        public override NSObject RepresentedObject
        {
            get
            {
                return base.RepresentedObject;
            }
            set
            {
                base.RepresentedObject = value;
                // Update the view, if already loaded.
            }
        }
        partial void KeywordChanged(NSObject sender)
        {
            var keyword = ((NSSearchField)sender).StringValue;
            dataSource.Members = Members
                .Where(member => member.Name.Contains(keyword))
                .ToList();
            MemberTableView.ReloadData();
        }
    }
    public class MemberTableViewDataSource : NSTableViewDataSource
    {
        public List<Member> Members { get; set; } = new List<Member>();
        public override nint GetRowCount(NSTableView tableView)
        {
            return Members.Count;
        }
    }
    public class MemberTableViewDelegate : NSTableViewDelegate
    {
        private const string CellIdentifier = "MemberCell";
        private MemberTableViewDataSource dataSource;
        public MemberTableViewDelegate(MemberTableViewDataSource dataSource)
        {
            this.dataSource = dataSource;
        }
        public override NSView GetViewForItem(NSTableView tableView,
            NSTableColumn tableColumn,
            nint row)
        {
            NSTextField view = (NSTextField)tableView.MakeView(CellIdentifier, this);
            if (view == null)
            {
                view = new NSTextField();
                view.Identifier = CellIdentifier;
                view.BackgroundColor = NSColor.Clear;
                view.Bordered = false;
                view.Editable = false;
                view.Selectable = false;
            }
            var member = dataSource.Members[(int)row];
            switch(tableColumn.Title)
            {
                case "Name":
                    view.StringValue = member.Name;
                    break;
                case "Twitter ID":
                    view.StringValue = member.TwitterId;
                    break;
            }
            return view;
        }
    }
}

また、Newtonsoft.Jsonでデシリアライズするに当たってMemberクラスの定義も変更したので、変更後のコードを以下に掲載する。

Member.cs

namespace tablesearch
{
    public class Member
    {
        public string Name;
        public string TwitterId;
        public Member(string name, string twitterId)
        {
            Name = name;
            TwitterId = twitterId;
        }
    }
}

Discussion