Trac を Silverlight から扱ってみる

Trac に XML-RPC 経由でチケットを投稿する | Moonmile Solutions Blog
Trac 投稿専用の Windows アプリケーションを作る | Moonmile Solutions Blog

の続きとして、今度は Silverlight から Trac を XML-RPC 経由で扱ってみます。

難関のポイントとしては、

  • Silverlight には、XmlDocument が無いので、XML-RPC を扱う時にはどうすればよいのか?LINQ to XML を使う?
  • Silverlight の webclient 系のイベントは、非同期なので、どうすればいいのか?
  • Silverlight から Trac を呼び出す時に、同じドメインでないと駄目なのだが、どうすればよいのか?

というところでした。

■Siverlight 版の XML-RPC を使う

例によって、XML-RCP.NET を使う訳ですが、数年前は無かった silverlight 版の xml-rpc が含まれています。
「CookComputing.XmlRpcSilverlight.dll」を silverlight のプロジェクトから参照設定してやると、ほぼwindows 版と同じように使えます。

ここでほぼってのが注意点です。

■siverlight 版の XML-RPC が非同期に対応している。

xml-rpc.net のソリューションを見ると、samples というフォルダーに「StateNameSilverlightClient」というプロジェクトがあります。ここで、xml-rpc のコードが、webclient 系の非同期メソッドを吸収しています。

IXmlRpcProxy インターフェースを継承する、プロキシクラスの書き方が、windows 版と silverlight 版で違います。

public interface ITrac : IXmlRpcProxy
{
#if false
	// windows 版
	[XmlRpcMethod("ticket.get")]
	object[] TicketGet(int id);
	[XmlRpcMethod("ticket.create")]
	int TicketCreate(string summary, string desc, XmlRpcStruct attrs);
	[XmlRpcMethod("ticket.update")]
	object[] TicketUpdate(int id,  string comment, XmlRpcStruct attrs);
	[XmlRpcMethod("ticket.delete")]
	int TicketDelete(int id);
	[XmlRpcMethod("ticket.getActions")]
	object[] TicketGetActions(int id);
#else
	// silverlight 版
	[XmlRpcBegin("ticket.get")]
	IAsyncResult BeginTicketGet(int id, AsyncCallback acb);
	[XmlRpcEnd]
	object[] EndTicketGet(IAsyncResult iasr);
	[XmlRpcBegin("ticket.create")]
	IAsyncResult BeginTicketCreate(string summary, string desc, XmlRpcStruct attrs, AsyncCallback acb);
	[XmlRpcEnd]
	int EndTicketCreate(IAsyncResult iasr);
	[XmlRpcBegin("ticket.update")]
	IAsyncResult BeginTicketUpdate(int id, string comment, XmlRpcStruct attrs, AsyncCallback acb);
	[XmlRpcEnd]
	object[] EndTicketUpdate(IAsyncResult iasr);
	[XmlRpcBegin("ticket.delete")]
	IAsyncResult BeginTicketDelete(int id, AsyncCallback acb);
	[XmlRpcEnd]
	int EndTicketDelete(IAsyncResult iasr);
	[XmlRpcBegin("ticket.getActions")]
	IAsyncResult BeginTicketGetActions(int id, AsyncCallback acb);
	[XmlRpcEnd]
	object[] EndTicketGetActions(IAsyncResult iasr);
#endif
}

windows 版のほうは、object[] TicketGet(int id); のように単純な関数呼び出しなのですが、silverlight のほうは、

  • IAsyncResult BeginTicketGet(int id, AsyncCallback acb);
  • object[] EndTicketGet(IAsyncResult iasr);

という2つのメソッドを作ります。Trac の xml-rpc をコールしてるところと、結果を得るところ、の2つということです。これの使い方は、

/// <summary>
/// 指定IDのチケットを取得
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public void TicketGet(int id)
{
	ITrac proxy = CreateProxy();
	proxy.BeginTicketGet(id, asr =>
		{
			Dispatcher.BeginInvoke(delegate()
			{
				object[] res = proxy.EndTicketGet(asr);
				Ticket ti = new Ticket(res);
				// UI に戻すコールバック
				completeTikectGet(ti);
			});
		});
}

上記のように書きます。Dispatcher.BeginInvoke のところが冗長な気もするのですが、サンプルコードがこうなっているので、そのままで。completeTikectGet のコールバックは、以下のようにハンドラを定義しておきます。

public Action<Ticket> completeTikectGet;

そして、UI のほう(XAMLのほう)で、次のようにロード時に登録しておきます。

private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
	_trac.Setting.Url = "http://localhost:8000/trac/gokui-ios5/login/rpc";
	_trac.Setting.UserName = "masuda";
	_trac.Setting.Password = "masuda";

	_trac.completeTikectGet    += eventTicketGet;
	_trac.completeTicketCreate += eventTicketCreate;
	_trac.completeTicektUpdate += eventTicketUpdate;
	_trac.completeTicketDelete += eventTicketDelete;
}

そして、ボタンイベントとコールバックを定義します。

/// <summary>
/// 指定IDのチケットを取得
/// </summary>
/// <param name="ti"></param>
private void buttonRead_Click(object sender, RoutedEventArgs e)
{
	int id = int.Parse(textBoxID.Text);
	_trac.TicketGet(id);
}
private void eventTicketGet(Ticket ti)
{
	_ti = ti;
	textBoxID.Text = ti.ID.ToString();
	textBoxID.IsReadOnly = false;
	textBoxSummary.Text = ti.Summary;
	textBoxRepoter.Text = ti.Reporter;
	textBoxOwner.Text = ti.Owner;
	textBoxDescription.Text = ti.Description;
}

この部分、無名関数を利用すれば短くなるのですが、無名関数内のデバッグがしずらい(ここでは、eventTicketGet メソッド内のコード)という理由で、メソッドを分けています。TicketGet メソッドの呼び出し時に動的にハンドラを設定してもよいのですが、なんか冗長な気がするのでやめました。

■ClientBin/*.xap をそのまま Trac の apache へコピーする

ドメイン間のセキュリティの問題は、silverlight の実行ファイル(*.xap)を apache のほうにコピーしてしまいます。そうすると、apache のほうのドメインになるので、silverlight から trac へ xml-rpc が通るようになります。

テスト的に実行する場合は、silverlight の web プロジェクト(TracSilverlight.Web など)にある TracSilverlightTestPage.html と clientBin/*.xap をコピーするだけです。TracSilverlightTestPage.html のほうは適当に、index.html に名前を変えておきます。

逆に、siverlight 側で、php か何かでプロキシを通してもよいですね。このほうが IIS 上で動作するのでデバッグは楽になりそうです。

■画面レイアウトはチープに

本当はリッチな画面が作れるのですが、ひとまず、Windows アプリで稼働済みな画面をそのままコピーします。

■XAML側のコード

namespace TracSilverlight
{
	public partial class MainPage : UserControl
	{
		public MainPage()
		{
			InitializeComponent();
		}

		private string _Repoter = "masuda";
		private TracTools _trac = new TracTools();
		private Ticket _ti = null;

		private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
		{
			_trac.Setting.Url = "http://localhost:8000/trac/gokui-ios5/login/rpc";
			_trac.Setting.UserName = "masuda";
			_trac.Setting.Password = "masuda";

			_trac.completeTikectGet    += eventTicketGet;
			_trac.completeTicketCreate += eventTicketCreate;
			_trac.completeTicektUpdate += eventTicketUpdate;
			_trac.completeTicketDelete += eventTicketDelete;
		}

		/// <summary>
		/// 新規作成
		/// テキストボックス等をクリアする
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void buttonClear_Click(object sender, RoutedEventArgs e)
		{
			textBoxID.Text = "";
			textBoxID.IsReadOnly = true;
			textBoxSummary.Text = "";
			textBoxRepoter.Text = _Repoter;
			textBoxOwner.Text = "someone";
			textBoxDescription.Text = "";
			textBoxAction.Text = "";
			textBoxActionValue.Text = "";
			_ti = null;
		}

		/// <summary>
		/// 指定IDのチケットを取得
		/// </summary>
		/// <param name="ti"></param>
		private void buttonRead_Click(object sender, RoutedEventArgs e)
		{
			int id = int.Parse(textBoxID.Text);
			_trac.TicketGet(id);
		}
		private void eventTicketGet(Ticket ti)
		{
			_ti = ti;
			textBoxID.Text = ti.ID.ToString();
			textBoxID.IsReadOnly = false;
			textBoxSummary.Text = ti.Summary;
			textBoxRepoter.Text = ti.Reporter;
			textBoxOwner.Text = ti.Owner;
			textBoxDescription.Text = ti.Description;
		}

		/// <summary>
		/// チケットを書き込む
		/// IDが空白の場合は、新規登録。
		/// IDが空白でない場合は、更新登録。
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void buttonWrite_Click(object sender, RoutedEventArgs e)
		{
			if (textBoxID.Text == "")
			{
				// 新規登録
				Ticket ti = new Ticket();
				ti.Summary = textBoxSummary.Text;
				ti.Description = textBoxDescription.Text;
				ti.Reporter = textBoxRepoter.Text;
				ti.Owner = textBoxOwner.Text;
				_ti = ti;
				_trac.TicketCreate(ti);
			}
			else
			{
				// 更新登録
				int id = int.Parse(textBoxID.Text);

				Ticket ti = _ti;
				ti.Description = textBoxDescription.Text;
				ti.Owner = textBoxOwner.Text;
				_trac.TicketUpdate(ti);
			}
		}
		private void eventTicketCreate(int id)
		{
			textBoxID.Text = id.ToString();
			textBoxID.IsReadOnly = true;
		}
		private void eventTicketUpdate(Ticket ti)
		{
			_ti = ti;
			textBoxID.IsReadOnly = true;
		}

		/// <summary>
		/// 指定IDのチケットを削除
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void buttonDelete_Click(object sender, RoutedEventArgs e)
		{
			if (_ti == null)
				return;
			_trac.TicketDelete(_ti.ID);
		}
		private void eventTicketDelete(int res)
		{
			textBoxID.Text = "";
			textBoxSummary.Text = "";
			textBoxRepoter.Text = _Repoter;
			textBoxOwner.Text = "someone";
			textBoxDescription.Text = "";
			textBoxAction.Text = "";
			textBoxActionValue.Text = "";
			textBoxID.IsReadOnly = false;
			_ti = null;
		}

		/// <summary>
		/// アクションを更新
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void buttonAction_Click(object sender, RoutedEventArgs e)
		{
			if (_ti == null)
				return;
			string act = textBoxAction.Text;
			_trac.TicketUpdate(_ti, act);
		}
	}
}

■TracTools クラス

namespace moonmile.trac
{
	public class TracTools : UserControl
	{
		public _Setting Setting { get; set; }

		public class _Setting
		{
			public string UserName { get; set; }
			public string Password { get; set; }
			public string Url { get; set; }
		}
		public TracTools()
		{
			this.Setting = new _Setting();
		}

		/// <summary>
		/// プロキシを作成する
		/// </summary>
		/// <returns></returns>
		private ITrac CreateProxy()
		{
			ITrac proxy = CookComputing.XmlRpc.XmlRpcProxyGen.Create<ITrac>();
			proxy.Url = this.Setting.Url;
			proxy.UserAgent = "trac-tools";
			proxy.Credentials = new NetworkCredential(
				Setting.UserName, Setting.Password);
			return proxy;
		}
		/// <summary>
		/// api をリストアップ
		/// </summary>
		/// <returns></returns>
		public List<string> ListMethods()
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			string [] res = proxy.SystemListMethods();
			List<string> lst = res.ToList<string>();
			return lst;
		}
		public string MethodHelp(string name)
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			return proxy.SystemMethodHelp(name);
		}

		/// <summary>
		/// アクションをリストアップ
		/// </summary>
		/// <param name="id"></param>
		/// <returns></returns>
		public void TicketGetActions(int id)
		{
			ITrac proxy = CreateProxy();
			proxy.BeginTicketGetActions(id, asr =>
			{
				Dispatcher.BeginInvoke(delegate()
				{
					object[] res = proxy.EndTicketGetActions(asr);
				});
			});
		}

		/// <summary>
		/// 指定IDのチケットを取得
		/// </summary>
		/// <param name="id"></param>
		/// <returns></returns>
		public void TicketGet(int id)
		{
			ITrac proxy = CreateProxy();
			proxy.BeginTicketGet(id, asr =>
				{
					Dispatcher.BeginInvoke(delegate()
					{
						object[] res = proxy.EndTicketGet(asr);
						Ticket ti = new Ticket(res);
						// UI に戻すコールバック
						completeTikectGet(ti);
					});
				});
		}

		/// <summary>
		/// チケットを新規作成
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public void TicketCreate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			proxy.BeginTicketCreate(ti.Summary, ti.Description, ti.Attributes(), asr =>
				{
					Dispatcher.BeginInvoke(delegate()
					{
						int id = proxy.EndTicketCreate(asr);
						completeTicketCreate(id);
					});
				});
		}

		/// <summary>
		/// チケットにコメントを追加
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public void TicketUpdate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			proxy.BeginTicketUpdate(ti.ID, ti.Comment, ti.Attributes(), asr =>
				{
					Dispatcher.BeginInvoke(delegate()
					{
						object[] res = proxy.EndTicketUpdate(asr);
						Ticket ti2 = new Ticket(res);
						completeTicektUpdate(ti2);
					});
				});
		}
		/// <summary>
		/// チケットのアクションを更新
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public void TicketUpdate(Ticket ti, string act)
		{
			ITrac proxy = CreateProxy();
			XmlRpcStruct st = ti.Attributes();
			st.Add("action", act);

			proxy.BeginTicketUpdate(ti.ID, ti.Comment, ti.Attributes(), asr =>
			{
				Dispatcher.BeginInvoke(delegate()
				{
					object[] res = proxy.EndTicketUpdate(asr);
					Ticket ti2 = new Ticket(res);
					completeTicektUpdate(ti2);
				});
			});
		}

		/// <summary>
		/// チケットを削除
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public void TicketDelete(int id)
		{
			ITrac proxy = CreateProxy();
			proxy.BeginTicketDelete(id, asr =>
				{
					Dispatcher.BeginInvoke(delegate()
					{
						int res = proxy.EndTicketDelete(asr);
						completeTicketDelete(res);
					});
				});
		}

		public Action<Ticket> completeTikectGet;
		public Action<int>    completeTicketCreate;
		public Action<Ticket> completeTicektUpdate;
		public Action<int> completeTicketDelete;
	}

	public interface ITrac : IXmlRpcProxy
	{
#if false
		[XmlRpcMethod("ticket.get")]
		object[] TicketGet(int id);
		[XmlRpcMethod("ticket.create")]
		int TicketCreate(string summary, string desc, XmlRpcStruct attrs);
		[XmlRpcMethod("ticket.update")]
		object[] TicketUpdate(int id,  string comment, XmlRpcStruct attrs);
		[XmlRpcMethod("ticket.delete")]
		int TicketDelete(int id);
		[XmlRpcMethod("ticket.getActions")]
		object[] TicketGetActions(int id);
#else
		[XmlRpcBegin("ticket.get")]
		IAsyncResult BeginTicketGet(int id, AsyncCallback acb);
		[XmlRpcEnd]
		object[] EndTicketGet(IAsyncResult iasr);
		[XmlRpcBegin("ticket.create")]
		IAsyncResult BeginTicketCreate(string summary, string desc, XmlRpcStruct attrs, AsyncCallback acb);
		[XmlRpcEnd]
		int EndTicketCreate(IAsyncResult iasr);
		[XmlRpcBegin("ticket.update")]
		IAsyncResult BeginTicketUpdate(int id, string comment, XmlRpcStruct attrs, AsyncCallback acb);
		[XmlRpcEnd]
		object[] EndTicketUpdate(IAsyncResult iasr);
		[XmlRpcBegin("ticket.delete")]
		IAsyncResult BeginTicketDelete(int id, AsyncCallback acb);
		[XmlRpcEnd]
		int EndTicketDelete(IAsyncResult iasr);
		[XmlRpcBegin("ticket.getActions")]
		IAsyncResult BeginTicketGetActions(int id, AsyncCallback acb);
		[XmlRpcEnd]
		object[] EndTicketGetActions(IAsyncResult iasr);
#endif
	}

	/// <summary>
	/// チケットの大まかなクラス
	/// </summary>
	public class Ticket
	{
		public int ID;
		public DateTime CreateDateTime;
		public DateTime ChangeDateTime;
		public string Summary = "";
		public string Reporter = "";
		public string Owner = "";
		public string Description = "";
		public string TicketType = "";
		public string Priority = "";
		public string Comment = "";
		public object[] result = null;

		public Ticket()
		{
		}
		public Ticket(object[] ary)
		{
			this.FromArray(ary);
		}

		public Ticket FromArray(object[] ary)
		{
			Ticket ti = this;
			ti.ID = (int)ary[0];
			ti.result = ary;
			foreach (object o in ary)
			{
				XmlRpcStruct st = o as XmlRpcStruct;
				if (st != null)
				{
					foreach (string key in st.Keys)
					{
						object obj = st[key];
						Console.WriteLine("{0}:{1}:{2}", key, obj.GetType(), obj);
						switch (key)
						{
							case "owner": ti.Owner = (string)obj; break;
							case "reporter": ti.Reporter = (string)obj; break;
							case "summary": ti.Summary = (string)obj; break;
							case "description": ti.Description = (string)obj; break;
							case "time": ti.CreateDateTime = (DateTime)obj; break;
							case "changetime": ti.ChangeDateTime = (DateTime)obj; break;
							case "type": ti.TicketType = (string)obj; break;
							case "priority": ti.Priority = (string)obj; break;
						}
					}
				}
			}
			return ti;
		}
		public XmlRpcStruct Attributes()
		{
			XmlRpcStruct st = new XmlRpcStruct();
			st.Add( "owner", this.Owner );
			st.Add("reporter", this.Reporter);
			st.Add("summary", this.Summary);
			st.Add("description", this.Description);
			st.Add("type", this.TicketType);
			st.Add("priority", this.Priority);
			return st;
		}
	}
}

非同期イベントのところを変えないといけないので、windows 版のものとは共有できません。このあたり、もうちょっと工夫したいところです。

■実行してみる

画面は windows と同じように作れます。Blend を使うと、もっときれいな画面ができるのですが、まあ骨格を先に作るということです。

windows 版のほうは、実行ファイルを配布しないといけないのですが、silverlight 版だとブラウザから自動でダウンロードされます。なので、バージョンアップに関しては silverlight のほうはうまくいきます。ただし、その他の画面切替えやらメニュー表示などをしようとすると、windows アプリとは違うところでハマります。これは WPF アプリでも同じですが(windows 8 になると、更にややこしいかも)

カテゴリー: C#, Trac パーマリンク