電子文書管理システムの構築例に関するWindows Workflow Foundationの概要[パート2]

ワークフローシステムの開発は、小さなチームにとって不可能な作業ですか?





以前の投稿で、このテクノロジを適用するのが最適な場合のWFのアーキテクチャ上の機能を調べました。 このパートでは、特定のプロジェクトでのWFの適用方法を説明します。







そのため、タスクは電子文書管理システムを実装することです。









便宜上、この部分をいくつかのブロックに分割します。







ドキュメントのリストを維持する



このモジュールは、ASP.NET MVCテクノロジーを使用して実装されました。 さまざまな種類のドキュメントを作成および編集し、それらのドキュメントでワークフローを開始できます。 将来参照できるようにここで指定しますが、詳細には検討しません。







次の一般的なインターフェイスをほぼ実装するドキュメントクラスがあると仮定します*:







/// <summary> ///   /// </summary> public interface IDocument { /// <summary> ///   /// </summary> int DocumentId { get; set; } /// <summary> ///   /// </summary> User Author { get; set; } /// <summary> ///    /// </summary> int AuthorId { get; set; } }
      
      





*以降、簡素化するために、製品コードの簡略バージョンが使用されます。







一般的な活動



ユーザーはドキュメントアクティビティを開始します(以降-一般アクティビティ)。 共有アクティビティには2つの入力引数があります。







 /// <summary> ///      /// </summary> public interface IDocumentWorkflow { /// <summary> ///   ( ) /// </summary> InArgument<int> DocumentId { get; set; } /// <summary> ///  ,   ( ) /// </summary> Inrgument<int> UserId { get; set; } }
      
      





、ここでDocumentIdはドキュメントキー、 UserIdはアクティビティを開始するユーザーのキーです。







したがって、一般的なアクティビティでは、オブジェクト全体ではなくドキュメントキーのみが回転します。これにより、将来ドキュメントクラスを変更する必要がある場合(プロパティを追加する場合など)に、保存されたワークフローの状態を復元する際の問題を回避できます。







さらに、一般アクティビティごとに、特定のメタデータセットが個別に保存されます:実行する特権、アクティビティを実行できるドキュメントの種類、 Dynamic LINQ-起動できるかどうかを確認するためのドキュメントへの式など。







全体的なアクティビティ(システムのカスタム部分)は、ワークフローデザイナーによってエンコードされ、XAMLとして保存されます。 以下でデザイナーの実装を検討します。







以下は、WFランタイムでアクティビティを起動するコードです。







 /// <summary> ///         /// </summary> /// <typeparam name="T">  </typeparam> /// <param name="documentId"> </param> /// <param name="userId"> </param> public static void StartDocumentWorkflow<T>(int documentId, int userId) where T: Activity, IDocumentWorkflow, new() { //              var wfApp = new WorkflowApplication(new T(), new Dictionary<string, object>() { { "DocumentId", documentid}, { "UserId", userId}}); //     wfApp.InstanceStore = new SqlWorkflowInstanceStore(ApplicationData.ConnectionString); wfApp.PersistableIdle = (e) => { // ,    –    return PersistableIdleAction.Unload; }; wfApp.Completed = (completeArg) => { //   }; wfApp.Aborted = (abortArg) => { //  }; wfApp.OnUnhandledException = (exceptionArg) => { //    return UnhandledExceptionAction.Abort; }; wfApp.Run(); }
      
      





ここで1つの点を明確にする必要があります。必要に応じてワークフローがその状態を維持できるようにするために、 プロパティインジェクションを使用してストレージクラスSqlWorkflowInstanceStoreのインスタンスがランタイムに渡されます。







ワークフローに引数を渡す



したがって、全体的なアクティビティは稼働しています。 しかし、ある時点で、仕事のためにシステムのユーザーからの追加データが必要になる場合があります。 このデータに基づいて、彼女のロジックに従って、彼女は実行ブランチを選択し、ドキュメントのプロパティを変更し、通知などのアクションを送信します。







したがって、このデータを説明する必要があります(以下-引数)。 2つのオプションがありました。 情報をワークフローに転送するために使用される1つのユニバーサルタイプ(キーと値のコレクション)を説明します。 しかし、厳密な型指定を利用して、動的にロードされたカスタム引数のライブラリを実装しました。 クラスはIAssignArgumentインターフェイスを実装します。







 /// <summary> ///        /// </summary> public interface IAssignArgument { /// <summary> ///  ,   /// </summary> int UserId { get; set; } } /// <summary> ///          /// </summary> public abstract class AssignArgumentBase : IAssignArgument { /// <summary> ///  ,   /// </summary> [DisplayName(" ")] [EditorBrowsable(EditorBrowsableState.Never)] public int UserId { get; set; } } [DisplayName("")] [Category("")] public sealed class Learn : AssignArgumentBase { } [DisplayName(" , , ")] [Category("")] public sealed class EnterNumberDateFile : AssignArgumentBase { [Required, DisplayName(" ")] public string Number {get; set;} [Required, DisplayName(" ")] public DateTime Date {get; set;} [FileId, Required, DisplayName("")] public int? FileId {get; set;} }
      
      





UserIdは、引数をワークフローに転送するユーザーのキーです。







次に、入力を待機しているアクティビティ自体に進みましょう。 これらの目的のために、 NativeActivity <T>クラスがアクティビティのベースライブラリで提供されます。Tはアクティビティの結果であり、引数です。 NativeActivityからクラスを継承します。







 [DisplayName(" ...")] [Category("")] [Designer("DocWorkflow.Activities.Designer.GenericActivityDesigner, DocWorkflow.Activities.Designer")] public class AssignDocumentActivity<T> : NativeActivity<T> where T : class, IAssignArgument, new() { /// <summary> ///    /// </summary> [DisplayName("")] public new OutArgument<T> Result { get { return base.Result; } set { base.Result = value; } } /// <summary> ///    /// </summary> [DisplayName("")] [RequiredArgument] public InArgument<int> DocumentId { get; set; } /// <summary> ///  ,     /// </summary> [DisplayName(" ")] [RequiredArgument] public InArgument<int> UserId { get; set; } /// <summary> /// 1-  (   ): /// -       ,     ; /// -  . /// </summary> protected override void Execute(NativeActivityContext context) { var bookmarkName = Guid.NewGuid().ToString(); using (var store = new DataStore()) { store.Add(new AssignedDocumentInfo() { UserId = context.GetValue(this.UserId), WorkflowInstanceId = context.WorkflowInstanceId.ToString(), ActivityInstanceId = context.ActivityInstanceId.ToString(), WorkflowType = context.GetExtension<WorkflowInstInfo>().GetProxy().WorkflowDefinition.GetType().FullName, ArgumentType = typeof(T).FullName, BookmarkName = bookmarkName, DocumentId = context.GetValue(this.DocumentId), ActivityName = this.DisplayName, AssignedDate = DateTime.Now }); } context.CreateBookmark(bookmarkName, new BookmarkCallback(this.Continue)); } /// <summary> /// 2-  (   ): /// -       ; /// -    . /// </summary> protected void Continue(NativeActivityContext context, Bookmark bookmark, object obj) { using (var store = new DataStore()) { foreach (var item in store.AssignedDocumentInfo.Where(aa => aa.WorkflowInstanceId == context.WorkflowInstanceId.ToString() && aa.ActivityInstanceId == context.ActivityInstanceId.ToString() && aa.UserId == context.GetValue(this.UserId) && aa.BookmarkName == bookmark.Name).ToArray()) { store.Remove(item); } } Result.Set(context, (T)obj); } }
      
      





ご覧のとおり、最初の段階のこのアクティビティは、リポジトリに状態を復元するために必要なデータを保存します(ドキュメントキー、タスクが割り当てられているユーザーキー、一般アクティビティのタイプ、引数タイプ、アクション名、回復用の「ブックマーク」)。 2番目の段階で、入力引数を受け取り、それを一般ストリームの本文で使用するために出力パラメーターに渡します。











次に、ドキュメントワークモジュールは、ユーザーに割り当てられたタスクを参照し、ワークフローの回復プロセスを開始するときに、引数の種類に基づいてビューを動的に生成します(もちろん、カスタムビューを設定することも可能です)ユーザーから引数を受け取り、それを一般アクティビティの復元に渡します。

















引数の厳密な型指定( 対極引数[“ Prop1”] )により、デザイナーでアクティビティを構築する段階で引数のプロパティへの呼び出しを検証できます。









型変換の規則に違反するストリームは開始されません。 つまり、エラーは実行段階ではなくコンパイル段階で発生します。







以下は、ワークフローを復元するためのコードです。







 public static void ResumeWorkflow<T>(int assignedDocumentInfoKey, T arg) where T: IAssignArgument { AssignedDocumentInfo assignedDocumentInfo = null; using (var store = new DataStore()) { //     assignedDocumentInfo = store.AssignedDocumentInfo .Where(aa => aa.AssignedDocumentInfoId=assignedDocumentInfoKey).First(); } var activity = (Activity) Activator.CreateInstance(Type.GetType(assignedDocumentInfo.WorkflowType)); WorkflowApplication wfApp = new WorkflowApplication(activity); wfApp.InstanceStore = new SqlWorkflowInstanceStore(ApplicationData.ConnectionString); wfApp.PersistableIdle = (e) => { return PersistableIdleAction.Unload; }; //    wfApp.Load(new Guid(assignedDocumentInfo.WorkflowInstanceId)); //           wfApp.ResumeBookmark(new Bookmark(assignedDocumentInfo.BookmarkName), arg); }
      
      





2種類のアクティビティプロパティの違いを明確にしたいと思います。







 public class MyActivity<T> : CodeActivity { public InArgument<int> UserId1 {get; set;} public int UserId2 {get; set;} }
      
      





最初のケースでは、プロパティの実際の値(これはOutArgument <T>およびVariable <T>にも適用されます )はアクティビティプロセスの参加者であり、プロセスの値を変更し、ストレージの状態を保存および復元できます:デザイナーのアクティビティプロパティテーブルの要素は 、XAMLのアクティビティ属性は







2番目の場合、値は定数です-アクティビティ中に変更されません。 XAMLで不変のアクティビティパラメータを保存するために使用すると便利です:XAML:デザイナーのアクティビティプロパティテーブルの要素- 、XAMLのアクティビティ属性は







子アクティビティの起動の管理



次に、リポジトリ内のオブジェクトを更新するアクティビティを分析します。 たくさんのオプションがあります。 たとえば、子アクティビティの構成の形式で実装します。 外部からのデータを必要としないアクティビティの場合、特別なCodeActivity基本クラスも基本アクティビティライブラリに実装されます。







 /// <summary> ///         -  /// </summary> /// <typeparam name="T">  </typeparam> public abstract class ObjectSetPropertyActivity<T> : CodeActivity where T : class { public T Object { get; set; } } /// <summary> ///       -  /// </summary> /// <typeparam name="T">  </typeparam> /// <typeparam name="TProperty">  </typeparam> public class ObjectSetPropertyActivity<T, TProperty> : ObjectSetPropertyActivity<T> where T : class { /// <summary> ///   /// </summary> public string Property { get; set; } /// <summary> ///     ( ) /// </summary> [RequiredArgument] public InArgument<TProperty> Value { get; set; } protected override void Execute(CodeActivityContext context) { //   typeof(T).GetProperty(Property).SetValue(Object, Value.Get(context), null); } } /// <summary> /// ,      /// </summary> /// <typeparam name="T">   </typeparam> /// <typeparam name="TKey">  </typeparam> public class ObjectUpdateActivity<T, TKey> : CodeActivity where T : class { public ObjectUpdateActivity() { this.Activities = new Collection<ObjectSetPropertyActivity<T>>(); } /// <summary> ///     ( ) /// </summary> public InArgument<TKey> Key { get; set; } /// <summary> ///        /// </summary> public Collection<ObjectSetPropertyActivity<T>> Activities { get; set; } protected override void Execute(NativeActivityContext context) { if (this.Activities.Count > 0) { var store = new DocWorkflowDbContext(); var obj = store.LoadObject<T>(context.GetValue(Key)); var index = 0; CompletionCallback callback = (context1, activityInstance) => { index++; //    : //-  ; //-      . if (index < this.Activities.Count) { this.Activities[index].Object = obj; context1.ScheduleActivity(this.Activities[index], callback); } else { store.UpdateObject(obj); store.SaveChanges(); store.Dispose(); } }; //      this.Activities[index].Object = obj; //    context.ScheduleActivity(this.Activities[index], callback); } } }
      
      





ご覧のとおり、親アクティビティはデータベースからオブジェクトをロードし、WFランタイムですべての子アクティビティを起動してオブジェクトのプロパティを変更し、変更をリポジトリに保存します。 アクティビティタイプの普遍性により、デザイナーのオブジェクトプロパティの入力新しい値の検証もあります。







そのため、このアクティビティはデザイナーで見ることができます。









ワークフローデザイナー



これは、テクノロジーの最も「粘性のある」部分です。つまり、範囲をわずかに超える何かを実現するには、「タンバリンを踊り、打ち負かす」必要があります。 同じ







デザイナーはテクノロジーWPFに基づいています。







ここでの主な要素は、 WorkflowDesigner-ワークフローの構築が行われるグラフィック領域です。







[Designer]属性を使用して、各アクティビティを独自のデザインオブジェクトに関連付けることができます(VSには、WF要素のデザインを開発するための特別なタイプのプロジェクト-Activity Designer Libraryもあります)。 同じことがアクティビティのプロパティ[Editor]属性にも当てはまります。 つまり、実際、この部分で新しいものは何もありません。







目立つものやコンパクトなものを特定するのは難しいため、コードは提供しません。 このために、いくつかのリンクを提供します。







デザイナーの実装の素晴らしい例を次に示します。 これは、XAMLでエンコードされた作成済みアクティビティを、新しいアクティビティのビルディングブロックとしてすぐに使用できる方法のモデルです。 この例の特徴は、最初のアクティビティが変更されても、最初のアクティビティが使用されるすべてのアクティビティのアルゴリズムが変更されないことです。 残念ながら、この「機能」は私たちには適していない。







そして、 ここに例付きの優れたWF投稿のシリーズがあります(特に、デザイナーでユニバーサルタイプの引数を置き換えることでサンプルを見つけることができます)。 必要に応じて、「タンバリンでジャンプ」する必要は少なくなりますが、それでも必要です。







おわりに



ご覧のとおり、開発はそれほど複雑ではなく、システムは簡単に拡張できます。 必要に応じて、たとえば、特定のドキュメントプロパティの変更の可用性を制御するアクティビティを実装したり、ドキュメントのリストを管理するモジュールでこの機能をサポートしたりできます。







多くの作業が行われましたか? 大きなもの。 しかし、それは小さなチームの歯のためですか? はい、3〜4人のチームでも。 そして、妥当な時間で!







この投稿の情報がお役に立てば幸いです。 私が何かを逃した場合、十分に正しく設定しなかった場合、または質問がある場合は、コメントへようこそ!







画像







All Articles