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


 Xamarin - Developers - Guides - Mac - UserInterface - TableViews Using Table Views in a Xamarin.Mac applicationを参考にして、QiitaAPI GET /api/v2/itemsで投稿一覧を取得したDataTableをNSTableで表示してみた。

 なお、C#でJSONも扱えるようですが、うまく取得できなかったので正規表現で取得しています。

API

NSTableView

概要

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

制限事項

匿名アクセスのため、時間60回以上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」に変更 画像は9だが4で列を作成
    • BorderedScrollViewを選択してSizeInspectorで、View-Arrangeのコンボボックスで「FillContainerHorizontal」と「FillContainerVertical」をクリックして最大化
    • AddNewConstraintsをクリックして、上下左右の赤いmarginIをアクティブにして「Add4Constraints」ボタンをクリック
  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:」を選択
    6. Save

実行画面

コード

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
  2. Xamarin - Developers - Guides - Mac - UserInterface - Menus
  3. Xamarin - Developers - Guides - Mac - UserInterface - Alerts
  4. QiitaAPI GET /api/v2/items

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

License:

Copyright (c) 2017 grayhead0603
Released under the MIT license