もう LINQ to XML は要らないけど、LINQ は必要

ExDoc を Silverlight でも動くようにちまちまと書き換えている途中。

moonmile/ExDoc – GitHub
https://github.com/moonmile/ExDoc

既に NUnit を使ってテストコードを書いていたものを MSTest に書き換えています。本当は NUnit のまま動かしたかったのですが、Silverlight 上で動かん…と言うか、MSTest の場合も Siliverlight のクラスライブラリは対象にしていない(???)という感じで、結局、Visual Studio 2010 の単体テストコードの雰囲気を感じるのも兼ねて、MSTest を利用しています。

さて、その中でちょっと面白いテストコードを追加しました。

public void TestEqualTwoValueLinq()
{
	EXDocument doc = new EXDocument();
	doc.LoadXML(@"
<members>
    <person name='masuda' age='40'>masuda tomoaki</person>
    <person name='yamada' age='20'>yamada taro</person>
</members>
&quot;);
	var els = from t in doc / &quot;members&quot; / &quot;person&quot;
			  where t % &quot;name&quot; == &quot;masuda&quot;
			     && t % &quot;age&quot; == &quot;40&quot;
			  select t;
	EXElement el = els.First();
	Assert.AreEqual(&quot;masuda tomoaki&quot;, el.Value);
}

ExDoc は、暗黙のキャストと演算子のオーバーライドを派手にやっていて、「==」演算子もオーバーライドして EXElements クラス(要素のコレクションクラス)を返すようにしています。このコレクションは、List で定義してあるので、List ジェネリックのメソッドが使えます…つーか、LINQ の文法に直接載せることができます。

じゃあ、LINQ to XML で記述したほうがいいのではないか?という話は脇において。

ExDoc は、直観的にXMLのツリー構造を辿れることを目的としているので、「doc / “members” / “person”」というのは、XPath で云えば「/members/person」と同じことです。

じゃあ、XPath で書けばいいじゃん!という話は脇において。

属性を比較する場合は、「t % “name” == “masuda”」のように書けるのがミソです。
なので、ExDoc と LINQ を組み合わせて、上のような不思議な文法で書けます。

ちなみに、単一の属性の比較の場合は、一行で書けます。

public void TestEqualValue()
{
	EXDocument doc = new EXDocument();
	doc.LoadXML(@&quot;
<members>
    <person name='masuda'>masuda tomoaki</person>
    <person name='yamada'>yamada taro</person>
</members>
&quot;);
	EXElements els = doc / &quot;members&quot; / &quot;person&quot; == &quot;yamada taro&quot;;
	Assert.AreEqual(1, els.Count);
	Assert.AreEqual(&quot;yamada&quot;, (string)(els % &quot;name&quot;));

	EXElement el = doc / &quot;members&quot; / &quot;person&quot; == &quot;yamada taro&quot;;
	Assert.AreEqual(&quot;yamada&quot;, (string)(el % &quot;name&quot;));
}

まぁ、このあたりのワンライナーが作りたかったのが ExDoc の目的です。

ただし、実装的に毎回 foreach で検索して EXElemnt の配列を返しているのでパフォーマンスは良くないんですよね。LINQ の実装と同じく、遅延実行をするようにして

  1. 「/」演算子で取得する部分は、イテレーターの作成のみ
  2. string 型にキャストするか、Value プロパティを参照した時に、探索を実行する。

という工夫が必要なはなずです。まぁ、それは後程。

カテゴリー: 開発, EXDoc | 1件のコメント

moonmile の分家を hateblo.jp に作る

はてなダイアリーは、常々あの「はてな記法」に慣れなくて、躊躇していたのですがブログが公開ベータ版になったそうなので、参加しました。

moonmileのブログ
http://moonmile.hateblo.jp/

ここが記事のまとめの本家だとすると、はてなブログのほうは技術メモ帳代わりに使う予定です。はてなブックマークはいままで使ったことがないのですが、そのあたりもこれから使おうかなぁと。

コードを公開するというよりも、資料集めしたものをはてなブログのほうに追記していくという感じ。今までは、ツイッターに垂れ流しなり、ブログに紛れ込ませるなり、手元のメモ帳なりを使っていたのですが。

カテゴリー: 雑談 | moonmile の分家を hateblo.jp に作る はコメントを受け付けていません

もう LINQ to XML はいらない「ExDoc」を github にアップ

Home – GitHub
https://github.com/moonmile/ExDoc/wiki

github を使ってみようと思って、お試しに ExDoc をアップしてみました。

もう LINQ to XML はいらない(謀略編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2083
もう LINQ to XML はいらない(虚実編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2086

のあたりで作ってみたソースコードですね。
単純な RSS の解析や、twitter api の戻り値なんかを解析するときに、LINQ to XML だと冗長なので、ピンポイントに取れると便利かなぁと。
確か、insert 関係のメソッドを作ったはずなのですが、github に載せたものは入っていない模様です。このあたりは、後から。

そうそう、github へのアクセスは「Git GUI」を使っています。

小粋空間: Github を Windows で利用する(Git GUI編)
http://www.koikikukan.com/archives/2010/08/04-235555.php

の解説に従うと、最初のリポジトリが簡単に作成できます。秘密キーも。


SubVersion クライアントより便利なのかは、ちょっと不明です。Visual Studio のアドオンを使っていたので。

カテゴリー: 開発, C#, VB | 1件のコメント

期間見積の見積期間は、どのくらいが妥当か

「見積もり」にも色々あって、大別すると、「金額見積」と「期間見積」があります。
金額見積のほうは、単価×人員数ということで、いわゆる「人月計算」で使います。経営的、経済的な視点から見れば、社員の給与(あるいは外注費)は、さほど期間に依存するものではありません。期間にあまり依存しないというのは、開発期間が1、2日違ったところで、実は「人月単位」でしか変化しないといういうことです。また、社員なり派遣社員なりの場合には、月次という形で計算するのが普通(あるいは、複数プロジェクトで按分)ですから、時間単価、日単価でちまちまと計算するよりも、大枠の予算にあわせる(あるいは、合わせてプロジェクトが負われるか)に注力したほうが賢明です…が、まぁ大人の事情もありますから、人それぞれ。

さて、ここで話すのは「期間見積」のほうです。開発期間が、1年間なのか、6ヶ月なのか、という見積ですね。
勿論、1人で1年間の期間見積をしたとしても、2人で半年…になるはずもなく(これは自明ですよね、多分)、コミュニケーションコストなり、管理コストなりが掛かって、単純な「単価×人数」にはなりません。

ここで、「どの位の期間でできるのか?」という質問に対して、2つの記述があります。

  • リリース日が、固定であるのか?
  • リリース日は、できるだけ早くがいいのか?

ということで、リリース日が固定というのは、例えば4月から運用開始、とか年末までに仕上げて年始にWebで大々的に発表とかいう、運用リリース、プレスリリースが決まっているものです。リリース固定の場合は、開発期間の見積の優先度として、「リリース日までに終わるのか?」という優先項目があります。

逆に、できるだけ早くというパターンがあり(多分、ベンチャーとかね)、機能やデザインを作成するまでにどのくらいの期間がかかるのか?という見積です。優先事項は、「開発スピード」ということになります。

この2種類を取り違えてしまうと、

  • リリース前にできあがったけど、暇になった。
  • 正常にリリースされたけど、機会を逸してしまった。

というパターンに陥ります。

さて、どちらの期間見積であっても、その見積をする期間(時間)はどのくらいとればよいでしょう?という問題があります。マネージメントの立場からすれば、機能の洗い出し、機能の積み重ね、お客への返答機嫌、などを考えると、2週間と思われるのですが、実のところは先の金額見積もりの制限とあいまって「3日間」ぐらいが限界です。さらに、「ざっくりとした金額」を求められる場合は、即日だったりします。

なので、この1~3日間にどのくらいの正確な見積をすればよいのか?という問題ですが、先の「リリース日までの制限」を見極めて、「単価×人数」で計算した後に、「リスクに対して、どのくらいのパワーを突っ込めるか?」というリスク管理と、「難解な機能があった場合、どの位の期間/予算で解決できるのか」という能力の問題があります。

このあたりの期間見積に関しては、パレートの法則を使った概算を使います。「機能の 20% が全体の 80% を占める」のであれば、全体の 2 割の機能を見積もって、1.2 倍すればよいわけです。全体の 2 割というのは、例えば、ざっくりとした構造設計(機能設計でもよい)をした後に、上位の 2 割だけを正確に見積もればよいのです。あるいは、10 機能という風に数を決めておいて(全体が 50 機能になる)、その部分だけ細かく見積もります。

この上位 2 割の機能をピックアップするときには、

  • マスター系の機能は外す。
  • 移行のための機能は外す。

のように、既知のものは外していきます。既知の機能は、前例があるからこそ「既知」なので、期間見積のぶれはそう大きくないという前提があります。また、何らかのリスクが発生した場合であっても、「既知」だからこそ過去のプロジェクトなり、過去の人員なりでフォローする可能性が高くなります。

そんな訳で、「未知」のものに対して概要的な設計をして、FP などを使い規模が大きいのか、小さいのかを見極めます。「未知」のものが規模的に少ないのであれば、安定したプロジェクトとして単純に「単価×人数」で計算します。「未知」の部分は、リスク管理として別枠で予算/期間を取ります。

逆に「未知」の部分の規模が大きい場合は、先の「リリース日」に合わせて、リリース可能かどうかの照らし合わせをしたのちに、期間から逆算します。逆算してしまうのは、「未知」ゆえのブレをあらかじめ見積もったところで正確には見積もれないのは明らか(ですよね、多分)なので、「リリース日」という制限を優先した後に、リリースに間に合うかどうかを優先させます。

このあたり、後で図解でも書きますか。フロー的に書けると思うので。

カテゴリー: プロジェクト管理 | 期間見積の見積期間は、どのくらいが妥当か はコメントを受け付けていません

小1の宿題を手早く済ますための iPhone アプリを作ってみたが

小学一年生の冬休みの宿題で、算数の繰り上がり/繰り下がりのカードがある。
これを毎日やるのが宿題なのだけど、

  1. 親がカードが捲って
  2. 子供が答える
  3. 間違えていたら、後でもう一度やり直し。

という手順になっている。

2の「親が捲る」のが面倒なので、数を足す練習ぐらいならば「自分でできる」ほうが良かろう、と思って iPhone アプリを作ってみた。

例によって、プロトタイプの画面はひどくチープです。
そして、ロジックは、ばかばかしいほど簡単に。

#import "KidCalcViewController.h"

@implementation KidCalcViewController

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

int num1, num2, ans;
void nextQuest()
{
    
    num1 = rand() % 9 + 1;
    num2 = rand() % num1 + (10-num1);
    ans  = num1 + num2;
}


- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    srand(time(NULL));
    nextQuest();
    labelCalc.text = [NSString stringWithFormat:@"%d + %d = ", num1, num2 ];
    labelAns.text = [NSString stringWithFormat:@"%d", ans ];
    labelAns.hidden = YES;
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
	[super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
	[super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
    } else {
        return YES;
    }
}

-(IBAction)buttonCalcClick:(id)sender
{
    labelAns.hidden = NO;
}
-(IBAction)buttonNextClick:(id)sender
{
    nextQuest();
    labelCalc.text = [NSString stringWithFormat:@"%d + %d = ", num1, num2 ];
    labelAns.text = [NSString stringWithFormat:@"%d", ans ];
    labelAns.hidden = YES;
}
@end

娘に見せて、まぁ評判が良かったら、画像なり色なりを付けてリリースすれば、この冬「相手をしなくてよいか」と画策したのだが、娘曰く

「自分でやって、自分で答えをみると、『ああ、そうか』だけで詰まらない」
「親とやりとりしているのが楽しい」

のだそうだ。

まぁ、そりゃそうだろうな。自分でやってみても「ひどく面白くなかった」ので。IT が埋められないもの(かな?)が其処にある、という話。

カテゴリー: 雑談, Objective-C | 1件のコメント

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=&quot;id&quot;></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=&quot;ti&quot;></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 = &quot;masuda&quot;;
		private TracTools _trac = new TracTools();
		private Ticket _ti = null;

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

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

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

		/// <summary>
		/// 指定IDのチケットを取得
		/// </summary>
		/// <param name=&quot;ti&quot;></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=&quot;sender&quot;></param>
		/// <param name=&quot;e&quot;></param>
		private void buttonWrite_Click(object sender, RoutedEventArgs e)
		{
			if (textBoxID.Text == &quot;&quot;)
			{
				// 新規登録
				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=&quot;sender&quot;></param>
		/// <param name=&quot;e&quot;></param>
		private void buttonDelete_Click(object sender, RoutedEventArgs e)
		{
			if (_ti == null)
				return;
			_trac.TicketDelete(_ti.ID);
		}
		private void eventTicketDelete(int res)
		{
			textBoxID.Text = &quot;&quot;;
			textBoxSummary.Text = &quot;&quot;;
			textBoxRepoter.Text = _Repoter;
			textBoxOwner.Text = &quot;someone&quot;;
			textBoxDescription.Text = &quot;&quot;;
			textBoxAction.Text = &quot;&quot;;
			textBoxActionValue.Text = &quot;&quot;;
			textBoxID.IsReadOnly = false;
			_ti = null;
		}

		/// <summary>
		/// アクションを更新
		/// </summary>
		/// <param name=&quot;sender&quot;></param>
		/// <param name=&quot;e&quot;></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 = &quot;trac-tools&quot;;
			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=&quot;id&quot;></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=&quot;id&quot;></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=&quot;ti&quot;></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=&quot;ti&quot;></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=&quot;ti&quot;></param>
		/// <returns></returns>
		public void TicketUpdate(Ticket ti, string act)
		{
			ITrac proxy = CreateProxy();
			XmlRpcStruct st = ti.Attributes();
			st.Add(&quot;action&quot;, 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=&quot;ti&quot;></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(&quot;ticket.get&quot;)]
		object[] TicketGet(int id);
		[XmlRpcMethod(&quot;ticket.create&quot;)]
		int TicketCreate(string summary, string desc, XmlRpcStruct attrs);
		[XmlRpcMethod(&quot;ticket.update&quot;)]
		object[] TicketUpdate(int id,  string comment, XmlRpcStruct attrs);
		[XmlRpcMethod(&quot;ticket.delete&quot;)]
		int TicketDelete(int id);
		[XmlRpcMethod(&quot;ticket.getActions&quot;)]
		object[] TicketGetActions(int id);
#else
		[XmlRpcBegin(&quot;ticket.get&quot;)]
		IAsyncResult BeginTicketGet(int id, AsyncCallback acb);
		[XmlRpcEnd]
		object[] EndTicketGet(IAsyncResult iasr);
		[XmlRpcBegin(&quot;ticket.create&quot;)]
		IAsyncResult BeginTicketCreate(string summary, string desc, XmlRpcStruct attrs, AsyncCallback acb);
		[XmlRpcEnd]
		int EndTicketCreate(IAsyncResult iasr);
		[XmlRpcBegin(&quot;ticket.update&quot;)]
		IAsyncResult BeginTicketUpdate(int id, string comment, XmlRpcStruct attrs, AsyncCallback acb);
		[XmlRpcEnd]
		object[] EndTicketUpdate(IAsyncResult iasr);
		[XmlRpcBegin(&quot;ticket.delete&quot;)]
		IAsyncResult BeginTicketDelete(int id, AsyncCallback acb);
		[XmlRpcEnd]
		int EndTicketDelete(IAsyncResult iasr);
		[XmlRpcBegin(&quot;ticket.getActions&quot;)]
		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 = &quot;&quot;;
		public string Reporter = &quot;&quot;;
		public string Owner = &quot;&quot;;
		public string Description = &quot;&quot;;
		public string TicketType = &quot;&quot;;
		public string Priority = &quot;&quot;;
		public string Comment = &quot;&quot;;
		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(&quot;{0}:{1}:{2}&quot;, key, obj.GetType(), obj);
						switch (key)
						{
							case &quot;owner&quot;: ti.Owner = (string)obj; break;
							case &quot;reporter&quot;: ti.Reporter = (string)obj; break;
							case &quot;summary&quot;: ti.Summary = (string)obj; break;
							case &quot;description&quot;: ti.Description = (string)obj; break;
							case &quot;time&quot;: ti.CreateDateTime = (DateTime)obj; break;
							case &quot;changetime&quot;: ti.ChangeDateTime = (DateTime)obj; break;
							case &quot;type&quot;: ti.TicketType = (string)obj; break;
							case &quot;priority&quot;: ti.Priority = (string)obj; break;
						}
					}
				}
			}
			return ti;
		}
		public XmlRpcStruct Attributes()
		{
			XmlRpcStruct st = new XmlRpcStruct();
			st.Add( &quot;owner&quot;, this.Owner );
			st.Add(&quot;reporter&quot;, this.Reporter);
			st.Add(&quot;summary&quot;, this.Summary);
			st.Add(&quot;description&quot;, this.Description);
			st.Add(&quot;type&quot;, this.TicketType);
			st.Add(&quot;priority&quot;, this.Priority);
			return st;
		}
	}
}

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

■実行してみる

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

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

カテゴリー: C#, Trac | Trac を Silverlight から扱ってみる はコメントを受け付けていません

Trac 投稿専用の Windows アプリケーションを作る

前回、Trac に XML-RPC 経由でチケットを投稿する | Moonmile Solutions Blog で、コマンドライン版ができたので、これを windows アプリケーションから作ります。

通常、Unix/Linux の場合には、コマンド呼出をアプリケーションの内部に持って、パイプでつなげるという手法を取ります。windows アプリの場合でも、コマンドラインツールに GUI をくっ付ける場合は、そうします。
また、ライブラリとして切り出すことも可能なのですが…実は、.NET の場合には、直接コマンドラインツールを参照設定することで、ツール内にあるクラス(今回は、TracTools クラス)を使えます。クラスライブラリは DLL に限らず、普通の exe ファイルでも使えるのです…という例ですね。

今までは、COM で作ったり、DLL で関数を共通化したりという小細工が必要(設計段階で考えなくちゃいけなかったという意味で)になるのですが、.NET の場合は、exe を直接クラスライブラリとして扱えるので、適当なクラス分けさえしてあれば、windows アプリケーションとして動作させるのも簡単なのです。

# まぁ、コマンドラインツールのほうで、適度にクラス化が必要なわけですが。そのあたりが、UIDD(User Interface Driven Development)に繋がります。

windows アプリのコードは以下のようにさっくりと、

namespace TracWin
{
	public partial class Form1 : Form
	{
		public Form1()
		{
			InitializeComponent();
		}

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

		}

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

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

		/// <summary>
		/// 指定IDのチケットを読み込む
		/// </summary>
		/// <param name=&quot;sender&quot;></param>
		/// <param name=&quot;e&quot;></param>
		private void buttonGet_Click(object sender, EventArgs e)
		{
			if (textBoxID.Text == &quot;&quot;)
				return;
			int id = int.Parse(textBoxID.Text);
			Ticket ti = _trac.TicketGet(id);

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

				Ticket ti = _ti;
				ti.Description = textBoxDescription.Text;
				ti.Owner = textBoxOwner.Text;
				_ti = _trac.TicketUpdate(ti);
				textBoxID.Enabled = false;
			}
		}
		/// <summary>
		/// 指定IDのチケットを削除
		/// </summary>
		/// <param name=&quot;sender&quot;></param>
		/// <param name=&quot;e&quot;></param>
		private void buttonDelete_Click(object sender, EventArgs e)
		{
			if (_ti == null)
				return;
			_trac.TicketDelete(_ti.ID);
			textBoxID.Text = &quot;&quot;;
			textBoxSummary.Text = &quot;&quot;;
			textBoxRepoter.Text = _Repoter;
			textBoxOwner.Text = &quot;someone&quot;;
			textBoxDescription.Text = &quot;&quot;;
			textBoxAction.Text = &quot;&quot;;
			textBoxActionValue.Text = &quot;&quot;;
			textBoxID.Enabled = true;
			_ti = null;
		}

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

画面もチープな感じでよいので、こんな感じ。

こんな風にプロトタイプを作る場合には、

  1. コマンドラインツールで動作確認
  2. 適度にクラス分けをしておく(特に呼び出し形式に注意)
  3. チープな画面で windows アプリとして作る

という流れで作ります。NUnit が絡むところは、1 と 2 の間ぐらいなのですが、1 で動作確認をしていると NUnit を使わなくてもそこそこ動作ができます。いわゆるコードの品質が上がります。
windows アプリを作る 3 の段階では、既に内部の動作確認済みなので、GUI との結合だけを確認します。
そして、その後、画面をリッチにしたり、ボタンの制御(不要なときは押せないとか)の処理を入れていきます。

こういう風に組み立てていくと、不思議な動作による手戻り…というかバグ解析が減るので、結構効率があがりますよ、という話です。あと、今回の trac を xml-rpc でアクセスするような内部動作がいまいち不明な場合、try and error が必要な場合は、こんな風に手順よくやっていったほうが結果的に早く終わります。

カテゴリー: 開発, C#, Trac | Trac 投稿専用の Windows アプリケーションを作る はコメントを受け付けていません

Trac に XML-RPC 経由でチケットを投稿する

Trac のチケット関係の api は結構あるのですが、ひとまず

  • ticket.getActions: アクションを設定する(解決済み、担当者変更など)
  • ticket.get: 指定ID のチケットを取得する
  • ticket.create: 新しいチケットを作成する
  • ticket.update: 既存のチケットを変更する、コメントを付ける
  • ticket.delete: 指定ID のチケットを削除する

なところがあれば、大丈夫かなと。細かなところはブラウザ上で操作すればよいので、完全にエミュレートするのは後ほど機会があれば作っていきます。

以下が、TracTools クラス。チケット自体は、object[] になり、そのままでは扱いづらいので Ticket クラスを作っています。

namespace moonmile.trac
{
	public class TracTools
	{
		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 = &quot;trac-tools&quot;;
			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=&quot;id&quot;></param>
		/// <returns></returns>
		public object[] TicketGetActions(int id)
		{
			ITrac proxy = CreateProxy();
			object[] res = proxy.TicketGetActions(id);

			foreach (object[] act in res)
			{
				Console.WriteLine(&quot;{0}:{1}:{2}&quot;, act[0], act[1], act[2]);
			}
			return res;
		}

		/// <summary>
		/// 指定IDのチケットを取得
		/// </summary>
		/// <param name=&quot;id&quot;></param>
		/// <returns></returns>
		public Ticket TicketGet(int id)
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			object []res =  proxy.TicketGet(id);
			return new Ticket(res);
		}

		/// <summary>
		/// チケットを新規作成
		/// </summary>
		/// <param name=&quot;ti&quot;></param>
		/// <returns></returns>
		public int TicketCreate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			int id = proxy.TicketCreate(ti.Summary, ti.Description, ti.Attributes());
			return id;
		}

		/// <summary>
		/// チケットにコメントを追加
		/// </summary>
		/// <param name=&quot;ti&quot;></param>
		/// <returns></returns>
		public Ticket TicketUpdate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			object[] res = proxy.TicketUpdate(ti.ID, ti.Comment, ti.Attributes());
			return new Ticket(res);
		}
		/// <summary>
		/// チケットのアクションを更新
		/// </summary>
		/// <param name=&quot;ti&quot;></param>
		/// <returns></returns>
		public Ticket TicketUpdate(Ticket ti, string act)
		{
			ITrac proxy = CreateProxy();
			XmlRpcStruct st = ti.Attributes();
			st.Add(&quot;action&quot;, act);
			object[] res = proxy.TicketUpdate(ti.ID, ti.Comment, st);
			return new Ticket(res);
		}
		/// <summary>
		/// チケットを削除
		/// </summary>
		/// <param name=&quot;ti&quot;></param>
		/// <returns></returns>
		public int TicketDelete(int id)
		{
			ITrac proxy = CreateProxy();
			int res = proxy.TicketDelete(id);
			return res;
		}
	}

	public interface ITrac : IXmlRpcProxy
	{
		[XmlRpcMethod(&quot;ticket.get&quot;)]
		object[] TicketGet(int id);
		[XmlRpcMethod(&quot;ticket.create&quot;)]
		int TicketCreate(string summary, string desc, XmlRpcStruct attrs);
		[XmlRpcMethod(&quot;ticket.update&quot;)]
		object[] TicketUpdate(int id,  string comment, XmlRpcStruct attrs);
		[XmlRpcMethod(&quot;ticket.delete&quot;)]
		int TicketDelete(int id);
		[XmlRpcMethod(&quot;ticket.getActions&quot;)]
		object[] TicketGetActions(int id);
	}

	/// <summary>
	/// チケットの大まかなクラス
	/// </summary>
	public class Ticket
	{
		public int ID;
		public DateTime CreateDateTime;
		public DateTime ChangeDateTime;
		public string Summary = &quot;&quot;;
		public string Reporter = &quot;&quot;;
		public string Owner = &quot;&quot;;
		public string Description = &quot;&quot;;
		public string TicketType = &quot;&quot;;
		public string Priority = &quot;&quot;;
		public string Comment = &quot;&quot;;
		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(&quot;{0}:{1}:{2}&quot;, key, obj.GetType(), obj);
						switch (key)
						{
							case &quot;owner&quot;: ti.Owner = (string)obj; break;
							case &quot;reporter&quot;: ti.Reporter = (string)obj; break;
							case &quot;summary&quot;: ti.Summary = (string)obj; break;
							case &quot;description&quot;: ti.Description = (string)obj; break;
							case &quot;time&quot;: ti.CreateDateTime = (DateTime)obj; break;
							case &quot;changetime&quot;: ti.ChangeDateTime = (DateTime)obj; break;
							case &quot;type&quot;: ti.TicketType = (string)obj; break;
							case &quot;priority&quot;: ti.Priority = (string)obj; break;
						}
					}
				}
			}
			return ti;
		}
		public XmlRpcStruct Attributes()
		{
			XmlRpcStruct st = new XmlRpcStruct();
			st.Add( &quot;owner&quot;, this.Owner );
			st.Add(&quot;reporter&quot;, this.Reporter);
			st.Add(&quot;summary&quot;, this.Summary);
			st.Add(&quot;description&quot;, this.Description);
			st.Add(&quot;type&quot;, this.TicketType);
			st.Add(&quot;priority&quot;, this.Priority);
			return st;
		}
	}
}

これを使ったコマンドラインが以下のコード。ユーザー名やらパスワードが直書きですが、これは後で、wordpress の時と同じように config ファイルから取得する予定。

namespace TracPost
{
	class Program
	{
		static void Main(string[] args)
		{
			TracTools trac = new TracTools();

			trac.Setting.Url = &quot;http://localhost:8000/trac/gokui-ios5/login/rpc&quot;;
			trac.Setting.UserName = &quot;masuda&quot;;
			trac.Setting.Password = &quot;masuda&quot;;

			if (args.Length == 0)
			{
				Usage();
				return;
			}
			else
			{
				switch (args[0])
				{
					case &quot;apilist&quot;:
						List<string> lst = trac.ListMethods();
						foreach (string name in lst)
						{
							Console.WriteLine(name);
						}
						break;
					case &quot;help&quot;:
						if (args.Length == 2)
						{
							string txt = trac.MethodHelp(args[1]);
							Console.WriteLine(txt);
						}
						break;
					case &quot;actions&quot;:
						if ( args.Length == 2 ) {
							trac.TicketGetActions(int.Parse(args[1]));
						}
						break;
					case &quot;get&quot;:
						if (args.Length == 2)
						{
							Ticket ti = trac.TicketGet(int.Parse(args[1]));
							Console.WriteLine(ti.Summary);
							Console.WriteLine(ti.Description);
						}
						break;
					case &quot;post&quot;:
						if (args.Length == 3)
						{
							Ticket ti = new Ticket();
							ti.Summary = args[1];     //  &quot;投稿&quot; + DateTime.Now.ToString();
							ti.Description = args[2]; //  &quot;投稿テスト\nat&quot; + DateTime.Now.ToString();
							ti.Reporter = &quot;masuda&quot;;
							ti.Owner = &quot;someone&quot;;
							int id = trac.TicketCreate(ti);
							Console.WriteLine(&quot;ID:{0}&quot;, id );
						}
						break;
					case &quot;update&quot;:
						if (args.Length == 3)
						{
							int id = int.Parse(args[1]);
							Ticket ti = trac.TicketGet(id);
							ti.Comment = args[2];
							ti = trac.TicketUpdate(ti);
						}
						break;
					case &quot;action&quot;:
						if (args.Length == 3)
						{
							int id = int.Parse(args[1]);
							string act = args[2];
							Ticket ti = trac.TicketGet(id);
							ti = trac.TicketUpdate(ti, act);
						}
						break;
					case &quot;delete&quot;:
						if (args.Length == 2)
						{
							int id = int.Parse(args[1]);
							int res = trac.TicketDelete(id);
							Console.WriteLine(&quot;result:{0}&quot;, res);
						}
						break;

				}
			}
		}
		public static void Usage()
		{
			Console.WriteLine(&quot;tracpost list : api list を表示&quot;);
			Console.WriteLine(&quot;tracpost help [command]: コマンドのヘルプを表示&quot;);
			Console.WriteLine(&quot;tracpost get [id]: 指定IDのチケットを取得&quot;);
			Console.WriteLine(&quot;tracpost post [summary] [description]: チケットを登録&quot;);
			Console.WriteLine(&quot;tracpost delete [id]: 指定IDのチケットを削除&quot;);
			Console.WriteLine(&quot;tracpost update [id] [comment]: コメントを追加&quot;);
			Console.WriteLine(&quot;tracpost action [id] [resolve]: アクションを変更&quot;);
		}
	}
}

さて、このコーディングの仕方ですが、順序として「クラス」→「main 関数」の順で作っているわけではなく、「main 関数」で呼び出し側を作った後に、TicketTools クラスをそれに合わせています。
オブジェクト指向設計を思って身構えてしまうと、ついつい複雑なメソッドや余分なプロパティを作ってしまいがちですが、main 関数で、どのようにクラスを利用したいかを書いてから、クラスを実装すると、これらの無駄が省けます。ある意味で、テスト駆動に近いのですが、テスト自体にはあまり重きを置いていなくて、クラスのインターフェース(User Interface Develop Driven とか)を先に決めてどのように使うのかを主導させています。

例えば、新しいチケットをポストする場合は、

Ticket ti = new Ticket();
ti.Summary = args[1];     
ti.Description = args[2]; 
ti.Reporter = "masuda";
ti.Owner = "someone";
int id = trac.TicketCreate(ti);

のように Ticket オブジェクトを作った後に、適当にプロパティを設定しています。これは、以下のようにも書くことも可能です。

string summary = args[1];
string desc = args[2];
int id = trac.TicketCreate(summary, desc, "masuda", "someone");

この場合、TicketCreate メソッドの引数に各パラメータを指定するのですが、フィールドやら何やらが多くなってしまたときには破綻しそうですね。また、チケットの update を考えると、チケットの内容はTicket クラスを使うユーザーとしてなんらかの形でまとめておきたいところです。

で、

public class Ticket 
{
	public string summary;
	public string desc;
	public string repoter ;
	public string owner ;
}

のようにユーザー側でクラスを指定して、TicketCreate メソッドをプロパティを指定して呼び出してもいいのですが、これっていちいちユーザー側で指定するのは面倒な話です。Ticket クラスのプロパティ名などを変えたいときも困るし。

という訳で、ユーザーの気持ちになって、ユーザー側のほうからコーディングをすると、先の「trac.TicketCreate(ti)」で呼び出したほうが楽であろう、というのが、このコーディングスタイルのスタートだったりします。

カテゴリー: C#, Trac | Trac に XML-RPC 経由でチケットを投稿する はコメントを受け付けていません

Trac にダイジェスト認証で接続する

Trac に HttpWebRequest で接続するときは、UserAgent を設定する
http://www.moonmile.net/blog/archives/2679

の続き。C# から XML-RPC で接続するので、http://www.xml-rpc.net/ を使いました。なんとなくオーバーヘッドが多そうなので気になるところですが、wordpress の post ツールを作ったことなので、その流用も兼ねて利用します。

public class TracTools
{
	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 = &quot;trac-tools&quot;;
		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;
	}
}

実際は、下記のようなプロキシのクラスを作るのですが、api をリストアップするメソッドは、既に IXmlRpcProxy インターフェース内で SystemListMethods として定義済みなので、このまま使います。xml-rpc で「system.listMethods」として呼び出されます。

public interface ITrac : IXmlRpcProxy
{
	[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.getActions")]
	object[] TicketGetActions(int id);
}

ダイジェスト認証を使う場合は、プロキシの Credentials プロパティに情報を設定します。

呼出はこんな感じ。

static void Main(string[] args)
{
TracTools trac = new TracTools();

trac.Setting.Url = &quot;http://localhost:8000/trac/gokui-ios5/login/rpc&quot;;
trac.Setting.UserName = &quot;masuda&quot;;
trac.Setting.Password = &quot;masuda&quot;;

if (args.Length == 0)
{
	Usage();
	return;
}
else
{
	switch (args[0])
	{
		case &quot;apilist&quot;:
			List<string> lst = trac.ListMethods();
			foreach (string name in lst)
			{
				Console.WriteLine(name);
			}
	}
}

■実行結果

D:\work\blog\src\trac\TracTools\TracPost\bin\Debug>tracpost apilist
system.multicall
system.listMethods
system.methodHelp
system.methodSignature
system.getAPIVersion
ticket.query
ticket.getRecentChanges
ticket.getAvailableActions
ticket.getActions
ticket.get
ticket.create
ticket.update
ticket.delete
ticket.changeLog
ticket.listAttachments
ticket.getAttachment
ticket.putAttachment
ticket.deleteAttachment
ticket.getTicketFields
ticket.status.getAll
ticket.status.get
ticket.status.delete
ticket.status.create
ticket.status.update
ticket.component.getAll
ticket.component.get
ticket.component.delete
ticket.component.create
ticket.component.update
ticket.version.getAll
ticket.version.get
ticket.version.delete
ticket.version.create
ticket.version.update
ticket.milestone.getAll
ticket.milestone.get
ticket.milestone.delete
ticket.milestone.create
ticket.milestone.update
ticket.type.getAll
ticket.type.get
ticket.type.delete
ticket.type.create
ticket.type.update
ticket.resolution.getAll
ticket.resolution.get
ticket.resolution.delete
ticket.resolution.create
ticket.resolution.update
ticket.priority.getAll
ticket.priority.get
ticket.priority.delete
ticket.priority.create
ticket.priority.update
ticket.severity.getAll
ticket.severity.get
ticket.severity.delete
ticket.severity.create
ticket.severity.update
wiki.getRecentChanges
wiki.getRPCVersionSupported
wiki.getPage
wiki.getPageVersion
wiki.getPageHTML
wiki.getPageHTMLVersion
wiki.getAllPages
wiki.getPageInfo
wiki.getPageInfoVersion
wiki.putPage
wiki.listAttachments
wiki.getAttachment
wiki.putAttachment
wiki.putAttachmentEx
wiki.deletePage
wiki.deleteAttachment
wiki.listLinks
wiki.wikiToHtml
search.getSearchFilters
search.performSearch

D:\work\blog\src\trac\TracTools\TracPost\bin\Debug>

ひとまず、Trac に xml-rpc 経由で問合せができるところまで確認OKと。

カテゴリー: C#, Trac | Trac にダイジェスト認証で接続する はコメントを受け付けていません

Trac に HttpWebRequest で接続するときは、UserAgent を設定する

久々に Trac Lighting をインストールして、ひとりプロジェクト管理をしようかなぁと思い

どうせならば、wordpress への投稿ツールを作った時と同じように XML-RPC 経由が良かろうといことで。

.NET(C#)でXML-RPCを使ってブログに投稿する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/1284

Trac への XML-RPC 投稿の場合は、authenticated に XML_RPC の権限を与えて、投稿時には Digest 認証で接続、ってな感じです。このダイジェスト認証なのですが、昨日の夜中に3時間ほどハマりました。

結果から言えば、こんな感じ。

string url = "http://localhost:8000/trac/gokui-ios5";
string userName = "masuda";
string password = "masuda";
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url);
req.UserAgent = "trac-post"; // User-Agent が必須★2
req.Credentials = new NetworkCredential(userName, password); // 認証★1
HttpWebResponse res = (HttpWebResponse)req.GetResponse();
StreamReader sr = new StreamReader(res.GetResponseStream());
string txt = sr.ReadToEnd();
Debug.Print(txt);

まずは、★1 のところでダイジェスト認証をします。HttpWebRequest クラスを使うと、ベーシック認証とダイジェスト認証が使えるそうなのですが、この区別は自動的に行われるみたいです。★1 の行をコメントアウトすると、401 のエラーが帰ります。認証を要求しているのだが、ユーザー名、パスワードを送らなかったので、「許可されていません」ってな具合です。

上記は、google で検索して msdn に行きあたると、なんとかなるのですが、★2 の User-Agent の件でハマりました。
この行をコメントアウトすると、500 内部サーバーエラーが発生します。

これが良くわからなくて、なんだろう…と思っていたのですが、Apache のほうで User-Agnet がないのでエラーを返しているみたいなんですね…IIS サーバーのほうでも、ローカルにある IIS 7.0 では User-Agent がなくても大丈夫なのですが、microsoft.com に HttpWebRequest クラスを使って接続すると、同じように内部サーバーエラーが返ります。多分、サーバーで動作しているプログラムの関係かと。

なので、

req.UserAgent = "trac-post";

のように、UserAgent プロパティに設定を適当に設定します。ブラウザを偽装してもよいし、どうせ、XML-RPC なので、ここは適当に設定すればよいでしょう。

これで接続確認だけはできたので、wordpress の時と同じように、http://www.xml-rpc.net/ を使ってチケットを投稿できるようにするか、自前で XML を構築するかを思案。

カテゴリー: C# | 1件のコメント