Xamarin - Developers - Guides - Mac - UserInterface - TableViews Using Table Views in a Xamarin.Mac applicationを参考にして、QiitaAPI GET /api/v2/itemsで投稿一覧を取得したDataTableをNSTableで表示してみた。
なお、C#でJSONも扱えるようですが、うまく取得できなかったので正規表現で取得しています。
API
概要
- 「GET /api/v2/items」で読み込んだQiita最新記事リストのデータテーブルをViewBasedのNSTableViewに表示します。
- ArrayControllerは利用していません。
- リスト取得後書類フォルダのsqliteデータベース(Qiita.db)にも保存します。
- 2回目以降の起動時は、sqliteを読み込みます。
- ファイルメニューの「New(Read API)」で最新記事を再読み込みします。
制限事項
匿名アクセスのため、時間60回以上Updateするとエラーになるかもしれません。
ソリューションの新規作成
省略
参照の追加
- System.Data
- Mono.Data.Sqlite
Classの追加とコード編集
- フォルダ「MyDatabase」を追加(右クリック新しいフォルダ)
- フォルダに新しいファイルを追加 「Qiita.cs」
- ArticleTableDataSource.cs追加
- ArticleTableDelegate.cs追加
- ViewController.csを編集
InterfaceBuilder
- ViewControllerにNSTableViewを追加
-
Childの「ClipView」を展開して「TableView」を選択
さらに展開して、TableColumnを一つずつ選択して、AttributesInspectorのTitleをTitle,User,Created,Tagsにそれぞれ変更していく。
-
AssistantEditorにViewController.hを表示してOutletを追加
- TableView: Connection:Outlet Name:ArticleTable
- 各TableColumn Connection:Outlet Name:Column[各Title]
-
Menuの編集
実行画面
コード
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:
- Xamarin - Developers - Guides - Mac - UserInterface - TableViews Using Table Views in a Xamarin.Mac application
- Xamarin - Developers - Guides - Mac - UserInterface - Menus
- Xamarin - Developers - Guides - Mac - UserInterface - Alerts
- 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