📜 ⬆️ ⬇️

Overview of Windows Workflow Foundation on the example of building an electronic document management system [Part 2]

Development of a document management system is an overwhelming task for a small team?



In a previous post, I looked at the architectural features of WF when it is best to use this technology. In this part, I will convey how WF is applied in a particular project.


So, the task is to implement an electronic document management system.



For convenience, I will break this part into several blocks.


Maintaining lists of documents

This module was implemented using ASP.NET MVC technology. Allows you to create and edit various types of documents, run workflows on them. We denote it here so that it can be referenced in the future, but we will not consider it in detail.


We assume that we have document classes that implement approximately the following common interface *:


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

* Here and further, a simplified version of the production code is used to simplify.


General activities

Users launch document activity activities (hereinafter referred to as General Activities). Common activities have 2 input arguments:


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

, here DocumentId is the document key, and UserId is the key of the user who starts the activity.


Thus, in the General activities, only the key of the document rotates, and not the entire object, which will help avoid problems when restoring the state of the saved workflows if we need to modify the document class in the future (add a property, for example).


In addition, a separate set of metadata is separately stored for each General activity: launch privileges, document types for which the activity is allowed to run, Dynamic LINQ expression to the document for testing the launch capability, and others.


The total activity — the custom part of our system — is coded by the workflow designer and saved as XAML. The implementation of the designer will be discussed below.


Below is the code to start the activity in the WF runtime.


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

One thing needs to be clarified here: in order for the workflow to save its state if necessary, an instance of the SqlWorkflowInstanceStore storage class is transferred to the runtime using Property Injection .


Passing Arguments to the Workflow

So Total Activity is up and running. But at some point, she may need additional data from users of the system for her work. On the basis of this data, in accordance with her logic, she chooses a branch of execution, changes the properties of a document, sends notifications, as well as other actions.


Therefore, it is necessary to describe these data (hereinafter - Arguments). There were 2 options. Describe one universal type (the “key - value” collection) that would be used to transfer any information to the workflow. But I chose the benefits of strong typing and implemented a dynamically loadable library of custom Arguments. Classes implement the IAssignArgument interface:


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

where UserId is the key of the user who is passing the Argument to the workflow.


We now turn to the activity itself, which is waiting for input data. For these purposes, the NativeActivity <T> class is provided in the base activity library, where T is the result of the work of the activity — our Argument. We will inherit our class from 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); } } 

As you can see, this activity at the first stage saves the data necessary for restoring the state to the repository (document key, user key to which the task is assigned, type of General activity, type of Argument, name of action, “bookmark” for recovery). At the second stage, it receives the input Argument and passes it to the out-parameter for use in the body of the Common Stream.




The module for working with documents, in turn, sees the tasks assigned to the user, when starting the workflow restoration process dynamically forms a view based on the type of Argument (if necessary, you can specify a custom view, of course) receives an Argument from the user and sends it to restore Total Activity .





Strong typing of Arguments (the antipode - Argument [“Prop1”] ) allows validating references to the properties of the Argument at the stage of building activity in the designer.



Stream with violation of the rules of type conversion will not start. That is, errors occur at compile time, not at run time, literally.


Below is the code to restore the workflow.


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

I would like to clarify the difference between the two types of activity properties:


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

In the first case, the actual value of the property (this also applies to OutArgument <T> and Variable <T> ) is a participant in the work activity process, it can change the value in the process, save and restore its state in the repository: an element of the activity property table in the designer XAML Activity Attribute - .


In the second case, the value is constant - it does not change when the activity is working. It is convenient to use to save unchanged activity parameters in XAML: an element of the table of activity properties in the designer - XAML Activity Attribute - .


Managing the launch of child activities

Next, analyze the activity for updating objects in the repository. There may be plenty of options. For example, we implement it in the form of a composition of child activities. For activities that do not require data from the outside, a special base class CodeActivity is also implemented in the base activity library.


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

As you can see, the parent activity loads the object from the database, runs all child activities in the WF runtime to change the properties of the object, and then saves the changes to the repository. Due to the universality of the types of activities, we also have a validation of the types of input new values ​​for the properties of an object in the designer.


So this activity may look like in a designer.



Workflow Designer

This is the most "viscous" part of the technology, that is, to realize something slightly protruding beyond the framework, we need to "dance and beat the tambourine." that same


Designer is built on technology WPF.


The main element here is WorkflowDesigner - the graphic area in which the workflow is being constructed.


Each activity can be associated with its own design-object (VS even has a special type of project for developing the design of a WF element - Activity Designer Library) using the [Designer] attribute. The same applies to the properties of the activity - attribute [Editor] . That is, nothing new in this part, in fact.


I will not give the code, since it is difficult to identify something that stands out at the same time and is quite compact. For that I will give a few links.


Here is a great example of the implementation of the designer. This is a model of how created activities encoded in XAML can be immediately used as building elements in new activities. The peculiarity of the example is that a change in the initial activity does not lead to a change in the algorithms of all the activities where the original is used. Unfortunately, this “feature” did not suit us.


And here is a series of excellent posts on WF with examples (in particular, you can find a sample by replacing the arguments of the universal type in the designer). If necessary, you will need less “jumping with a tambourine”, but you still have to.


Conclusion

As you can see, the development is not very complicated, the system easily expands. If desired, we can implement the activity that controls the availability of specific properties of the document for changing, for example, and in the module for maintaining lists of documents - support this functionality.


Was a lot of work done in the end? Big. But is she in the teeth for a small team? Yes, even for a team of 3 - 4 people. And within a reasonable time!


I hope the information outlined in this post will be useful to you. If I missed something, stated it incorrectly, or you have questions - welcome to the comments!


image

')

Source: https://habr.com/ru/post/182746/


All Articles