Training Site Template - Part 2: Workflows

Sean Hey there,

 It's Sean again with another installment of the template development walkthrough. Last time we covered the list schema for the Employee Training Scheduling and Materials template, one of the new Application Templates for Windows SharePoint Services 3.0. In this series I'm showing you how we built one of these templates from the ground up, and today we're going to author all of the training site's workflows using SharePoint Designer. With that done, you'll have built the whole back-end of the template, so in a future installment we'll get our hands dirty with SharePoint's views and forms.

Folder List (Training Site)Open up a fresh installation of the training site in SPD and you'll see the folder list pictured on the right - with the three workflows you'll be creating today in the Workflow Designer. They'll provide some of the rich behavior that you wouldn't see in a static web app, and you won't need to write any code to get it done. Let's get started!

Automatic Reminders

People are forgetful. Computers aren't. So when an instructor comes to your training site to create a course, why should they have to remember when it is? That's where you can come in with this workflow, which we'll set on the Courses list:

  1. In SPD, go to File > New > Workflow...
  2. Name it "Instructor reminder" and attach it to the SharePoint list "Courses"
  3. Check the box to Automatically start this workflow when a new item is created
  4. Click Next > to continue

From here you'll want to create 3 steps which will logically separate our workflow into sets of related actions. Click Add workflow step to move on to the next step, and in each case if you don't see the specific action listed, click the Actions button > More Actions... to see the full list. Additionally I'm using [brackets] to refer to what you need to do in each placeholder.

Also, fx just means the data binding button. You get the same dialog whenever you push Add Lookup to Body in the e-mail builder.

So before you start, first create two variables (click Variables... > Add... to make them), one called "Reminder Time" (Date/Time) and one called "Subject" (String). You'll need them once you get to the individual steps:

  1. First step: name it "Set reminder time"
    1. No conditions
    2. Actions > Add Time to Date - Add -24 hours to [fx :: Current Item, Start Time] (Output to Variable: Reminder Time)
    3. Actions > Build Dynamic String -  Store "RE: [fx :: Current Item, Course Title]" in Variable: Subject
  2. Second step: name it "Send confirmation"
    1. No conditions
    2. Actions > Send an Email - Email [...]
      1. To: Workflow Lookup... :: Current Item, Instructor
      2. Subject: fx :: Current Item, Course Title
      3. Body: This is a confirmation that you are scheduled to teach [Add Lookup to Body :: Current Item, Course Title] on [Add Lookup to Body :: Current Item, Start Time] at [Add Lookup to Body :: Current Item, Location]. For more information, please visit the training site.
  3. Third step: name it "Send reminder about course"
    1. Conditions > Compare Courses field - If Created is less than [fx :: Workflow Data, Variable: Reminder Time]
    2. Actions > Pause Until Date - Pause until [fx :: Workflow Data, Variable: Reminder Time]
    3. Actions > Send an Email - Email [...]
      1. To: same as above
      2. Subject: fx :: Workflow Data, Variable: Subject
      3. Body: This is a reminder that you are scheduled to teach [Add Lookup to Body :: Current Item, Course Title] on [Add Lookup to Body :: Current Item, Start Time] at [Add Lookup to Body :: Current Item, Location] in 24 hours. For more information, please visit the training site.
  4. Click Finish

Make sense? We're sending two reminders to anyone who adds a new item to the Courses list (i.e. an instructor creating a course) - one right when they make it as a confirmation, and another 24 hours before the course is scheduled. And the first step was just to keep the variable assignment separate, there's nothing wrong with doing it in the other steps right before they are used.

We'll want to do a similar reminder for students, but we can do it at the same time that we enforce seating policy (each item in Courses has an Available Seats and Total Seats) since it only really makes sense to send reminders if you actually make it into a course.

Processing Student Registrations

If you try to add a new course by browsing to Courses > New..., you'll see this line in the new form:

And you'll also notice that there's no line for Available Seats or Filled Seats, even though we added those fields to the Courses schema in part one. I'll teach you the trick to hide form items next time, what's important here now is that 0 will be our sentinel value in the workflow to come.

Let's go through this one a bit faster by using some shorthand.

  • ... means I'm referring to a string (probably for those long auto-generated e-mails) that you can find the full text for by opening Attendee registration.xoml in SPD.
  • Courses* means you need to perform a lookup back into the Courses list. To do this, start by pushing fx and pick Source: Courses, at which point you'll see the "Find the List Item" section appear. In that section, match Field: Courses:ID to Value: Registrations:Course ID (use the nearby fx :: Current Item, Course ID).
  • Variable: = anything with a leading Variable: in front of it is a workflow variable that can be found in fx :: Workflow Data.

Start with a new workflow called "Attendee registration," attached to Registrations and triggered when an item is created. Create 7 variables before you begin, 4 of type String (Subject, Confirmation Body, Reminder Body, Reminder Body 2), 2 Date/Time (Reminder Time, Reminder Time 2), and 1 Number (New Filled Seats). From there build out the steps:

  1. "Cache all variables"
    1. No conditions
    2. Actions > Build Dynamic String (x4)
      1. Store ... in Variable: Confirmation Body
      2. Store ... in Variable: Reminder Body
      3. Store ... in Variable: Reminder Body 2
      4. Store ... in Variable: Subject
  2. "Enforce seating policy"
    1. Conditions > Compare any data source (x2)
      1. If [fx :: Courses*, Total Seats] equals 0
      2. or [fx :: Courses*, Filled Seats] is less than [fx :: Courses*, Total Seats]
    2. Actions > Set Workflow Variable (x2)
      1. Set Variable: Reminder Time to [fx :: Courses*, Start Time]
      2. Set Variable: Reminder Time 2 to [fx :: Courses*, End Time]
    3. Actions > Do Calculation - Calculate [fx :: Courses*, Filled Seats] plus 1 (Output to Variable: New Filled Seats)
    4. Actions > Update List Item
      1. List: Courses*
      2. Add... :: Set Filled Seats to Variable: New Filled Seats
    5. Click Add 'Else If' Conditional Branch
    6. No conditions
    7. Actions > Delete Item - Delete item in Registrations (Current Item)
    8. Actions > Stop Workflow - Stop the workflow and log "Course is already full."
  3. "Set reminder time"
    1. No conditions
    2. Actions > Add Time to Date - Add -24 hours to Variable: Reminder Time (Output to Variable: Reminder Time)
  4. "Send confirmation"
    1. No conditions
    2. Actions > Send an Email - Email ...
  5. "Send reminder about course"
    1. Conditions > Compare Registrations field - If Created is less than Variable: Reminder Time
    2. Actions > Pause Until Date - Pause until Variable: Reminder Time
    3. Actions > Send an Email - Email ...
  6. "Send reminder for feedback"
    1. Conditions > Compare Registrations field - If Created is less than Variable: Reminder Time 2
    2. Actions > Pause Until Date - Pause until Variable: Reminder Time 2
    3. Actions > Send an Email - Email ...
  7. "Move to Past Registrations"
    1. No conditions
    2. Actions > Copy List Item - Copy item in Registrations (Current Item) to Past Registrations
    3. Actions > Delete Item - Delete item in Registrations (Current Item)

Yikes, I know. But thankfully that's about as bad as it gets. Here's a brief explanation - a student will register for a course, but they won't get in right away (technically, they get in but they may be kicked out immediately if the seating logic fails). After setting all the variables we check if Total Seats is 0 (unlimited) or greater than Filled Seats (i.e. is there room in the course?). If not, we abort prematurely (delete/stop step two), otherwise we do the math and add 1 to the Filled Seats, send reminders, etc. The last step is the archival in the mirror list Past Registrations, something that will become more evident in part three when we build up the front end.

A Dirty Workflow Secret

That's all well and good, but what happens when someone wants to unregister from the course? We need to subtract 1 from the number of Filled Seats, then delete their registration from Registrations, but here's a problem - workflow cannot be triggered to run before an item is deleted. So here's how we'll fake it - since workflow can run when an item is changed, we'll set up another sentinel value for our workflow to be interested in. DELETE seems pretty self explanatory, right?

(Note that this workaround has some serious implications - we'll need to hide item deletion in Registrations and block all edits to our field so the user never trips the sentinel. Don't worry, I'll explain in due time...)

Our last workflow, "Attendee unregistration" (for lack of a better word), attached to Registrations, triggered when an item is changed. One variable before you start: a String called Log Message. Not even really necessary, just for posterity:

  1. "Unregister from course"
    1. Conditions > Compare Registrations field - If Title equals "DELETE"
    2. Actions > Delete Item - Delete item in Registrations (Current Item)
    3. Actions > Build Dynamic String - Store ... in Variable: Log Message
    4. Actions > Stop Workflow - Stop the workflow and log Variable: Log Message

And there you have it, dynamic behavior that takes a long time to spell out in a blog post, but goes a long way towards the richness and robustness of your SharePoint app. It's taken us a few key design decisions to get here, and so in part three we'll learn what consequences that holds for the front end system, which is driven by SharePoint views and forms. I'll see you then!

Cheers,
Sean