Thursday, August 8, 2013

Developing SDL Tridion 2013 Workflows

In a previous post I described what is new in SDL Tridion 2013 Workflows but using a Technical Preview point of view, this time I wanted to provide some information about development of SDL Tridion 2013 GA.

Classes Design

The GA version introduced a new way of Automatic Activities development, all of them inherits from ExternalActivity, this abstract class provides basic workflow properties and functionality as shown below.

public class ExternalActivity : IExternalActivity {
    public ExternalActivity();

    public ActivityInstanceData ActivityInstance { get; }
    public SessionAwareCoreServiceClient CoreServiceClient { get; }
    public ProcessInstanceData ProcessInstance { get; }
    public StreamDownloadClient StreamDownloadClient { get; }
    public StreamUploadClient StreamUploadClient { get; }

    protected virtual void Execute();
    protected virtual object GetSessionContextData();
    protected virtual void Resume(string bookmark);
}


This class will give us direct access to the current Activity Instance as well as to the current Process Instance, additionally it gives us access to the CoreServices via three endpoints, CoreServiceClient, StreamDownloadClient, StreamUploadClient.

This class also provide virtual methods (must be overridden), the most important ones are Execute and Resume, Execute contains the code that will be executed when the activity is executed, Resume contains the code that will be executed when the activity is resumed.

As you might noticed, we have replaced the modular way of implementing Workflow Automatic Activities (one activity executes a method in a class) for an object oriented programming approach. It is great since it allow us now to create our own classes hierarchy.

Implementing a Classes Design

For this post I have implemented a light classes design that will effectively show the power of the new Workflow.
 

This is a basic classes design but it can be extended and used for production ready solutions. This classes design is intended to reuse functionality by using abstract classes. Here some explanation.

BaseActivity

This activity inherits from the External Activity base class and is used to extend the out of the box functionality adding common used attributes and functionality.



This class will make available information like the current Bundle, Activity Definition, Process Definition, Last Performer and so on.

BaseActivity Implementation

public abstract class BaseActivity : ExternalActivity {
    public IEnumerable<WorkItemData> WorkItems { get; set; }
    public VirtualFolderData Bundle { get; set; }
    public TrusteeData NextAssignee { get; set; }
    public string FinishMessage { get; set; }
    public PublicationData Publication { get; set; }
    public ProcessDefinitionData ProcessDefinition { get; set; }
    public ActivityInstanceData SuspendedActivity { get; set; }
    public TridionActivityDefinitionData ActivityDefinition { get; set; }
    public WorkflowConfiguration WorkflowConfiguration { get; set; }
    public ReadOptions ReadOptions { get; set; }

    public bool IsSuspended {
        get {
            return SuspendedActivity != null && SuspendedActivity.ActivityState == ActivityState.Suspended;
        }
    }

    protected virtual void Initialize() {
        ReadOptions = new ReadOptions();

        Bundle = GetBundle();
        Publication = GetPublication();

        ActivityDefinition = (TridionActivityDefinitionData)CoreServiceClient.Read(ActivityInstance.ActivityDefinition.IdRef, ReadOptions);
        ProcessDefinition = (ProcessDefinitionData)CoreServiceClient.Read(ProcessInstance.ProcessDefinition.IdRef, ReadOptions);

        WorkflowConfiguration = new WorkflowConfiguration(CoreServiceClient, Publication);
    }

    protected virtual void FinishActivity() {
        if (!IsSuspended) {
            CoreServiceClient.FinishActivity(ActivityInstance.Id, new ActivityFinishData() {
                Message = FinishMessage,
                NextAssignee = NextAssignee != null ? new LinkToTrusteeData() { IdRef = NextAssignee.Id } : null
            }, ReadOptions);
        }
    }

    protected override void Resume(string bookmark) {
        SuspendedActivity = (ActivityInstanceData)CoreServiceClient.Read(ActivityInstance.Id, ReadOptions);
    }

    private PublicationData GetPublication() {
        int publicationId = new TcmUri(WorkItems.ElementAt(0).Id).PublicationId;
        string publicationUri = string.Format("tcm:0-{0}-1", publicationId);
        return (PublicationData)CoreServiceClient.Read(publicationUri, ReadOptions);
    }

    private VirtualFolderData GetBundle() {
        WorkItemData bundleWorkItem = WorkItems.First(b => new TcmUri(b.Subject.IdRef).ItemType == ContentManager.ItemType.VirtualFolder);
        if (bundleWorkItem != null) {
            return (VirtualFolderData)CoreServiceClient.Read(bundleWorkItem.Subject.IdRef, ReadOptions);
        }
        return null;
    }

    public UserData GetLastManualActivityPerformer() {
        ActivityInstanceData lastManualActivity = GetLastManualActivity();
        return (UserData)CoreServiceClient.Read(lastManualActivity.Performers.Last().IdRef, ReadOptions);
    }

    public ActivityInstanceData GetLastManualActivity() {
        IEnumerable<ActivityInstanceData> activityInstances =
            ProcessInstance.Activities.OfType<ActivityInstanceData>().OrderByDescending(o => o.StartDate);

        return activityInstances.First(a => {
            TridionActivityDefinitionData activityDefinition =
                (TridionActivityDefinitionData)CoreServiceClient.Read(a.ActivityDefinition.IdRef, ReadOptions);
            return string.IsNullOrEmpty(activityDefinition.Script);
        });
    }
}

    

PublisherAwareActivity 

This class represents an activity that deals with the publisher, this is a base class for all Publish or Unpublish activities. This provides publisher generic information like Publisher Instructions, Publish Transactions, Publication Targets and Publish Priorities.



 This class is not intended to be used directly in Automatic Activities but to be a base class.

PublishActivity

This class contains Publish specific functionality

public abstract class PublishActivity : PublisherAwareActivity {
    protected override void Initialize() {
        PublisherInstructions = new PublishInstructionData();
        ((PublishInstructionData)PublisherInstructions).RenderInstruction = new RenderInstructionData();

        base.Initialize();
    }

    protected override void Execute() {
        Publish();
    }

    protected void Publish() {
        string[] itemsToPublish = Bundle != null ? new string[] { Bundle.Id } : WorkItems.Select(s => s.Id).ToArray();
        string[] publicationTargets = PublicationTargets.Select(s => s.Id).ToArray();

        PublishTransactions = CoreServiceClient.Publish(itemsToPublish, (PublishInstructionData)PublisherInstructions, publicationTargets, Priority, ReadOptions);

        for (IEnumerator<string> e = PublishTransactionKeys.GetEnumerator(); e.MoveNext(); ) {
            if (!ProcessInstance.Variables.ContainsKey(e.Current)) {
                ProcessInstance.Variables.Add(e.Current, PublishTransactions.ElementAt(0).Id);
            }
            else {
                ProcessInstance.Variables[e.Current] = PublishTransactions.ElementAt(0).Id;
            }
        }
    }
}





This post is intended to be a guideline and a base for Workflow implementations, if you are interested in the full implementation please don't hesitate to contact me.

6 comments:

  1. Excellent post and example, Eric. I definitely like the object-oriented approach for workflow. In addition to making it easier to program and maintain, I can see how it'd make both design and testing much easier.

    We can focus on what's in common and needed across all activities. The use cases could map to basic use case (UML) diagrams to eventually class diagrams. The functional and technical designs will even be smaller and easier-to-read!

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi,
    I have implemented the Workflow using core-service in Tridion 2011. recently the Tridion version is being upgraded to 2013 and the Workflow is working fine, but now if i am trying to debug the code, the execution is not stopping at the breakpoint and i am not able to Debug the code. can you please help me on this

    ReplyDelete
  4. Sunil, consider asking on Tridion Stack Exchange with the details of your setup (including if you've uploaded the dll, attached to the right process, and any errors).

    ReplyDelete
  5. Hi Eric,

    Can you please provide more detail on the PublishToDevActivity & PublishToLiveActivity from the Class diagram?

    ReplyDelete