[VS-Mac]VisualStudio for Mac (Cocoa)でDataTable（Qiita投稿一覧）をTableViewで表示

　Xamarin - Developers - Guides - Mac - UserInterface - TableViews [Using Table Views in a Xamarin.Mac application](https://developer.xamarin.com/guides/mac/user-interface/working-with-tableviews/)を参考にして、QiitaAPI [GET /api/v2/items](http://qiita.com/api/v2/docs#get-apiv2items)で投稿一覧を取得したDataTableをNSTableで表示してみた。  
　なお、C#でJSONも扱えるようですが、うまく取得できなかったので正規表現で取得しています。

## API
[NSTableView](https://developer.apple.com/reference/appkit/nstableview)

## 概要
* 「GET /api/v2/items」で読み込んだQiita最新記事リストのデータテーブルをViewBasedのNSTableViewに表示します。
* ArrayControllerは利用していません。
* リスト取得後書類フォルダのsqliteデータベース（Qiita.db）にも保存します。
* ２回目以降の起動時は、sqliteを読み込みます。
* ファイルメニューの「New(Read API)」で最新記事を再読み込みします。

## 制限事項
匿名アクセスのため、時間６０回以上Updateするとエラーになるかもしれません。

## ソリューションの新規作成
省略

## 参照の追加
* System.Data
* Mono.Data.Sqlite

## Classの追加とコード編集
1. フォルダ「MyDatabase」を追加（右クリック新しいフォルダ）
2. フォルダに新しいファイルを追加　「Qiita.cs」
3. ArticleTableDataSource.cs追加
4. ArticleTableDelegate.cs追加
5. ViewController.csを編集

## InterfaceBuilder
1. ViewControllerにNSTableViewを追加
2. Childの「ClipView」を展開して「TableView」を選択
    * AttributesInspectorのColumnsを「4」に変更![Table05.png](https://qiita-image-store.s3.amazonaws.com/0/182396/54936147-f6e6-f295-4a60-ad6c6e11278a.png)
画像は９だが４で列を作成
    * BorderedScrollViewを選択してSizeInspectorで、View-Arrangeのコンボボックスで「FillContainerHorizontal」と「FillContainerVertical」をクリックして最大化
    * AddNewConstraintsをクリックして、上下左右の赤いmarginIをアクティブにして「Add4Constraints」ボタンをクリック![Table06.png](https://qiita-image-store.s3.amazonaws.com/0/182396/5096faf8-1cf3-b23a-d058-b6f1030bb8ec.png)

3. さらに展開して、TableColumnを一つずつ選択して、AttributesInspectorのTitleをTitle,User,Created,Tagsにそれぞれ変更していく。
4. AssistantEditorにViewController.hを表示してOutletを追加
    1. TableView: Connection:Outlet Name:ArticleTable
    2. 各TableColumn Connection:Outlet Name:Column[各Title]
5. Menuの編集
    1. MenuのFile - Newを選択
    2. AttributesInspectorでTitleを「New(Read API)」に変更
    3. 選択したNewメニューをCtrlキーを押してFirstResponderにドラッグ
    4. Action選択の黒のポップアップが出現する
    5. ViewController.csに追加した「readApi:」を選択
![Table11.png](https://qiita-image-store.s3.amazonaws.com/0/182396/2812971a-03ed-ea36-a2db-1509d9ad4ee4.png)
    6. Save

## 実行画面
![Table10.png](https://qiita-image-store.s3.amazonaws.com/0/182396/6f681bfc-d972-abce-56bb-ef4a4655e7cc.png)
## コード
### Qiita.cs
```
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Data;
using Mono.Data.Sqlite;

namespace MyDatabase
{
	
    public class Qiita
	{
		private string _sqlitefile = Environment.GetFolderPath(Environment.SpecialFolder.Personal)
                                                + "/Documents/Qiita.db";
		private SqliteConnection _conn;
		private DataTable _tbl;
        private DataView _dv;
        public DataView Dv { get { return _dv; } }

        public Qiita()
		{
			CreateTable();
            if (!File.Exists(_sqlitefile))
                CreateSqlite();
            else
                LoadSqlite();
		}
		public int GetList(int page_count, int perpage)
		{
            int count = 0;
            if (perpage > 100)
                perpage = 100;
			using (WebClient wc = new WebClient())
			{
				int page = 1;
				do
				{
                    string url = $"https://qiita.com/api/v2/items?page={page}&per_page={perpage}";
					Console.WriteLine(url);
					wc.Encoding = Encoding.UTF8;
					string source = wc.DownloadString(url);
					int count1 = ReadJson(source);
					if (count1 == 0)
						break;
                    count += count1;
					page++;
				} while (page <= Math.Min(page_count, 60));
			}
            if(count > 0)
				UpdateSqlite();

			return count;
		}
		private int ReadJson(string json)
		{
            int count = 0;
			string pattern = "\"rendered_body\".*?\"body\":\"(?<body>.*?)\".*?\"created_at\":\"(?<create>.*?)\"" +
				".*?\"group\":(?<group>.*?),.*?\"id\":\"(?<id>.*?)\".*?\"tags\":\\[(?<tags>.*?)}\\]" +
				".*?\"title\":\"(?<title>.*?)\".*?\"updated_at\":\"(?<update>.*?)\".*?\"url\":\"(?<url>.*?)\".*?\"id\":\"(?<userid>.*?)\"" +
				".*?\"website_url";
			MatchCollection mc = Regex.Matches(json, pattern);
			Console.WriteLine(mc.Count);
			if (mc.Count > 0)
			{
                Dv.Sort = "id";
				foreach (Match m in mc)
				{
                    int idx = _dv.Find(m.Groups["id"].Value);
                    if (idx < 0)
                    {
                        DataRowView r = _dv.AddNew();
                        r.BeginEdit();
                        r["id"] = m.Groups["id"].Value;
                        r["title"] = m.Groups["title"].Value;
                        r["url"] = m.Groups["url"].Value;
                        r["user"] = m.Groups["userid"].Value;
                        r["body"] = m.Groups["body"].Value;
                        r["groups"] = m.Groups["group"].Value;
                        string pattern_tag = "\"name\":\"(?<name>.*?)\"";
                        MatchCollection mc_tag = Regex.Matches(m.Groups["tags"].Value, pattern_tag);
                        StringBuilder sb = new StringBuilder();
                        foreach (Match m2 in mc_tag)
                            sb.AppendFormat(" {0}", m2.Groups["name"].Value);
                        string tag = sb.ToString().Trim();
                        r["tags"] = tag;
                        string[] ss = m.Groups["create"].Value.Replace("T", " ").Split('+');
                        //r["created"] = DateTime.Parse(ss[0]).Add(TimeSpan.Parse(ss[1]));
						r["created"] = DateTime.Parse(ss[0]);
						ss = m.Groups["update"].Value.Replace("T", " ").Split('+');
                        r["updated"] = DateTime.Parse(ss[0]);
                        r.EndEdit();
                        count++;
                    }
				}
                Dv.Sort = "created desc";
			}
			else
			{
				Console.WriteLine("Error");
				System.Diagnostics.Debug.Print(json);
			}
			for (int i = 0; i < _tbl.Rows.Count; i++)
			{
				Console.WriteLine(_tbl.Rows[i]["title"].ToString());

				for (int j = 0; j < 7; j++)
					System.Diagnostics.Debug.Write(_tbl.Rows[i][j].ToString() + "\t");
				System.Diagnostics.Debug.WriteLine(_tbl.Rows[i][8].ToString());

			}
			return count;
		}
		private void UpdateSqlite()
		{
			if (_conn == null)
				_conn = new SqliteConnection($"Data Source={_sqlitefile}");
			_conn.Open();
			string field_list = "@id, @title, @url, @user, @created, @updated, @tags, @body, @groups";
			string query = $"replace into qiita values ({field_list})";
			using (SqliteCommand command = new SqliteCommand(query, _conn))
			{
				string[] fields = field_list.Replace("@", "").Replace(" ", "").Split(',');
				for (int i = 0; i < fields.Length; i++)
					command.Parameters.Add(fields[i], DbType.String);
				for (int i = 0; i < _tbl.Rows.Count; i++)
				{
					for (int j = 0; j < 9; j++)
						command.Parameters[fields[j]].Value = _tbl.Rows[i][j].ToString();
					command.ExecuteNonQuery();
				}
			}
			_conn.Close();
		}
		private void CreateTable()
		{
			_tbl = new DataTable();
			_tbl.Columns.Add("id", typeof(string));
			_tbl.Columns.Add("title", typeof(string));
			_tbl.Columns.Add("url", typeof(string));
			_tbl.Columns.Add("user", typeof(string));
			_tbl.Columns.Add("created", typeof(DateTime));
			_tbl.Columns.Add("updated", typeof(DateTime));
			_tbl.Columns.Add("tags", typeof(string));
			_tbl.Columns.Add("body", typeof(string));
			_tbl.Columns.Add("groups", typeof(string));
            _dv = new DataView(_tbl, "", "created desc", DataViewRowState.CurrentRows);
		}

        private void LoadSqlite()
        {
			if (_conn == null)
				_conn = new SqliteConnection($"Data Source={_sqlitefile}");
			Console.WriteLine(_conn.DataSource);
			_conn.Open();
            string query = "select * from qiita";
            using(SqliteCommand command = new SqliteCommand(query, _conn)){
                using(SqliteDataReader reader = command.ExecuteReader()){
                    while(reader.Read())
                    {
                        DataRow r = _tbl.NewRow();
                        for (int i = 0; i < 9;i++)
                            r[i] = (string)reader[i];
                        _tbl.Rows.Add(r);
                    }
                }
            }
            _conn.Close();
		}
		private void CreateSqlite()
		{
			SqliteConnection.CreateFile(_sqlitefile);
			if (_conn == null)
				_conn = new SqliteConnection($"Data Source={_sqlitefile}");
			Console.WriteLine(_conn.DataSource);
			_conn.Open();
			string query = "create table qiita (id text primary key, title text, url text, user text" +
				", created text, updated text, tags text, body text, groups text)";
			using (SqliteCommand command = new SqliteCommand(query, _conn))
			{
				command.ExecuteNonQuery();
			}
			_conn.Close();
            GetList(1, 100);
		}
	}
}
```

### ArticleTableDataSource.cs
```
using System;
using AppKit;
using Foundation;
using System.Data;

namespace MyDatabase
{
	public class ArticleTableDataSource : NSTableViewDataSource
	{
        public DataView Dv { get; } = null;
        private Qiita _qiita;
		public ArticleTableDataSource()
		{
            _qiita = new Qiita();
            Dv = _qiita.Dv;
		}

        public int Update(int page, int per_page)
        {
            return _qiita.GetList(page, per_page);
        }
		public override nint GetRowCount(NSTableView tableView)
		{
            return Dv.Count;
		}
	}
}
```

### ArticleTableDelegate.cs
```
using System;
using AppKit;
using Foundation;

namespace MyDatabase
{
	public class ArticleTableDelegate : NSTableViewDelegate
	{
		private const string CellIdentifier = "ArticleCell";
		private ArticleTableDataSource DataSource;

        public ArticleTableDelegate(ArticleTableDataSource datasource)
		{
			this.DataSource = datasource;
		}
		public override NSView GetViewForItem(NSTableView tableView, NSTableColumn tableColumn, nint row)
		{
			// This pattern allows you reuse existing views when they are no-longer in use.
			// If the returned view is null, you instance up a new view
			// If a non-null view is returned, you modify it enough to reflect the new data

			NSTextField view = (NSTextField)tableView.MakeView(CellIdentifier, this);
			if (view == null)
			{
				view = new NSTextField();
				view.Identifier = CellIdentifier;
				view.BackgroundColor = NSColor.Clear;
				view.Bordered = false;
				view.Selectable = false;
				view.Editable = false;
			}
            // Setup view based on the column selected
            string dbColname = tableColumn.Title.ToLower();
            if (DataSource.Dv[(int)row][dbColname] != DBNull.Value)
            {
                if (dbColname == "created" | dbColname == "updated")
                    view.StringValue = ((DateTime)DataSource.Dv[(int)row][dbColname]).ToString();
                else
                    view.StringValue = (string)DataSource.Dv[(int)row][dbColname];
            }
            else
                view.StringValue = "";
			return view;
		}
	}

}
```

### ViewController.cs
```
using System;

using AppKit;
using Foundation;

namespace MyTutorial_TableQiita
{
    public partial class ViewController : NSViewController
    {
        private MyDatabase.ArticleTableDataSource dataSource;
        public ViewController(IntPtr handle) : base(handle)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            dataSource = new MyDatabase.ArticleTableDataSource();
            ArticleTable.DataSource = dataSource;
            ArticleTable.Delegate = new MyDatabase.ArticleTableDelegate(dataSource);
        }
		public override void ViewWillAppear()
		{
			base.ViewWillAppear();
			this.View.Window.Title = "MyTutorial NSTableView Qiita List";
		}
		public override void ViewWillDisappear()
		{
			base.ViewWillDisappear();
			//Windowを閉じる時にアプリも終了
			NSApplication.SharedApplication.Terminate(Self);
		}
        [Action("readApi:")]
        public void readApi(NSObject sender)
        {
            Console.WriteLine("Request to define readApi");
            var alert = new NSAlert()
            {
                AlertStyle = NSAlertStyle.Informational,
                InformativeText = "最新投稿リストを取得します",
                MessageText = "Qiita",
            };
            alert.AddButton("Ok");
            alert.AddButton("Cancel");
            nint res = 0;
			alert.BeginSheetForResponse(null, (result) =>
			{
				Console.WriteLine("Alert Result: {0}", result);
                res = result;
			});
			if (res == 1000)
			{
				int count = dataSource.Update(1, 50);    //1:page_count  2:per_page
				ArticleTable.ReloadData();
				var alert2 = new NSAlert()
				{
					AlertStyle = NSAlertStyle.Informational,
					MessageText = $"{count} タイトル取得しました。",
				};
				alert2.RunModal();
			}

		}

		public override NSObject RepresentedObject
        {
            get
            {
                return base.RepresentedObject;
            }
            set
            {
                base.RepresentedObject = value;
                // Update the view, if already loaded.
            }
        }
    }
}
```

## Reference:
1. Xamarin - Developers - Guides - Mac - UserInterface - TableViews [Using Table Views in a Xamarin.Mac application](https://developer.xamarin.com/guides/mac/user-interface/working-with-tableviews/)
2. Xamarin - Developers - Guides - Mac - UserInterface - [Menus](https://developer.xamarin.com/guides/mac/user-interface/working-with-menus/)
3.  Xamarin - Developers - Guides - Mac - UserInterface - [Alerts](https://developer.xamarin.com/guides/mac/user-interface/working-with-alerts/)
4. QiitaAPI [GET /api/v2/items](http://qiita.com/api/v2/docs#get-apiv2items)

GUに関する操作方法ならびに関連コードの著作権はXamarinに帰属します。
また、「ArticleTableDataSource.cs」はXamarinの「ProductTableDataSource.cs」を、「ArticleTableDelegate.cs」は「ProductTableDelegate.cs」を参考にデータベース用に改変したものです。


## License:
Copyright (c) 2017 grayhead0603
Released under the [MIT license](http://opensource.org/licenses/mit-license.php)