小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="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 | 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 = "http://localhost:8000/trac/gokui-ios5/login/rpc";
			_trac.Setting.UserName = "masuda";
			_trac.Setting.Password = "masuda";

		}

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

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

		/// <summary>
		/// 指定IDのチケットを読み込む
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void buttonGet_Click(object sender, EventArgs e)
		{
			if (textBoxID.Text == "")
				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="sender"></param>
		/// <param name="e"></param>
		private void buttonPost_Click(object sender, EventArgs e)
		{
			if (textBoxID.Text == "")
			{
				// 新規登録
				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="sender"></param>
		/// <param name="e"></param>
		private void buttonDelete_Click(object sender, EventArgs e)
		{
			if (_ti == null)
				return;
			_trac.TicketDelete(_ti.ID);
			textBoxID.Text = "";
			textBoxSummary.Text = "";
			textBoxRepoter.Text = _Repoter;
			textBoxOwner.Text = "someone";
			textBoxDescription.Text = "";
			textBoxAction.Text = "";
			textBoxActionValue.Text = "";
			textBoxID.Enabled = true;
			_ti = null;
		}

		/// <summary>
		/// アクションを更新
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></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 = "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 object[] TicketGetActions(int id)
		{
			ITrac proxy = CreateProxy();
			object[] res = proxy.TicketGetActions(id);

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

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

		/// <summary>
		/// チケットを新規作成
		/// </summary>
		/// <param name="ti"></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="ti"></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="ti"></param>
		/// <returns></returns>
		public Ticket TicketUpdate(Ticket ti, string act)
		{
			ITrac proxy = CreateProxy();
			XmlRpcStruct st = ti.Attributes();
			st.Add("action", act);
			object[] res = proxy.TicketUpdate(ti.ID, ti.Comment, st);
			return new Ticket(res);
		}
		/// <summary>
		/// チケットを削除
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public int TicketDelete(int id)
		{
			ITrac proxy = CreateProxy();
			int res = proxy.TicketDelete(id);
			return res;
		}
	}

	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.delete")]
		int TicketDelete(int id);
		[XmlRpcMethod("ticket.getActions")]
		object[] TicketGetActions(int id);
	}

	/// <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;
		}
	}
}

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

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

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

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

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

さて、このコーディングの仕方ですが、順序として「クラス」→「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 = "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;
	}
}

実際は、下記のようなプロキシのクラスを作るのですが、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 = "http://localhost:8000/trac/gokui-ios5/login/rpc";
trac.Setting.UserName = "masuda";
trac.Setting.Password = "masuda";

if (args.Length == 0)
{
	Usage();
	return;
}
else
{
	switch (args[0])
	{
		case "apilist":
			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件のコメント

Active Directory は DirectoryEntry を使って検索する

.NET で、ドメインサーバーにある情報を検索するには、3 つのクラスを駆使します。

  • DirectoryEntry クラス: エントリそのもの
  • DirectorySearcher クラス: LDAP クエリで検索
  • SearchResult クラス: DirectorySearcher で検索した結果

あとは、DirectoryEntry オブジェクトの Properties コレクションを使えば、なんとかなるのですが…結構、これが慣れるまでが大変なので、メモがてら公開しておきます。

# 事情があって、コードは VB で。

■ドメイン構成と問題

ドメイン構成は、下記のようになっています。

訳あって、ドメインサーバーが2つあります。通常、ログインするほうは、plan.local ドメインなのですが、グループの設定やらなにやらがあるのは、moonmile.local ドメインのほうなのです。まぁ、通常業務のセキュリティ(文書閲覧とか)は plan.local ドメインで行っていて、アプリケーション絡みのややこしいセキュリティ関係は moonmile.local に閉じ込めた、と考えてください。

ここで、tomoaki@plan.local のユーザーがログインしているときに、GRP001 などのグループに属しているか?をチェックする、ことになります。

普通ならば、plan.local のほうにグループを作ればいいのですが…そこは業務的な制限です。

■ユーザーとグループの設定

実験的に、windows server 2008 R2 を使って、設定しています。

tomoaki@plan.local ユーザーを、どのようにして moonmile.local のほうに潜り込ませるかというと、tomoaki@plan.local の SID を使ったユーザーを moonmile.local に作成します。

これを moonmile.local ドメイン内で検索して、グループに入っているかどうかをチェックしようという仕組みです。
ForeignSecurityPrincipals のほうに入れているのは、SID を公開しているか、一応、ってことですね。本来ならば、moonmile.local と plan.local の SID を同じものにすれば話は簡単なのですが、作り方が分からない(苦笑)ので、別々の SID になります。


SID 自体をユーザー名にしていまいます。windows server 2008 R2 だと、SID の長さのままだと後ろのほうが切れてしまうので、実際に検索するのは表示名(displayName)になります。

■実験開始

少しずつ作っている/作ったので、ボタンが4つあります。

  • AD 検索(全検索): ひとまず、全検索してみる。
  • ForeignSecurityPrincipals: CN を指定して、絞ってみる。
  • ad-sv 問合せ: tomoaki ユーザーがログインするほうで、SID を取得します(実際は、ドメインのユーザー で、WindowsIdentity.GetCurrent().User のように SID が取得できます。
  • win2008-sv 問合せ: グループ名まで検索します。

■AD 全検索

単純に、AD の情報を取得します。

Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click

	Dim root As New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local", "masuda", "password")
	Dim se As New DirectorySearcher(root)

	ListBox1.Items.Clear()
	For Each res As SearchResult In se.FindAll
		Dim de As DirectoryEntry = res.GetDirectoryEntry
		Debug.Print(de.Path)
		ListBox1.Items.Add(de.Path)
	Next
End Sub

LDAP クエリを指定して、DirectoryEntry オブジェクトを作成します。ここでは、ドメインに入っていない状態なので、AD を検索可能なユーザー名とパスワードを指定していますが、既にドメインに入っている場合は、

Dim root As New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local")

のように指定しても OK です。また ドメインサーバーのフォワードがきちんと設定されていれば、

Dim root As New DirectoryEntry("LDAP://DC=moonmile,DC=local")

のように、サーバー名が無くても動作します。

全検索して、プログラム内で for/if しても良いのですが、ドメインサーバーに負担を掛けそうなので、もうちょっと工夫が必要です。

■CN などで検索を絞る

外部に公開している場合「CN=ForeignSecurityPrincipals」を付ければ少しは負担が軽くなります。

Private Sub Button2_Click(sender As System.Object, e As System.EventArgs) Handles Button2.Click
	Dim root As New DirectoryEntry("LDAP://win2008-sv/CN=ForeignSecurityPrincipals,DC=moonmile,DC=local", "masuda", "password")
	Dim obj = root.NativeObject
	Dim se As New DirectorySearcher(root)
	Dim de2 As DirectoryEntry = Nothing
	ListBox1.Items.Clear()
	For Each res As SearchResult In se.FindAll
		Dim de As DirectoryEntry = res.GetDirectoryEntry
		Debug.Print(de.Path)
		ListBox1.Items.Add(de.Path + " " + de.Properties("displayName").Value)
		If de.Path.IndexOf("増田 トニー") >= 0 Then
			de2 = de
		End If
	Next
	For Each nm In de2.Properties.PropertyNames
		Debug.Print(nm)
	Next
End Sub

表示名を調べるときは、de.Properties(“displayName”).Value のように、Properties プロパティを使えば OK です。ただし、目的のユーザーが指定できる場合は、DirectorySearcher で new するときにフィルターを指定したほうが良さそうです。

ちなみに、ここでデバッグ出力されるプロパティは、以下のものです。

objectClass
cn
sn
givenName
distinguishedName
instanceType
whenCreated
whenChanged
displayName
uSNCreated
memberOf
uSNChanged20111221
name
objectGUID
userAccountControl
badPwdCount
codePage
countryCode
badPasswordTime
lastLogoff
lastLogon
pwdLastSet
primaryGroupID
objectSid
accountExpires
logonCount
sAMAccountName
sAMAccountType
userPrincipalName
objectCategory
dSCorePropagationData
msDS-SupportedEncryptionTypes
nTSecurityDescriptor

ここでは、表示名(displayName)とSID(objectSid)を使います。あと、ユーザーが属しているグループを memberOf を使うと取得できます。

■フィルターを使ってみる

DirectorySearcher クラスで指定するフィルター(LDAPクエリ)を使って、カテゴリ(objectCategory)と名前で検索データを絞れます。

Private Sub Button3_Click(sender As System.Object, e As System.EventArgs) Handles Button3.Click
	Dim root As New DirectoryEntry("LDAP://ad-sv/DC=plan,DC=local", "tomoaki", "password")
	Dim obj = root.NativeObject
	Dim filter As String = "(&(objectCategory=User)(name=tomoaki))"
	Dim se As New DirectorySearcher(root, filter)
	Dim de As DirectoryEntry = se.FindOne.GetDirectoryEntry

	ListBox1.Items.Clear()
	For Each nm In de.Properties.PropertyNames
		Debug.Print(nm)
		Dim s As String = String.Format("{0}={1}", nm, de.Properties(nm).Value)
		ListBox1.Items.Add(s)
	Next

	Dim sid As String = SidToStringSid(
	 CType(de.Properties("objectSid").Value, Byte()))
	Debug.Print(sid)

End Sub

Declare Auto Function ConvertSidToStringSid Lib "advapi32.dll" (ByVal pSID() As Byte, _
	ByRef ptrSid As IntPtr) As Boolean
Private Function SidToStringSid(ByRef bytes As Byte()) As String

	Dim psid As IntPtr = Nothing
	Dim sid As String = ""
	ConvertSidToStringSid(bytes, psid)
	sid = System.Runtime.InteropServices.Marshal.PtrToStringAuto(psid)
	Return sid

End Function

あと、おまけですが、objectSid で取得するデータは byte 型の配列なのでちょっと扱いづらいのです。「S-…」のような文字列で使っていきたいので、変換関数を作ります。

ちなみに、CType(de.Properties(“objectSid”).Value, Byte()) のところが非常に遅いのですよね…CType を使って Byte 配列にするところが遅いらしい。DirectCast を使ってみたのですが、スピードはさほど変わらないので、妙なことになっているのかもしれません。このあたりは、後で調べる…ハズ。

■属しているグループの検索

ドメインにログオンしているユーザーの SID は WindowsIdentity.GetCurrent.User で取得できるので、最初の「ad-sv で “tomoaki” を検索」部分は不要になります。

Private Sub Button4_Click(sender As System.Object, e As System.EventArgs) Handles Button4.Click

	' ad-sv で "tomoaki" を検索
	Dim root As New DirectoryEntry("LDAP://ad-sv/DC=plan,DC=local", "tomoaki", "password")
	Dim filter As String = "(&(objectCategory=User)(name=tomoaki))"
	Dim se As New DirectorySearcher(root, filter)
	Dim de As DirectoryEntry = se.FindOne.GetDirectoryEntry
	Dim bytes As Byte() = CType(de.Properties("objectSID").Value, Byte())
	Dim sid As String = SidToStringSid(bytes)

	' win2008-sv で sid で検索
	root = New DirectoryEntry("LDAP://win2008-sv/CN=ForeignSecurityPrincipals,DC=moonmile,DC=local", "masuda", "password")
	filter = String.Format("(&(objectCategory=User)(displayName={0}))", sid)
	se = New DirectorySearcher(root, filter)
	de = se.FindOne.GetDirectoryEntry
	Dim sid2 As String = SidToStringSid(CType(de.Properties("objectSID").Value, Byte()))

	ListBox1.Items.Add("SID1:" + sid)
	ListBox1.Items.Add("SID2:" + sid2)

	' 属しているグループを取得
	root = New DirectoryEntry("LDAP://win2008-sv/DC=moonmile,DC=local", "masuda", "password")
	Dim groups As List(Of DirectoryEntry) = GetGroups(root, de)
	For Each ent As DirectoryEntry In groups
		ListBox1.Items.Add(ent.Properties("name").Value)
	Next
End Sub

Private Function GetGroups(root As DirectoryEntry, de As DirectoryEntry) As List(Of DirectoryEntry)
	Dim lst As New List(Of DirectoryEntry)

	If de.Properties("memberOf").Value IsNot Nothing Then
		If de.Properties("memberOf").Value.GetType Is GetType(String) Then
			Dim grp As String = de.Properties("memberOf").Value
			Dim se As New DirectorySearcher(root, String.Format("(&(objectCategory=Group)(distinguishedName={0}))", grp))
			Dim ent As DirectoryEntry = se.FindOne.GetDirectoryEntry
			lst.Add(ent)
			lst.AddRange(GetGroups(root, ent))
		Else
			Dim groups As Object() = CType(de.Properties("memberOf").Value, Object())
			For Each grp As String In groups
				Dim se As New DirectorySearcher(root, String.Format("(&(objectCategory=Group)(distinguishedName={0}))", grp))
				Dim ent As DirectoryEntry = se.FindOne.GetDirectoryEntry
				lst.Add(ent)
				lst.AddRange(GetGroups(root, ent))
			Next
		End If
	End If

	Return lst
End Function

LDAP クエリを使って、表示名(displayName)の SID の一致を検索するわけです。その時の DirectoryEntry オブジェクトが、それぞれのグループに属しているので、memberOf を使って調べていきます。取得した DirectoryEntry の SID を表示させていますが、実はこれも不要です。

属しているグループは、再帰的に検索させています。これは、GRP001 が GRP001PA に属している場合、ユーザーが属しているグループとしては「GRP001,GRP001PA」のように、両方とも取得させたいためです。memberOf プロパティで取得するデータは、ややこしいことに、String 単体と object 配列の 2 種類が存在します。属しているグループが1つの場合は String 単体で、2 つ以上属している場合は、String 配列が返されるという….変な仕様のため、GetType でクラスを比較させています。

ここまで来ると、属しているグループの一覧が取得できるので、グループのエントリから name プロパティなどを使えば、どのグループに属しているかどうかは簡単に調べられます。

■パフォーマンスの問題

これを試しに実行すると結構待たされます。多分、LDAP クエリの作り方がまずいような気がするのですが、object 配列から byte 配列へのキャスト(ctype)も結構重いのです。

最後の例だと、

	se = New DirectorySearcher(root, filter) ★1
	de = se.FindOne.GetDirectoryEntry
	Dim sid2 As String = SidToStringSid(CType(de.Properties("objectSID").Value, Byte())) ★2

のように、★1 の検索と、★2 の byte 配列への変換で遅くなります。
ここでは、SID を表示させているだけなので、ここは削ってしまうと早くなります。

LDAP クエリの検索部分は、キャッシュを使うようにすれば早くなるんですかね…問合せなので多少は掛かってもよいのでしょうが、もうちょっとレスポンスが良いほうがいいなぁと。

— 補足 2011/12/22

byte 配列のところ、以下のように分解すると、

Dim obj As Object = de.Properties("objectSid").Value ★ここで遅くなっている
Dim bytes As Byte() = CType(obj, Byte())
Dim sid As String = SidToStringSid(bytes)

どうやら、Properties にアクセスして値を拾ってくるところが重たいようです。byte 配列は関係ないですね。
キャッシュを有効にするとかで、スピードがあがる?

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

速効で Active Directory の環境を試してみる

ざっと作ってみたので、メモとして。
最終的な目的は、ドメインサーバーが2台(何故にw)とクライアントが1台という構成にする予定。

  • ドメインのほうは、「moonmile.local」と「plan.local」という2つのドメインが別々にある。
  • クライアントのほうは、どちらのドメインのアカウントも持っている。

という不思議な構成。まぁ、現実にあった構成。

■ドメインサーバーの構築

  1. windows server 2008 R2 をインストールする
  2. ライセンス認証をして、リモートデスクトップで動かせるようにする。
  3. リモートデスクトップ用にファイアウォールを切る(本当は、きちんと穴をあけるべき)
  4. 「Administrator」以外のユーザーを作成しておく。ここで作成したユーザーが、そのままドメイン内のアカウントになるので便利。
  5. DNS サーバーも兼ねるため、固定 IP を振っておく。
  6. 役割の追加で、「Active Directory ドメインサービス」をインストールする。
  7. 同時に DNS サーバーもインストールする。
  8. サーバーマネージャーの「Active Directory ドメインサービス」→「Acitve Directory ユーザーとコンピューター」でドメインを作成する。
  9. 「Computers」にあらかじめ、クライアントのコンピュータをいれておく。ここでは「vs2010」になっている。
  10. 「Users」を確認しておく

これで、ドメインサーバー側はおしまい。

■クライアントの構築

  1. VMWare で Windows 7 をインストール。お試しのドメインサーバーしか参照しないので、新たにクライアントは作成したほうが便利。
  2. 優先 DNS を設定する。
  3. コントロールパネルから、「ネットワークの状態とタスクの表示」を開いて「ローカルエリア接続」を選択して、優先 DNS サーバーで、ドメインサーバー兼 DNS サーバーの固定 IP を指定する。



  4. マイコンピュータのプロパティから「設定の変更」をクリックして、ドメインに参加させる。

これでクライアントの設定は完了

■クライアントからドメインにログイン

ドメインに参加すると、windows 7 のログイン画面が下記のようになって、

「ユーザーの切り替え」をすると、ドメインを指定してログインができます。

これを、moonmile.local ドメインと、plan.local ドメインのどちらでもログインができるようにすれば OK。

もうひとつのドメインサーバーは、VMware 上に立てるつもりなので、後で。

カテゴリー: 開発 | 速効で Active Directory の環境を試してみる はコメントを受け付けていません

DevFC.exe が暴走している場合は、プロセスを切っても良し。

昨日、ふとタスクマネージャを見ていると、DevFC.exe というプロセスが 200 MB ほどメモリを喰っている…ので何故?と思って調べてみると、

Azure の Compute Emulator のようですね。ver1.5 だとたまに暴走することがあるみたいです。現在、私の環境は、ver1.6 なので直っていないのか、このコンピューターはあまり再起動していないので、ver1.5 のものが動作したままなのか。

# 説明のところが空白なので、何かのウィルスかと思ってしまった。

なので、プロセスをいきなり切っても OK だし、インジケーターから「Windows Azure Simulator Monitor」を右クリックして「Showdwon Compute Emulator」を選択しても OK です。Visual Studio を使って Azure 開発をしていないときはプロセスを切っておいてもいいですね。デバッグ実行したときは自動的に起動されるので、落としておいても問題ありません。

ちなみに、Compute Emulator ってのは、こんなの。Visual Studio でデバッグ実行した時に、ローカルコンピューターでエミュレーションする機能です。起動時のログなんかが分かります。

カテゴリー: 開発, Azure | DevFC.exe が暴走している場合は、プロセスを切っても良し。 はコメントを受け付けていません

データ指向で画面設計をする手順

ここでいう「データ指向」というのは、古くはホストコンピューターの時代から連綿と続いているIBM 内の秘伝のタレ(かな?)のことで、昨今の O/R マッピングの話が出る以前の設計と実装手順のことです。20 年以上前ってことになるかなぁと。

  1. テーブル構造を作成する
  2. テーブルへアクセスするCRUDを記述する
  3. テーブルを連結して一覧する方法を記述する
  4. 画面を設計する

いきなり、テーブル構造(データ構造)から始めているのが「データ指向」の所以です。オブジェクト指向の場合は、どのデータが「存在するのか」を調べた後に、どのまとまり(クラス)にするのか、という整理の仕方(オブジェクト図からクラス図の設計の流れがそれ)をしますが、データ指向の場合は、まず、まとまったデータがいきなりあって、そこからスタートします。

そして、データ指向で実装をする場合には、後戻りをしませんッ!!! ここは重要です。データ指向をする場合には、最初のデータ構造ありきのところがあるので、逐一 CRUD を作るところから、完全なウォーターフォール方式(あるいは流れ作業方式)で作れ、完全に分業が可能な開発工程ができます。

■テーブル構造を作成する

アプリオリな話になりますが、データ構造をどうやって分析していたのか(いるのか?)は私には不明です。後には DFD を使ってデータ構造を作ったりするのですが(本来は、DFD はデータの前後の関係、データの加工手順を示すものなのですが)、それ以前はどうしていたのでしょうね?

なんとなく、まとまりがあるところから、なんとなく正規化をして、という流れで設計をしているのでしょうね。おそらく。

また、ホストコンピュータの時代からのデータ構造を流用し続けていくので、データ構造を新しく作ることはないと言っても過言ではありません。あらかじめ、データベースにテーブル構造があるところから、移行設計、追加設計をしていくのが普通です。

■テーブルへアクセスする CRUD を記述する

CRUD(Create, Read, Update, Delete)の表を、テーブル毎にすべて作ります。そして、CRUD する関数を全て最初に作ってしまいまいます。今でこそ、自動化なり、リフレクションを使って、なぞと小細工…いえ効率化をすることができるのですが、当時はちまちまとコーディングです。

基本はひとつのテーブルに対して、ひと揃いの CRUD を用意していきます。このあたり、いわゆる登録系の画面(いわゆる年金を登録する画面とかね)と、検索系の画面(いわゆる株価の動向を見る画面とかね)の二種類があって、ここで CRUD 表を作るのは、登録系の画面で使うものです。

検索する場合であっても、主キーのみで検索したり、外部キーで他のテーブルを参照したりするぐらいのリレーションを持っているものです。

これをひたすら設計/実装していきます。

■テーブルを連結して一覧する方法を記述する

いまでこそデータベースと BI はビジネス的に切っても切れない関係にありますが、かつてはデータを分析するのは別の機能でしたし、非常にお金のかかるものでした。なので、分析して帳票に出すというのは、結構手間が掛かりお金もかかったので、あまり機能的には豊富ではなかったのですね。
いまのように GUI でちょいちょいと検索項目を入れて、数分後には帳票ができるというものではありませんでした。プリンターも高かったし。

なので、帳票を出力する場合は、「事前に決めた検索項目」を使って出力するってのが普通だったし、それがデータ指向をするときの欠点にもなっています。

日次、月次のような決まりきった帳票を出すためのインターフェース(関数のロジック)を事前に決めることができるのですが、インタラクティブに GUI で検索項目を変える、というところにまでデータ指向の利点はでてきません。

なので、この部分は、現在データ指向でプログラミングする場合は、

  • 事前に分かっている検索結果、帳票しか出さない要件とする。
  • それ相応に、要件定義を練り込む

という制限が必要になります。

■画面を設計する

ここに来て、やっとユーザーが利用する画面を設計&実装していきます。
データベースへのアクセスについては、2 と 3 で作成した関数を必ず通すので、おのずから画面のスタイルは決まっています。

  • 更新や閲覧のためのテーブル構造そのままの画面
  • リレーション先は、別画面(あるいは子画面)
  • リレーションが必要な一覧は、帳票として出力

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

当然、ユーザーがあれこれと画面を渡り歩くことはできず、画面遷移もデータ構造に一致します。

このデータ指向での設計&実装での最大の利点は、

  • 機能数や画面数が事前に見積もりやすい(FP を利用しやすい)
  • 機能数が事前に分かるので、開発期間が見積しやすい
  • CRUD 表に従うので、標準的なプログラマだけでよい(スーパープログラマはいらない)

というところになります。

欠点としては、

  • 開発プロセスとして面白みがない(改善する余地がほとんどないので、効率化もできない)
  • 画面にユーザーの要望が入らない。
  • 分析系が複雑化すると破綻しやすい(事前の関数の数を見極めにくい)

というところです。

でも、非常に開発プロセスとしては安定しているので、大規模開発には向いています。
ただし、効率化できるところが少ないので、アジャイルプロセスのように「作業の省略」ができません。全てを綿密に作る(無駄も含めて)ことになるので、あとから見ると無駄な関数を大量に作る可能性があります。

時間があれば、後で図解でも up しますか。

カテゴリー: 設計 | データ指向で画面設計をする手順 はコメントを受け付けていません