2009年9月27日日曜日

UserControl の Load イベントハンドラーが呼ばれない

.NET Framework2.0 SP2、.NET Framework3.5 SP1 で試したところ
UserControl に WebBrowser コントロールを貼り付けると、
Load イベントハンドラーが呼ばれません。

private void UserControl1_Load(object sender, EventArgs e)
{
    MessageBox.Show("呼ばれない orz");
}


このデリゲート(UserControl1_Load) を呼ぶのは、
ベースクラスの OnLoad() メソッドです。
というわけで、OnLoad() が呼ばれないのかな?と思い、
オーバーライドしてブレークを張ったところ、ちゃんと呼ばれています。

OnLoad() で追加したイベントハンドラーを呼ばれないなんてことがあるの?
など思いつつ Reflector で UserControl.OnLoad() を覗いてみました。
実装は次のようになっています。

protected virtual void OnLoad(EventArgs e)
{
  EventHandler handler = (EventHandler)base.Events[EVENT_LOAD];
  if (handler != null)
  {
    handler(this, e);
  }
}

実際、OnLoad() でブレイクして
base.Events[EVENT_LOAD] をウォッチしてみると
やっぱり null になっていました。
これで、イベントハンドラーが呼ばれないのも納得です。

だって呼んでいないんだもの (T ^ T)

では、なぜ呼ばれないか?
ブレイク張ってみると一目瞭然。張るところは当然

1.) this.Load += new System.EventHandler(this.UserControl1_Load);
2.) UserControl1.OnLoad()

UserControl に WebBrowser を貼り付けなければ
1 -> 2 と正しい順序で動きます。
一方、WebBrowser を張ると、2 -> 1 の順番で動きます。
どうやら this.Load += ... より前に
OnLoad() を呼んでしまう処理があるようです。
InitializeComponent() の先頭でブレイクし、
ステップ実行していくと・・・ありました。

this.Controls.Add(this.webBrowser1);
この後、UserControl1.OnLoad() が実行されてしまいました。

Refrelctor で今度は ControlCollection.Add() メソッドを覗いてみました。

public virtual void Add(Control value)
{
  if (value != null)
  {
    if (value.GetTopLevel())
    {
      throw new ArgumentException(SR.GetString("TopLevelControlAdd"));
    }
    if (this.owner.CreateThreadId != value.CreateThreadId)
    {
      throw new ArgumentException(SR.GetString("AddDifferentThreads"));
    }
    Control.CheckParentingCycle(this.owner, value);
    if (value.parent == this.owner)
    {
      value.SendToBack();
    }
    else
    {
      if (value.parent != null)
      {
        value.parent.Controls.Remove(value);
      }
      base.InnerList.Add(value);
      if (value.tabIndex == -1)
      {
        int num = 0;
        for (int i = 0; i < (this.Count - 1); i++)
        {
          int tabIndex = this[i].TabIndex;
          if (num <= tabIndex)
          {
            num = tabIndex + 1;
          }
        }
        value.tabIndex = num;
      }
      this.owner.SuspendLayout();
      try
      {
        Control parent = value.parent;
        try
        {
          value.AssignParent(this.owner);
        }
        finally
        {
          if ((parent != value.parent) && ((this.owner.state & 1) != 0))
          {
            value.SetParentHandle(this.owner.InternalHandle);
            if (value.Visible)
            {
              value.CreateControl();
            }
          }
        }
        value.InitLayout();
      }
      finally
      {
        this.owner.ResumeLayout(false);
      }
      LayoutTransaction.DoLayout(this.owner, value, PropertyNames.Parent);
      this.owner.OnControlAdded(new ControlEventArgs(value));
    }
  }
}

先ほど、

このデリゲート(UserControl1_Load) を呼ぶのは、
ベースクラスの OnLoad() メソッドです。


なんて言いましたが、では OnLoad() はどこから呼ばれるか?
答えは、UserControl.OnCreateControl() です。
となると、なんとなく怪しいのがマーカーを入れた value.CreateControl() で、
Visible プロパティを見て分岐していることから
実際、以下のようにすると UserControl1_Load が呼ばれるようになります。
それならと試しに、WebBrowser を継承して
OnCreateControl() をオーバーライドし、
base.OnCreateControl() を呼ぶ直前に Visible = false とすると
UserControl1_Load は呼ばれませんでした。残念。。orz

this.webBrowser1.Visible = false;
this.Controls.Add(this.webBrowser1);
this.webBrowser1.Visible = true;

てっきり WebBrowser の OnCreateControl() から
コンテナコントロールの OnCreateControl() を呼んでいるのだと
思ったのですが、見当違いだったみたいです。

間違いなく ControlCollection.Add() のどこかで
コンテナコントロールの OnCreateControl() が呼ばれるはずなのですが
長くなりすぎたし、とりあえずは解決できているので良しとします。

2009年9月26日土曜日

非同期デリゲート

.net では Thread、タイマーなどマルチスレッドを実現する方法が
いくつか用意されています。
今回のデリゲートもそのうちのひとつで、
スレッドの優先順位など細かい制御はできないものの、
任意の型のパラメーターをいくつでも渡せたり、
戻り値が簡単に取れるのはなにかと使い勝手がよいと思います。

実際の使用例は以下の通りです。

// 何かするデリゲート
delegate string SomeDelegate();
private void button1_Click(object sender, EventArgs e)
{
    // デリゲートの実装
    SomeDelegate some = new SomeDelegate(delegate
    {
        // 5 秒後 "hoge" を返します。
        System.Threading.Thread.Sleep(5000);
        return "hoge";
    });

    // SomeDelegate を非同期に呼び出します。
    IAsyncResult ar = some.BeginInvoke(null, null);

    // SomeDelegate から戻り値を得ます。
    Console.WriteLine("returned : " + some.EndInvoke(ar));
}


ボタンを押して 5 秒後、出力ウィンドウに

returned : hoge

と出力されます。

ただし、上の例では EndInvoke() をメインスレッドで
呼び出すことになるので、デリゲートが実行されている間
せっかく非同期に呼び出したのにメインスレッドが停止してしまいます。
(今回の場合、フォームが固まります。)
できればこれは避けたいので、少し実装に手間が増えますが
コールバック関数を用意します。

{
    /* BeginInvoke() の直前まで同じなので省略 */

    // AsyncCallback デリゲート
    AsyncCallback callback = new AsyncCallback(some_Completed);

    // 第一引数にコールバックメソッドを指定します。
    IAsyncResult ar = some.BeginInvoke(callback, null);
}

// SomeDelgate の終了時にコールされます。
private void some_Completed(IAsyncResult async)
{
    // AsyncResult にキャストします。
    AsyncResult asyncResult = (AsyncResult)async;

    // AsyncDelegate で SomeDelegate のインスタンスが取得できます。
    SomeDelegate some = (SomeDelegate)asyncResult.AsyncDelegate;
    Console.WriteLine("returned : " + some.EndInvoke(async));
}


メインスレッドでデリゲートの完了を待たないので
処理自体は先ほどと同じですが、フォームが固まることはありません。

最後になりましたが、デリゲート自体は ThreadPool を利用した技術なので
同時実行数には制限があります。
場合によってはデッドロックが発生することもあるそうなのですが、
特に必要に迫られたことがないのでこの辺あんまり調べていないです。
まぁそんな依存のある非同期処理ならコールバックで
次の非同期処理を BeginInvoke すればよいかと思います。

2009年9月15日火曜日

ディープコピー

オブジェクトをディープコピーする方法です。
コピーする型には SerializableAttribute 属性をつける必要があります。
コピーが不要なフィールドには NonSerializedAttribute 属性をつけることで
シリアライズの対象から除外されます。


[SerializableAttribute()]
public class SerializableClass
{
  public int SerializableField;

  // このフィールドはコピーされません。
  [NonSerializedAttribute()]
  public int NonSerializableField;

  public SerializableClass Clone()
  {
    object clone;
    using (MemoryStream stream = new MemoryStream())
    {
      BinaryFormatter formatter = new BinaryFormatter();
      formatter.Serialize(stream, this);
      stream.Position = 0;
      clone = formatter.Deserialize(stream);
    }
    return clone as SerializableClass;
  }
}

使いかたは以下のとおりです。

SerializableClass serializable = new SerializableClass();
serializable.SerializableField = 10;
serializable.NonSerializableField = 100;

SerializableClass clone = serializable.Clone();

// i には 10 が代入されます。
int i = clone.SerializableField;
// j には 0 が代入されます。(コピーされません)
int j = clone.NonSerializableField;


もちろん clone オブジェクトのプロパティを変更しても、
コピー元の serializable オブジェクトのプロパティは変更されません。

いろいろな型のクローンが必要なら次のようなヘルパークラスを用意すると
いいかもしれません。

public class CloneHelper
{
  public static T Clone<T>(T target)
  {
    object clone = null;
    using (MemoryStream stream = new MemoryStream())
    {
      BinaryFormatter formatter = new BinaryFormatter();
      formatter.Serialize(stream, target);
      stream.Position = 0;
      clone = formatter.Deserialize(stream);
    }
    return (T)clone;
  }
}

次のようにクローンメソッドを定義します。

[SerializableAttribute()]
public class SerializableClass
{
  public SerializableClass Clone()
  {
    return CloneHelper.Clone<serializableclass>(this);
  }
}

2009年9月14日月曜日

HTML ソースを見る

C# とは関係ないですが、
指定した URL の HTML ソースを見るスクリプト (WSH) です。

(*) Excel, Firefox がインストールされている必要があります。


try {
    var excel = WScript.CreateObject("Excel.Application");
    var result = excel.InputBox("source viewer", "URL", "http://");
    excel.Quit();

    // Firefox のパスです。ご自身の環境に合わせて書き換えてください。
    var browser = "C:\\Program Files\\Mozilla Firefox\\firefox.exe";
    var shell= WScript.CreateObject("WScript.Shell");
    shell.Run("\"" + browser + "\" -url " + "view-source:" + result);
} catch (e) {
}

2009年9月10日木曜日

拡張メソッド

C#3.0 から既存の型のインスタンスメソッドのように振舞う
拡張メソッドが追加されました。
使い方の一例として、String クラスにメソッドを追加してみます。

public static class StringExtensions
{
    // 指定された String オブジェクトが null
    // または Empty 文字列であるかどうかを示します。
    public static bool IsNullOrEmpty(this String s)
    {
        return String.IsNullOrEmpty(s);
    }
}

文字列の null or Empty を評価するメソッドです。
実際の呼び出し方法はこのようになります。
(StringExtensions を定義した namespace をインポートしてください。)

string s = null;
bool isNullOrEmpty = s.IsNullOrEmpty();

一見、NullReferenceException がスローされそうですが、
isNullOrEmpty 変数には true が入ります。

今度はよく使用する String.Format(String s, params object[] args) を
追加してみます。

public static class StringExtensions
{
    // String の書式項目を、指定した配列内の対応する Object インスタンスの
    // 値と等価のテキストに置換します。
    public static string Format(this String s, params object[] args)
    {
        return String.Format(s, args);
    }
}


string s = "{0} {1}".("A", "B");

変数 s には "A B" が格納されることを期待しましたが、
実際には、

インスタンス参照でメンバ 'string.Format(string, object)' に
アクセスできません。代わりに型名を使用してください。


とビルドエラーになってしまいます。

msdn によると、既存のメソッドと同じシグネチャを持つ拡張メソッドを
定義しても既存のメソッドの方が優先的にバインドされます (拡張メソッドは呼ばれません)。

参考
http://msdn.microsoft.com/ja-jp/library/bb383977.aspx
拡張メソッド (C# プログラミング ガイド)


つまり、拡張メソッドを追加した型に将来改変が加わり
同じシグネチャをもつメソッドが定義された瞬間から
拡張メソッドが呼ばれなくなってしまう危険性があるということになります。
当然ながら、同じ動きをしない可能性があります。

「ご利用は計画的に。」

2009年9月9日水曜日

イベントの実装

イベントを独自に実装するにはデリゲートを使います。
注意点は、イベントを発行する前に if (null != MyEvent) { ... } のように
呼び出し元でイベントハンドラーが追加されていることを
必ず確認することです。

public class EventClass
{
    public void DoSomething()
    {
        // イベントハンドラーの登録を確認します。
        if (null != MyEvent)
        {
            MyEvent(this, new MyEventArgs("Occurred MyEvent"));
        }
    }

    // イベントデリゲート
    public delegate void MyEventHandler(object sender, MyEventArgs e);
    // イベント
    public event MyEventHandler MyEvent;
}

// EventArgs を継承して独自のイベントデータを定義します。
public class MyEventArgs : EventArgs
{
    public MyEventArgs(string myVar)
    {
        this.myVar = myVar;
    }

    public string MyVar
    {
        get { return myVar; }
        set { myVar = value; }
    }

    private string myVar;
}

// イベントを受け取る側の実装
EventClass eventClass = new EventClass();
// イベントハンドラーを追加します。
eventClass.MyEvent += delegate(object sender, MyEventArgs e)
{
    MessageBox.Show(e.MyVar);
};

eventClass.DoSomething();








eventClass.MyEvent に追加したデリゲートが実行され、
メッセージボックスが表示されます。

ただ、今回のようにイベントの引数が object と EventArgs の派生クラスの
2 つで構成されるときは
System.EventHandler<TEventargs> デリゲートを使用し、
次のように書くこともできます。(1 ステップ減るだけですが)

public class EventClass
{
    public void DoSomething()
    {
        if (null != MyEvent)
        {
            MyEvent(this, new MyEventArgs("Occurred MyEvent"));
        }
    }

    public event EventHandler<MyEventArgs> MyEvent;
}

2009年9月8日火曜日

HTTP プロトコル違反

WebRequest を利用していると、

サーバーによってプロトコル違反が発生しました. Section=ResponseHeader Detail=CR の後には LF を指定しなければなりません。

と、WebException がスローされてしまいました。
ステータスは ServerProtocolViolation となっているので
HTTP プロトコル違反のようです。

調べたところ、古い CGI では改行を \n で記述されることが多く、
HTTP サーバーで \r\n に置換するべきところを \n のまま応答を返すので
厳密な .Net Framework の検証に引っかかっているとのこと。

というわけで、直すべきはサーバー側なのですが、
自前のサーバーに接続しているわけではなかったので
曲げて .Net Framework の方に少し馬鹿になってもらい、改行コードが \n でも
応答を受け入れるよう、アプリケーション構成ファイルに以下を追加。


  <system.net>
    <settings>
      <httpWebRequest useUnsafeHeaderParsing="true" />
    </settings>
  </system.net>


例外を出さず、処理を継続してくれました。

2009年9月7日月曜日

enum を型制約する

Generic クラスに enum の型制約をつけたところ、

class SomeGeneric<T> where T : enum
エラー : 型が必要です。
と、コンパイルエラーが出てしまいます。
それならと、System.Enum に変更してみました。

class SomeGeneric<T> where T : System.Enum
エラー : 制約は特殊クラス 'System.Enum' にはなれません。
これもだめだそうです。そうですか。残念です。

他にも System.Array 型、 System.Delegate 型、 System.ValueType 型で型制約することはできないようです。

参考
http://msdn.microsoft.com/ja-jp/library/56b2hk61.aspx
コンパイラ エラー CS0702


仕方がないので、System.Enum の定義にある IConvertible を型制約にして
とりあえず int 型にできればいいやという発想。


// System.Enum の定義
public abstract class Enum
    : ValueType, IComparable, IFormattable, IConvertible

class SomeGeneric where T : IConvertible
{
    protected T item = default(T);
    
    // enum : long で列挙されると困るので virtual 宣言
    public virtual T Item
    {
        get
        {
            return item;
        }
        set
        {
            if (item.ToInt32(null) != value.ToInt32(null))
            {
                // Do something
            }
        }
    }
}

2009年9月6日日曜日

WebBrowser 内のボタンをクリック

WebBrowser コントロールを使用していて
Windows アプリケーションからページ内のボタンをクリックする方法です。

以下のボタンが html で定義されているとして、
<input type = "button" id = "button1" onclick="javascript:foo();" />


HtmlElement element = null;
element = webBrowser1.Document.GetElementById("button1");
element.InvokeMember("click");


javascript の foo() が実行されます。
type="submit" でも可。

2009年9月5日土曜日

ストラテジーパターン

I/O が同じで、条件によって異なる処理を行いたい場合や、
将来的に変更または追加される可能性がある場合、
switch 文などの条件分岐に処理を埋め込んでいると、
変更が発生した際に他の条件へ意図しない影響が生じることがあります。

ストラテジーパターンは、インターフェースを介することで
アルゴリズムを動的に切り替えます。

まずは、ストラテジーパターンを使わない実装。

static void Main()
{
    string condition = "A";

    switch (condition)
    {
        case "A":
            Console.WriteLine("Executed condition A");
            break;
        case "B":
            Console.WriteLine("Executed condition B");
            break;
    }
}


続いて、ストラテジーパターンを適用した実装。

interface IStrategy {
    void Execute();
}

class StrategyA : IStrategy
{
    public void Execute()
    {
        Console.WriteLine("Called StrategyA.Execute()");
    }
}

class StrategyB : IStrategy
{
    public void Execute()
    {
        Console.WriteLine("Called StrategyB.Execute()");
    }
}

static void Main()
{
    GetStrategy("A").Execute();
    GetStrategy("B").Execute();
}

IStrategy GetStrategy(string condition)
{
    IStrategy result = null;

    switch (condition)
    {
        case "A":
            result = new StrategyA();
            break;
        case "B":
            result = new StrategyB();
            break;
        defalt:
            throw new ApplicationException();
    }

    return result;
}