2006年03月16日

COM クライアント実装の道程 for TaskScheduler その7

仕様を策定する人って絶対暇人ですよね。

前回の続きと言うことで、ITask に関するいくつかの実装を見ていきましょう。

まずは TaskScheduler クラスと同じく、Task クラスの基礎実装を。まあ ComInterfaceWrapper を継承して、内部で使用するインターフェイスを private プロパティにするだけですから同じですけど。ただ、コンストラクタは構成からして外部に公開する必要はないので、internal/Friend としておきます。ITask は単独で作成できますが、それは認めない方向で。

// C#
namespace HongliangSoft.Utilities.TaskSchedulers {
  public sealed class Task : ComInterfaceWrapper {
    private ITask ITask { get { return (ITask)base.Instance; } }
    internal Task(ITask iTask) : base(iTask) {
    }
  }
}

' VB.NET
Namespace HongliangSoft.Utilities.TaskSchedulers
  Public NotInheritable Class Task
    Inherits ComInterfaceWrapper
    Private ReadOnly Property ITask() As ITask
      Get
        Return DirectCast(MyBase.Instance, ITask)
      End Get
    End Property
    Friend Sub New(ByVal iTask As ITask)
      MyBase.New(iTask)
    End Sub
  End Class
End Namespace

あとはこれにメソッド・プロパティを作っていくだけですが、特にプロパティの方はほとんどが単純なものなので、そのままインターフェイスのメソッドを公開するのはちょっと……というのだけ書きましょう。

まず、GetMaxRunTime / SetMaxRunTime 。引数が Int32 です。実に分かりにくい。単位は分なのか秒なのか、そもそも回数なのではないか? 正解はミリ秒。このパラメータの意味は「実行される時間」ですから、.NET では TimeSpan 構造体で扱うべきだろうってことで、プロパティの型は TimeSpan と言うことにして、内部で変換することにしましょう。問題は 0xFFFFFFFF の時だけは無効であるという点ですが、これは負の TimeSpan 値ということにします。.NET 2.0 限定なら nullable も良いんですけどねー。

// C#
public TimeSpan MaxRunTime {
  get {
    const int infini = -1;
    int time;
    base.CheckHR(this.ITask.GetMaxRunTime(out time));
    if (time == infini)
      return new TimeSpan(infini);
    uint milli = unchecked((uint)time);
    return TimeSpan.FromMilliseconds(milli);
  }
  set {
    const int infini = -1;
    if (value < TimeSpan.Zero)
      base.CheckHR(this.ITask.SetMaxRunTime(infini));
    else {
      long time = (long)value.TotalMilliseconds;
      if (time > 0xffffffffL) {
        throw new ArgumentOutOfRangeException(
          "value", value, 
          "4294967295 ミリ秒(およそ 49 日 17 時間 2 分 47 秒) "
          + "以上に設定することはできません。");
      }
      base.CheckHR(this.ITask.SetMaxRunTime(unchecked((int)time)));
    }
  }
}

' VB.NET
Public Property MaxTunTime() As TimeSpan
  Get
    Const inifini As Integer = -1
    Dim time As Integer
    MyBase.CheckHR(Me.ITask.GetMaxRunTime(time))
    If time = inifini Then
      Return New TimeSpan(-1)
    End If
    Dim milli As Long = time
    If time < 0 Then
      milli += &H100000000L
    End If
    Return TimeSpan.FromMilliseconds(milli)
  End Get
  Set
    Const infini As Integer = -1
    If value.Ticks < 0 Then
      MyBase.CheckHR(Me.ITask.SetMaxRunTime(infini))
    Else
      Dim time As Long = CLng(Math.Floor(value.TotalMilliseconds))
      If time > &HffffffffL Then
        Throw New ArgumentOutOfRangeException( _
          "value", value, _
          "4294967295 ミリ秒(およそ 49 日 17 時間 2 分 47 秒) " _
          & "以上に設定することはできません。")
      End If
      If time >= &H80000000L Then
        time -= &H100000000L
      End If
      MyBase.CheckHR(Me.ITask.SetMaxRunTime(CInt(time))
    End If
  End Set
End Property

unchecked 構文がない VB.NET ではやはりどうしてもこの辺記述が面倒ですね。

あとは、GetIdleWait/SetIdleWait を紹介しましょう。このメソッドはそれぞれ二つずつ引数を取って、アイドル時間を取得・設定します。そのまま実装するならメソッド、しかも Get の方は最低一つは out(ByRef) を使うことになるでしょうが、こういう場合この二つをまとめて構造体にし、それをやりとりするプロパティと言うことにすればすっきりします。なお、全体に言えることですが、全体にタスクスケジューラ周りの COM インターフェイスは 16bit の値を要求することが多いです。では Int16 でいいかというと、Int16 では 32767 までしか扱えないため表現力不足。かといって UInt16 は CLS 非準拠ですし、VB7/7.1 からは非常に扱いにくくなります。ですから外に見せる値は大抵 Int32 で公開しています。値を取得・設定するのに分数を使うのと TimeSpan を使うのは、今回はどちらもありと思ったので両方プロパティで実装。構造体全部を書くのも面倒なので、TimeSpan と分数との変換を行うプロパティの実装を一つ書いてみましょう。

// C#
public struct IdleWait {
  private int idleMinutes;
  private int deadlineMinutes;
  public TimeSpan IdleTime {
    get { return TimeSpan.FromMinutes(this.idleMinutes); }
    set {
      long minutes = (long)value.TotalMinutes;
      if (minutes < 0 || minutes > 65535) {
        throw new ArgumentOutOfRangeException(
          "value", value,
          "マイナスの値、または 65535 分間を表す値よりも"
          + 大きい値にすることはできません。");
      }
      this.idleMinutes = (int)minutes;
    }
  }
  /* 略 */
}
' VB.NET
Public Structure IdleWait
  Private _idleMinutes As Integer
  Private _deadlineMinutes As Integer
  Public Property TimeSpan() As IdleTime
    Get
      Return TimeSpan.FromMinutes(Me._idleMinutes)
    End Get
    Set
      Dim minutes As Long = CLng(Math.Floor(value.TotalMinutes))
      If (minutes < 0) OrElse (minutes > 65535) Then
        Throw New ArgumentOutOfRangeException( _
          "value", value, _
          "マイナスの値、または 65535 分間を表す値よりも" _
          & 大きい値にすることはできません。")
      End If
      Me._idleMinutes = CInt(minutes)
    End Set
  End Property
  ' 略
End Structure

前述の通りこの構造体は分数での値そのまま出しているので、IdleWait プロパティは比較的単純な実装なんで省略。まあやっぱり VB.NET では面倒なところがありますが。

メソッドも二つ紹介しましょう。まず CreateTrigger です。前々回述べたように、ITask.CreateTrigger は作成した ITaskTrigger を返し、そして ITaskTrigger.SetTrigger にてTASK_TIGGER 構造体をセットすることでトリガをセットしました。ちなみにトリガを変更した後 IPersistFile.Save メソッドを使用しないと保存されません。保存は必ずしもトリガを設定したときにするとは限らないので取り入れず、Task.CreateTrigger では ITask.CreateTrigger と ITaskTrigger.SetTrigger の二つをまとめてこなすメソッドにすることにしましょう。

// C#
public int CreateTrigger(TaskTrigger trigger) {
  short triggerIndex;
  ITaskTrigger iTrigger;
  base.CheckHR(this.ITask.CreateTrigger(out triggerIndex, out iTrigger));
  try {
    base.CheckHR(iTrigger.SetTrigger(trigger));
  }
  finally {
    if (iTrigger != null)
      Marshal.ReleaseComObject(iTrigger);
  }
  return triggerIndex;
}
Public Function CreateTrigger(ByVal trigger As TaskTrigger) As Integer
  Dim triggerIndex As Short
  Dim iTrigger As ITaskTrigger
  MyBase.CheckHR(Me.ITask.CreateTrigger(triggerIndex, iTrigger))
  Try
    MyBase.CheckHR(iTrigger.SetTrigger(trigger))
  Finally
    If Not(iTrigger Is Nothing) Then
      Marshal.ReleaseComObject(iTrigger)
    End If
  End Try
  Return triggerIndex
End Function

TaskTrigger は次回解説する予定です。さて、特徴らしい特徴と言えば Marshal.ReleaseComObject くらいでしょうか? 返値の Int32 は追加されたトリガのインデックスですが、これがまた使いづらい値でして。途中のトリガを Delete するとその後のそれぞれのトリガのインデックスが自動的に一つずれるんで、保存しとくわけにはいきません。

もう一つ、GetRunTimes の方は、トリガの起動予定日時の配列を返すメソッドです。ITask.GetRuntimes は返値を受ける分を除いて三つパラメータを要求します。取得の先頭日時、取得の最終日時、最大取得数です。しかし毎回これらを指定するのは面倒ですし、オーバーロードで引数を手抜きできるようにしましょう。基本は「今後どういうスケジュールなのか」でしょうから、できる限り先頭日時を省略するオーバーロードが基本でしょうか。また、ITask.GetRunTimes は SYSTEM_TIME(SystemTimeClass)の配列を返してきますが、これは DateTime に変換しといた方が直感的でしょう。

// C#
// 一度に取得できる要素数の上限
public static readonly int RunTimesMax = 1440;
// 現在から将来にわたって、最大取得数を指定して予定を取得する
public DateTime[] GetRunTimes(int maxCount) {
  return this.GetRunTimes(DateTime.Now, DateTime.MaxValue, maxCount);
}
// 現在から将来にわたって、指定日時までの予定を取得する
public DateTime[] GetRunTimes(DateTime endTime) {
  return this.GetRunTimes(DateTime.Now, endTime);
}
// 指定した日時から日時までの間の予定を取得する
public DateTime[] GetRunTimes(DateTime beginTime, DateTime endTime) {
  return this.GetRunTimes(beginTime, endTime, Task.RunTimesMax);
}
// 全部の値を指定して予定を取得する
public DateTime[] GetRunTimes(DateTime beginTime,
                              DateTime endTime, int maxCount) {
  if (maxCount < 1 || maxCount > Task.RunTimesMax) {
    throw new ArgumentOutOfRangeException(
      "maxCount", maxCount,
      "0 以下、または Task.RunTimesMax "
      + より大きい値を指定することはできません。");
  }
  SystemTimeClass begin = new SystemTimeClass(beginTime);
  SystemTimeClass end = new SystemTimeClass(endTime);
  short count = (short)maxCount;
  IntPtr runTimes;
  HResult result = this.ITask.GetRunTimes(begin, end,
                                          ref count, out runTimes);
  if (result != HResult.OK && result != HResult.False) {
    // 致命的なエラーをチェック。
    CheckHR(result);
    // 致命的ではない場合(トリガを持ってないとか)
    return new DateTime[0];
  }
#if !V10 && !V11
  List<DateTime> dates = new List<DateTime>();
#else
  ArrayList dates = new ArrayList();
#endif
  int size = Marshal.SizeOf(typeof(SystemTimeClass));
  using (CoTaskMem array = runTimes) {
    for (int i = 0; i < count; i++) {
      SystemTimeClass runs;
#if !V10 && !V11
      runs = array.ReadStructure<SystemTimeClass>(size * i);
#else
      runs = mem.ReadStructure(typeof(SystemTimeClass),
                               size * i) as SystemTimeClass;
#endif
      if (! runs.IsInvalid) {
        dates.Add(runs.ToDateTime());
      }
    }
  }
#if !V10 && !V11
  return dates.ToArray();
#else
  return (DateTime[])dates.ToArray(typeof(DateTime));
#endif
}

' VB.NET
' 一度に取得できる要素数の上限
Public Shared ReadOnly RunTimesMax As Integer = 1440
' 現在から将来にわたって、最大取得数を指定して予定を取得する
Public Function GetRunTimes(ByVal maxCount As Integer) As DateTime()
  Return Me.GetRunTimes(DateTime.Now, DateTime.MaxValue, maxCount)
End Function
' 現在から将来にわたって、指定日時までの予定を取得する
Public Function GetRunTimes(ByVal endTime As DateTime) As DateTime()
  Return Me.GetRunTimes(DateTime.Now, endTime)
End Function
' 指定した日時から日時までの間の予定を取得する
Public Function GetRunTimes(ByVal beginTime As DateTime,
                            ByVal endTime As DateTime) As DateTime()
  Return Me.GetRunTimes(beginTime, endTime, Task.RunTimesMax)
End Function
' 全部の値を指定して予定を取得する
Public Function GetRunTimes(ByVal beginTime As DateTime,
                            ByVal endTime As DateTime,
                            ByVal maxCount As Integer) As DateTime()
  If maxCount < 1 OrElse maxCount > Task.RunTimesMax Then
    Throw New ArgumentOutOfRangeException( _
      "maxCount", maxCount, _
      "0 以下、または Task.RunTimesMax " _
      & "より大きい値を指定することはできません。")
  End If
  Dim begin As New SystemTimeClass(beginTime)
  Dim _end As New SystemTimeClass(endTime)
  Dim count As Short = CShort(maxCount)
  Dim runTimes As IntPtr
  Dim result As HResult = Me.ITask.GetRunTimes(begin, _end, count, runTimes)
  If Not(result = HResult.OK OrElse result = HResult.False) Then
    ' 致命的なエラーをチェック。
    CheckHR(result)
    ' 致命的ではない場合(トリガを持ってないとか)
    Return New DateTime(0){}
  End If
  Dim size As Integer = Marshal.SizeOf(GetType(SystemTimeClass))
#If Not(V = 10 OrElse V = 11) Then
  Dim dates As New List(Of DateTime)
  Using _array As New CoTaskMem(runTimes) {
    For i As Integer = 0 To count -1
      Dim runs As SystemTimeClass
      runs = _array.ReadStructure(Of SystemTimeClass)(size * i)
      If Not(runs.IsInvalid) Then
        dates.Add(runs.ToDateTime())
      End If
    Next
  End Using
  Return dates.ToArray()
#Else
  Dim dates As New ArrayList()
  Dim i As Integer
  Dim _array As CoTaskMem
  Try
    _array = New CoTaskMem(runTimes)
    For i = 0 To count - 1
      Dim runs As SystemTimeClass
      runs = DirectCast(_array.ReadStructure(GetType(SystemTimeClass), _
                                             size * i), _
                        SystemTimeClass)
      If Not(runs.IsInvalid) Then
        dates.Add(runs.ToDateTime())
      End If
    Next
  Finally
    _array.Free()
  End Try
  Return DirectCast(dates.ToArray(GetType(DateTime)), DateTime())
#End If
End Function

次回は TASK_TRIGGER とそのラッパ TaskTrigger ですが……だらだら解説を続けるのもそろそろやめて、どうラップしたかを簡単に解説するだけにしましょう。

posted by Hongliang at 22:29| Comment(0) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.seesaa.jp/tb/14930707

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。