Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Script Studio, a Drag-n-Drop Programming Interface

0.00/5 (No votes)
2 Jan 2009 2  
Use Script Studio to visually create small programs that perform the kinds of tasks that you might otherwise create a batch file to do. Full featured and fully extensible.

License

Please read the license that comes with this project (see the "eula.rtf" file that comes with the download). Different licenses govern different portions of the download. The basics are this:

  • SOURCE CODE FILES (non-FLEE source) -- source code files that are not related to the FLEE library are governed by the Code Project Open License, so you can basically use the source in any way you see fit.
  • SOURCE CODE FILES (FLEE source) -- The FLEE library is available on www.codeplex.com under the LGPL license. You must obey that license when using source code files related to the FLEE library.
  • BINARY FILES -- free for you to use for both personal and commercial purposes, but you cannot redistribute them, create derived works (other than to create new tasks as described in Chapter 5), or reverse engineer the binary files that come with this download. If you would like to reuse the user interface, WinForms controls, script runtime engine, or any of the other components or classes defined in this project's binary files to extend/enhance your application (internally or for redistribution), please contact the author at www.roundpolygons.com for licensing information.
  • DOCUMENTATION FILES -- You can basically do anything you want with documentation files.

Download

After Install

The download is an MSI installer package. After install:

  • Run the application from your start menu (Start -> All Programs -> Round Polygons -> Script Studio -> Script Studio).
  • Open the source code for 100% of the tasks in the project from your start menu (Start -> All Programs -> Round Polygons -> Script Studio -> Generic Tasks Source VS 200x.sln).
  • Be sure to read the section on creating your own tasks before messing around with the source code. The source generates DLLs, and as such, will not run directly, so please read the article before posting a comment that the project does not work.

Introduction

After getting tired of writing yet another batch file to perform a series of simple tasks, I decided to create a UI that I could use to drag tasks onto a design surface and chain them up. OK, mostly I just wanted to see if I could make a UI that would allow me to chain up tasks and then execute them in that order. The project grew and evolved into Script Studio (SS), the application you'll see in this article.

Script Studio is more powerful than initially meets the eye. You can create impressively complex scripts just with the tasks provided. Better though, is that it is exceedingly simple to create your own tasks that integrate fully and seamlessly into the app's interface.

This article will show you how to use the application, offer tutorials for setting up your own scripts, and finally, give detailed instruction on how to create your own tasks for use in your scripts. A community task repository will be set up for users to share their tasks (additionaltasks.roundpolygons.com).

Contents

  1. Background
  2. Chapter 1 -- Basics
    1. Navigation
    2. The Script
    3. Tasks and Their Properties
    4. Tutorial #1 -- Check Me!
  3. Chapter 2 -- Intermediate Concepts
    1. Common Task Properties
    2. Run Results
    3. Variables
    4. Expressions
    5. Tutorial #2 -- Replace File Contents
  4. Chapter 3 -- Script Levels
    1. Parent Tasks, Child Tasks, and Script Levels
    2. User Functions
    3. Tutorial #3 -- Counting Files
  5. Chapter 4 -- Advanced Concepts
    1. Threading
    2. Commandline Variables
  6. Chapter 5 -- Creating Your Own Tasks
    1. Installing the Item Templates
    2. Create Your First Task
    3. The ITaskImplementation Interface
    4. The ITaskCore Interface
    5. Property Attributes
    6. Supporting Serialization and Versioning
    7. Supporting Undo/Redo
    8. The IEmbeddedControl Interface
    9. The IScriptFlow Interface
    10. The IContainerTaskImplementation Interface
    11. The IChildrenScriptFlow Interface
    12. Extra Tasks Website
  7. History
  8. Roadmap

Background

About 2 years ago I released a project very similar to this one and it received fast attention. In about a month I sold the source code to a private company and hence was required to remove all traces of it from the web (I cannot name the project or the company, sorry). I later found that I regretted selling it. So...

I decided to do it all over again. Script Studio is a completely new application, with similar functionality, but far superior code architecture, a substantially more user-friendly UI, more powerful scripting capabilities, and many more options for end users to implement their own tasks. I hope you enjoy using this project as much as I've enjoyed coding it, and I encourage you to create and share your tasks at www.roundpolygons.com.

Chapter 1 -- Basics

In this first chapter, we'll learn the simple concepts of how to navigate the interface, create and attach tasks, and chain them up to form an SS script. We'll learn to start and stop the script, and see the run result of each task. We'll conclude with a tutorial.

Navigation

Navigating SS is pretty easy. The interface consists of 3 primary windows:

  • The Toolbox -- the task list (left). This list contains all of the tasks that you can add to your SS script, and you can drag those tasks onto the design surface as needed.
  • The Design Surface -- the main scripting surface (center). You will add and arrange tasks on this surface to create your script.
  • The Property Grid -- the task properties editor (right). You will adjust your tasks' properties in this grid to make them do your bidding.

These 3 windows are similar to the toolbox, code text editor, and property grid in Visual Studio. Drag tasks from the toolbox onto the design surface and arrange them on the surface as needed. Once you have at least two tasks on the surface, chain them up by dragging from the input or output socket () of one of the tasks and dropping it on another task. A connection will be created between the two tasks, and when your script is run, the script will flow from the output of the first task to the input of the second one.

As you edit your script, use the features in the following list to manipulate your tasks. Try each of them out as you read... they are quite simple, and in only minutes you'll be completely comfortable using the interface.

  • Creating Tasks -- Drag a task from the toolbox and drop it on the design surface.
  • Deleting Tasks -- Select a task and then hit the delete key, or right-click the task and click the delete command.
  • Selecting Tasks
    • Select Task -- Click a task on the design surface.
    • Multi-Select Tasks -- Drag a box around several tasks.
    • Add Task to Selection -- Shift+click or ctrl+click an unselected task.
    • Remove Task from Selection -- Ctrl+click a selected task.
    • Add Multiple Tasks to Selection -- Shift+drag a box around several tasks.
    • Toggle Multiple Tasks to/from Selection -- Ctrl+drag a box round several tasks (selected ones will be unselected and vice versa).
    • Select from Task Down -- Space+click a task. It and all following tasks in the script will be selected. Alternately, you can right-click the task and select "Select From Here Down".
    • Select from Task Up -- Space+shift+click a task. It and all previous tasks in the script will be selected. Alternately, you can right-click the task and select "Select From Here Up".
  • Moving Tasks
    • Move Task -- Drag a task around on the design surface.
    • Move Multiple Tasks -- Select multiple tasks, and drag any one of them around on the design surface.
  • Panning (Moving the View)
    • Hold the space bar and drag anywhere on the design surface (but NOT on a task).
    • Middle-drag anywhere on the design surface or on tasks.
    • Drag the little green plus () in the lower right corner of the design surface. This will show a mini-map of the entire script and allow you to move the viewport to a particular area of the script quickly. Use this method when move great distances.
    • Display the "Navigation" panel (located at the bottom of the application) and drag the viewport window.
  • Zooming
    • Change the zoom percentage in the toolbar.
    • Scroll the mouse wheel.
  • Chaining Tasks
    • Chain one task to another -- Drag from the output or input socket of any task to any other task.
    • Unchain one task from another -- right-click the connector between the tasks and select "Disconnect" in the context menu.
    • Chain several tasks -- Select multiple tasks, right-click one of them and select "Chain Selected Tasks". This will chain the tasks from the top down, considering only the vertical positioning of the tasks when figuring out the order in which to chain them.
    • Rechain several tasks -- Select multiple tasks, right-click one of them and select "Rechain Selected Tasks". This will first remove any chains that are already on the selected tasks and then perform the "Chain Selected Tasks" function.
    • Unchain several tasks -- Select multiple tasks, right-click one of them and select "Unchain Selected Tasks". This will remove any chains that are on the selected tasks.
  • Cut/Copy/Paste -- All standard cut/copy/paste commands work like any other Windows application (Ctrl+X/C/V). Note that when copying tasks, chains between the copied tasks are NOT maintained. You can even copy tasks in one script level and paste them into another (see the section on Subtasks for details on script levels).
  • Undo/Redo -- Standard undo and redo commands work as expected (Ctrl+Z/Y). Note that the developer of new tasks must implement undo and redo for his/her task, so some tasks may not support this (all of the supplies tasks support full undo/redo).
  • Rename a Task -- Double-click on the task's name and type the new name right on the task itself. Alternately, select the task and change its name in the property grid's "Name" property.
  • Navigate to Subtasks -- Double click a task that contains subtasks. More on subtasks later.

Consider practicing using the functions listed above until you are comfortable with them before moving on to the next section.

The Script

All SS scripts must start with a "Start" task. Tasks that are not [ultimately] chained to a start task will not run. You can have more than one start task if you want two parallel "threads" in your script at the same time (more on threading later).

To start your script, hit F5 or select "Program -> Start" from the main menu. To stop it mid-stream, hit F5 again or select "Program -> Stop". A complete log of what occurred during script runtime can be found in the script log window at bottom of the SS application.

Tasks and Their Properties

All tasks have properties that can be manipulated to make them behave as needed. When a task is selected, you will see its properties in the Properties panel to the right. Experiment with some of the properties of any task. When a property is selected in the property grid, you can see documentation for that property in a small window at the bottom of the property grid.

Note that when you have multiple tasks selected, only those properties that are common to all selected tasks will show in the property grid. If you make changes in the grid, the changes will apply to all selected tasks.

Tutorial #1 -- Check Me!

In this tutorial, you will create an SS script that will present a dialog with a checkbox to the user. When the user dismisses the box, a message box will be shown with a message indicating whether or not the user checked the checkbox. Boring, yes, but you'll get a quick idea of how SS works and how to interact with it.

Follow the steps below to create the script. You can also download a copy of the completed script here (right-click and select "Save Target As..."). A quick note about the downloaded scripts... CodeProject will only let you upload files with certain extensions. Script Studio files end with ".sss", but I had to upload them as ".xml". When you go to open the script files that you download from this article in Script Studio, you will either need to rename them to .sss files, or change the open file dialog's file filter to all files (*.*).

Figure 2 is a screenshot of the completed script.

Tutorial #1, Completed Script
Figure 2 -- Completed Tutorial #1 Script

Perform these steps for Tutorial #1:

  1. Create a new script with "File -> New".
  2. Add a "User Input" task to the script by dragging it from the Toolbox's "User Interface" group. Set the following properties on your User Input task:
    • Control 1 Label -- You should probably check this box!
    • Control 1 Result Variable -- isChecked
    • User Input Type -- OneCheckBox
  3. Add an "If Then Else" task to the script by dragging it from the Toolbox's "Program Flow" group. Set the following properties on your If Then Else task:
    • If Condition -- isChecked = true
  4. Add a "Message Box" task to the script by dragging it from the Toolbox's "User Interface" group. Set the following properties on your Message Box task:
    • Message -- Thanks, you checked the box.
    • Title -- SS Tutorial #1
    • Name -- Checked Message. (Note that you can change a task's name by double-clicking the name right on the task itself. Changing the name is an optional step and will not affect how the script runs).
  5. Select, copy and paste your message box task. You will now have a second one. Change the following properties on your new Message Box task:
    • Message -- I told you to check the box!
    • Name -- Unchecked Message
  6. Arrange the tasks as shown in the screenshot above and chain them up by dragging from output sockets or input sockets to other tasks.

Your script is ready to run. Hit F5. You should be presented with a dialog that contains a single checkbox with the text "You should probably check this box!" If you check it and press OK, you'll receive the message, "Thanks, you checked the box." If you don't check it you'll get the message, "I told you to check the box!" Finally, if you cancel the dialog rather than clicking OK, the SS script will show that the If Then Else task did not know how to respond.

Chapter 2 -- Intermediate Concepts

At this point you should already have a basic idea of how to get a new script in SS up and running. In this chapter we will discuss the various concepts that are common to all task types, and we'll show more complex scenarios that use variables and property expressions.

Common Task Properties

All tasks have a common collection of properties that offer generic functionality, regardless of the task type. The functionality includes things such as the ability to dynamically or permanently disable a task, ignore errors, and record run results. We will discuss each property in turn.

The Identity group has properties that identify the task:

  • ID -- Every task in the script has a unique ID. It is assigned to the task at creation time, and it cannot be altered. This ID is referenced in the script runtime log to identify which task produced the log (in the event that multiple tasks have the same name, which is quite common actually).
  • Name -- Every task has a name. By default, the name is the same as the task's type, so it is common to have multiple tasks with the same name. You can change the name to anything you like (no characters are off limits).
  • Task Type -- This property indicates the type of the task, which can be useful if you've renamed the task and have for some reason forgotten what the type of the task is. This property cannot be altered.

The Flow group has properties that affect script flow:

  • Condition -- A boolean expression that will be evaluated just before the task executes. If the expression returns true, the task will run as normal. If the expression returns false, the task will be skipped and script flow will proceed to the next task. When this property is left blank, the task will run as normal.
  • Create Error Output Socket -- When false (the default), most tasks will only have a single output, and script flow will always proceed out of that output. When true, a second output is added to the task, and in the event of an error, the script flow will proceed out of the second output (assuming that the Stop on Error property is set to false). This makes it easy to alter script flow based on failed tasks (you don't have to add an If Then Else task to your script to make the flow decision).
  • Disable -- When false (the default), the task will run as normal. When true, the task will be skipped and script flow will proceed to the next task.
  • Input Connections Behavior -- If only a single previous task is connected to this task's input socket, then this property is ignored. If there are multiple previous tasks connected to this task's input socket, this property is vitally important. When set to "WaitForAllInputs", the task will not execute until every previous task has run and completed execution. When set to "ProceedAfterEachInput", the task will execute after each of the previous tasks completes, which means that this task (and also all subsequent tasks) may run multiple times.
  • Notify Parent of Error -- If a task is not in the top script level, this property will determine whether or not this task's parent will result in an error since this task resulted in an error. More on script levels in Chapter 3.
  • Stop on Error -- When -- When true (the default), the task will halt script execution if it results in an error (if RunResult = Failed). When false, the script flow will proceed to the next task even when this task results in an error.

The Run Results group has properties that record the result of the task once it has completed:

  • Last Error -- Contains the error message that was generated by this task if it resulted in an error. This property will be blank if no error occurred.
  • Last Exception -- Contains the exception object that was thrown by this task at runtime. This property will be blank if no exception was thrown.
  • Run Result -- Contains the RunResult value of the task. See the Run Results section for details on the various RunResult values.
  • Run Result Variable -- Enter a variable name here if you want to reference the RunResult value of this task later in your script. For example, it may be useful to know whether or not a task failed, so you can use an "If Then Else" task and the variable you define here to take a different script path if the task failed.
  • Time Finished -- Contains the time that the task completed execution.

The Expression Overrides group has a single property that allows dynamic manipulation of virtually all of a task's other properties:

  • Expressions -- This property opens an editor that will allow you to define dynamic expressions for all (or most) of the task's other properties. The expressions will be evaluated just before the task executes, and all properties for which dynamic expressions are defined will take on the value that the appropriate expression evaluates to. See the Expressions section for details on expressions.

Note that it is possible for some tasks to hide one or more of the common properties listed above when it makes sense to do so, but that is rare. For example, the "If Then Else" task hides the "Disabled" property since that task cannot be skipped (through which output would the script proceed, the true output or the false output?).

Run Results

At any given time, a task will have one of the following "Run Results":

Run Results
Figure 3 -- Run Results

  • None -- The task did not execute. This is the state that all tasks have when the script starts. If a script has already been run and the user starts the script again, all tasks are reset to this state before the script begins execution.
  • Running -- The task is currently executing. When running, the task will flash yellow in the SS interface.
  • SucceededTrue -- The task executed successfully.
  • SucceededFalse -- The script flow passed through this task, but the task did not execute. This is the state that a task receives if its Condition property is not blank and the condition evaluates to false (causing the task to be skipped).
  • Disabled -- The task was disabled when the script flow passed through it.
  • Failed -- The task failed to execute properly and resulted in an error. See the Last Error and Last Exception properties for information on what happened. You can also hover the mouse over failed tasks to see the error message.

You can reference any task's RunResult at runtime in other tasks by setting the task's Run Result Variable. The variable name that you enter into the Run Result Variable property will receive the RunResult of the task after it finishes execution. You can then use that variable in subsequent tasks to alter script behavior.

Variables

Setting and using variables are integral to any programming language, and it is no different in Script Studio. You can define variables, create them at runtime, and set and reference their values just like in any other language. Variables in SS are hard-typed, and you can call .NET class methods and reference class properties on the objects contained in the variables.

Many tasks have properties that request a variable name be entered. When the task runs, the variable will be set with a value dictated by the task. For example, the "Read File" task will set the variable named in its "Resulting Variable" property to the contents of the file that is read when the task executes. You can define these variables in the master variable list before your script runs, though you are typically not required to do so. There are two main advantages to defining your variables in the master variable list:

  1. The data type you pick will be enforced at runtime. If you do not define your variable ahead of time and a task creates the variable for you, it may mistakenly create a variable of the wrong data type (if there is a bug in your SS script for example).
  2. The pre-defined variables are available in a list when editing expressions that reference your variables. Variables created at script runtime are not always available in the expression editor.

To define a new variable, activate the Variables window (to the left of the UI) and click the Add button () located at the top right of that window. The dialog that comes up will require a variable name, a data type, and a default value (the default value can be left blank for some data types). Enter these values and close the dialog. You will see the variable in the variable window's list.

SS pre-defines several variables for you. All of the environment variables are available, along with any variables that you may have passed on the commandline (see more on commandline variables later). To see the environment and commandline variables, click the appropriate "Show ... Variable" button () in the variables window.

Expressions

Once you have variables in your script, you can reference them in expressions. All tasks have an Expressions property that can be used to set dynamic expressions for the other properties in the task. For example, using the Expressions property (see Figure 4), you can set an expression for the "Message" property of the "Message Box" task to dynamically change the message at runtime.


Figure 4 -- Property Expression Collection

When a property has an overriding expression, it will show a small lambda icon () in the property grid (Figure 5). While the property can still be edited, it does not make any sense to do so, since the value you give it in the property grid will simply be overwritten at runtime by the value to which the overriding expression evaluates.

Figure 5 -- Properties Overridden by Expressions

In addition to the Expressions property, all tasks have the Condition property that takes an expression for dynamically skipping the task, and many tasks have custom properties that also except expressions (see each individual task for the behavior of its properties).

The expression engine is implemented by the wonderful Fast Lightweight Expression Evaluator (FLEE), found here. This library is distributed under the Lesser GPL (LGPL) license. I have included a copy of the source for that project in the downloads for this article. The engine supports the following functionality (this list is copied from the FLEE website):

Arithmetic Operators
Flee supports all the standard arithmetic operators as well as the modulo (%) and power (^) operators.
Example: a*2 + b ^ 2 - 100 % 5

ComparisonOperators
All the comparison operators are supported as well. The not equal operator is <> and the equal operator is =
Example: a <> 100

And/Or/Xor/Not Operators
Flee uses these operators for both logical and bitwise operations. Since the language is strongly-typed, Flee can determine the types of the operands to these operators. If both operands are booleans, then the operation is logical. If both are integral, the operation is bitwise. Any other combination results in a compile error.
Example (logical): a > 100 And Not b = 100
Example (bitwise): (100 or 2) and 1

Shift Operators
The left (<<) and right (>>) shift operators do a bitwise shift and are only valid on integral types.
Example: 100 >> 2

Concatenation
The + operator also serves as the string concatenation operator. If either of its operands is a string, it will perform a concatenate instead of an addition. It is valid for only one operand to be a string in which case, both operands are converted to Object and formatted accordingly.
Example: "abc" + "def"
Example: "the number is: " + 100

Indexing
The indexing operator takes the form: member[indexExpression]. Any expression can appear inside the brackets. If the member being indexed is an array, Flee will emit optimized array element loading instructions. If the indexed member has a default indexer property, flee will call the property with the evaluated index. Indexing a type which is not an array and does not have a default indexer generates a compile exception.
Example: arr[i + 1] + 100

Literals
Flee supports the following literals in expressions:
  • Char - A character in single quotes: 'a'
  • Boolean - Either true or false
  • Real - Any number with a decimal point is treated as a double-precision floating-point number. Append an "f" to force the number to single-precision.
  • Integral - Any number without a decimal point. Append "L" to force the number to a 64-bit integer and/or a "U" to force it to unsigned. Flee will try to assign an integer literal to the first integral type that can contain the value.
  • Hex - Integral constants can also be specified in hex notation: 0xFF12
  • String - String literals are enclosed in double quotes and escaping characters follows the same rules as C#: "string\u0021\r\n a \"new\" line"
  • Null - Using the keyword null will load the null reference into an expression.

Casting
Casting is performed using the special cast function which takes the form cast(value, type).
Example: 100 + cast(obj, int)

Conditional Operator
Flee supports a conditional operator that allows you to pick a result based on a boolean condition. It is implemented as a special function of the form if(condition, whenTrue, whenFalse). The operator is a "true" conditional operator; meaning it only evaluates the result that corresponds to the condition.
Example: If(a > 100 and b > 10, "both greater", "less")

In Operator
The In operator is a boolean binary operator that returns true if its first operand is contained in its second operand. It has two forms:
  • List: Searches a list of values for a given value: value IN (value1, value2, value3,...). The value is compared against each value in the list and true is returned if the value is found, false if no match is found.
    Example: If(100 in (100, 200, 300, -1), "in", "not in")
  • Collection: Searches a single collection for a given value: value IN collection. The collection variable must implement ICollection<T>, IDictionary<K,V>, IList, or IDictionary for the expression to compile. Arrays can be searched with this operator as they implement the first interface.
    Example (Collection): if(100 in collection, "in", "not in")

Overloaded Operators On Types
When evaluating an arithmetic or comparison operation where the operands are not primitives, Flee will look for and use any overloaded operators defined on the operands. This means that you can create expressions such as a + b (where a and b are custom types), as long as there is an addition operator defined on either operand.

Tutorial #2 -- Replace File Contents

OK, this is a pretty contrived example, but bear with me... you'll see some good concepts here. The script that you'll create in this tutorial will do the following:

  1. Ask the user to select a file.
  2. If the user cancels the open file dialog, tell the user not to cancel, and then make the user try again.
  3. Read in the selected file.
  4. Replace every word that starts with 'S' in the file with "SS".
  5. Save the newly created content to a temp file.
  6. Open the temp file in notepad.
  7. Wait for the user to close notepad.
  8. Delete the temp file.
  9. Tell the user how many characters long the temp file was.

Let's get started. Or if you'd prefer, you can download the completed version of the script here (right-click and select "Save Target As..."). Take a look at Figure 6 and follow the steps below:


Figure 6-- Tutorial #2

  1. Create and chain up all of the tasks as shown in Figure 6. (I have not renamed any of the tasks, so the names shown are the same names you'll see in the toolbox.)
  2. Set the following properties on each task:
    1. File System Browser
      1. Error on User Cancel -- True. This is so the task results in an error if the user cancels.
      2. Result Variable -- Filename
      3. Create Error Output Socket -- True. This is so the task will have a second output, out of which the script will flow if the user cancels the dialog.
      4. Input Connections Behavior -- ProceedAfterEachInput. This is so the task will run each time script flow arrives at the input, regardless if whether or not it arrives from the Start task or the Message Box task.
      5. Stop on Error -- False. This is so the script does not stop if the user cancels (causing the task to result in an error).
    2. Message Box
      1. Message -- Pick something!
    3. Read File
      1. Resulting Variable -- FileContents
      2. Expressions -> File -- Filename
    4. Regex
      1. Match Count Type -- All
      2. Regex Match Pattern -- \b[s]\w*\b
      3. Regex Replace Path -- SS
      4. Result Variable -- AlteredContents
      5. Expressions -> Source Text -- FileContents
    5. Write File
      1. Expressions -> File -- TEMP + "\\SSTemp.txt"
      2. Expressions -> File Contents -- AlteredContents
    6. Start Process
      1. Filename -- notepad.exe
      2. Expressions -> Arguments -- TEMP + "\\SSTemp.txt"
    7. File System
      1. File Search Pattern -- SSTemp.txt
      2. Operation -- DeleteFiles
      3. Expressions -> Source Folder -- temp
    8. Message Box
      1. Expressions -> Message -- "The temporary file \"" + TEMP + "\\SSTemp.txt\" has been deleted.\n\nAfter altering the file, it was \"" + FileContents.Length + "\" characters long."

Note-worthy in this tutorial are the following items:

  • A loop is created between the File System Browser and Message Box tasks simply by chaining them in the fashion shown in Figure 6 .
  • The values of the "Error on User Cancel", "Create Error Output Socket", "Input Connections Behavior", and "Stop on Error" properties in the File System Browser task are of interest. See the notes on those properties in the steps above.
  • You may not recognize/understand the Regex task's match pattern if you are not familiar with Regex. Learn about this class! It is amazingly powerful, and you will be glad to have it in your toolbox.
  • The expression TEMP + "\\SSTemp.txt", used by several tasks, uses the TEMP environment variable.
  • The expression used for the "Message" property of the last Message Box task calls the UserFileContents.Length property. Any public method or property on objects stored in variables can be accessed directly in your expressions.

Chapter 3 -- Script Levels

Certain tasks in your SS scripts can contain a sub-collection of tasks called "subtasks" or "child tasks". Tasks that can have children are called "container tasks" or "parent tasks". Child tasks are owned by the parent task, and they run before the parent task runs.

There are two primary types of parent tasks:

  1. Those that are hard-coded to be parent tasks by the task developer (such as the "For Each" or "While" tasks), and
  2. Those that are defined at runtime by the SS user, called User Functions.

Each type will be discussed in the sections that follow.

Parent Tasks, Child Tasks, and Script Levels

When you first start creating a script in SS, you add tasks to the design surface in "level 1", the highest level. Parent tasks in level 1 have children that are said to be in level 2. And of course, parent tasks in level 2 have children in level 3. Because any script level can contain parent tasks, there are an unlimited number of levels that your script can have.

To edit child tasks, double-click on a parent task. The design surface will be replaced with only those tasks that are in the children collection for that parent. You manipulate them in exactly the same manner as you did for the tasks in the parent level. To return to the top level, click the "Go to top most level" icon () in the toolbar.

Each level of your script is like a mini SS script in and of itself. Each level contains a collection of variables, and those variables are not accessible to parent levels. Variables defined in parent levels, however, are available to child levels (no matter how many levels deep the children are). Take a look at the tooltips on the little book icons in the variables window (). You'll notice that the yellow and pink ones show variables at the current and higher levels respectively. When you create a new variable in the variables list, you are creating it at whatever level is currently active in the design surface. This is important, since you will not be able to reference that variable in tasks that reside in higher levels.

Errors that occur in any child task can be (and by default, are) propagated to the parent task. This is controlled by the Notify Parent of Error property found on all tasks (note that that property is ignored for tasks in level 1 since they have no parent task). When this property is true, any child task that results in an error will cause the parent task to also result in an error.

It is interesting to note that, even if a child task's Stop on Error and Notify Parent of Error properties are both set to true, this may not stop the script if that child errors. The Stop on Error property will only cause the script at the level of the child to stop, and script flow immediately returns to the parent task. If the parent task's Stop on Error property is false, the script will continue running from the parent task regardless of whether or not it was notified of the child's error. This means that if you want the script to stop completely if a child task errors, both the child task AND the parent task must have their Stop on Error properties set to true.

User Functions

Often you will want to create a series of tasks that perform a specific bit of functionality and then use that series over and over. User Functions allow you to do exactly that. You can create a "function" that has as many child tasks as you see fit, and then create as many instances of that function as you like.

To create a user function, go to the User Functions window (to the left of the interface) and click the Add User Function button (). You will be required to give the function a name (no restrictions here... all characters are valid). Once it is in the User Functions list, you can drag it to the design surface as many times as you like.

Alternately, you can create a user function by selecting a group of tasks, right-clicking one of them and selecting "Create Function From Selected Tasks" in the context menu. If the tasks were already chained in your script, SS will attempt to keep those chains in tact by chaining the resulting function instance to the proper tasks in the parent level, and chaining the right child task(s) to a new Start task in the child level. Experiment with this... it's kinda cool.

Editing a function is exactly like editing child tasks for any parent task. Double-click on the function task to get to the child tasks, and just work as usual. There is one major difference with functions though... any changes that you make to any instance of a function changes all instances of the function. Try it out using these simple steps:

  1. Create a new function from the User Functions window.
  2. Name the function, and then drag it from the list in the User Functions window to the design surface TWICE. You now have two copies of the function in your script.
  3. Double-click on either of the functions and add a couple tasks.
  4. Navigate back to the top level ( in the top toolbar) and then double-click on the other user function instance. You'll see that it looks exactly like the one you just left. Changes that you make to either instance affect the other.

This behavior is desirable, but comes with one caveat: when you have multiple instances of a function in your script, the child tasks will always show the run result information of the LAST function instance that ran. For example, if 1) the first instance of a function runs and a child task results in an error, 1) then the script flow proceeds to the second instance of the function but the children do NOT error (this might happen if variables changed values between the two function instances), if you then take a look at the children by navigating through first instance of the function, you will not see the child task's error that occurred when it first ran. This is because the child task now has the results from the second instance of the function stored in it. While this behavior may be changed in a future version of the program, for now you can still see that the child task failed on its first run by looking in the script log (located in the "Log" window at the bottom of the interface).

Tutorial #3 -- Counting Files

In this tutorial we will use a task that contains subtasks, and we will create a user function that has the ability to count files in a folder. We'll use that function twice in our script. The script will perform the following steps:

  1. Set the variable RegistryValueName with the value "Personal", which will be used to query the registry for the path to the user's "My Documents" folder.
  2. Run the user function, performing these steps:
    1. Query the registry for the path to the "My Documents" folder
    2. Look in that path for all existing files (not recursively)
    3. Iterate all of the files, counting them and adding them to a list.
  3. Set the variable RegistryValueName with the value "Desktop", which will be used to query the registry for the path to the user's desktop.
  4. Run the user function, performing the same steps as before, but this time iterating files on the user's Desktop rather than in the My Documents folder.
  5. Determine whether or not our list of files is over 1000 characters long (total), and if it is, truncate it at 1000 characters (this is just so we don't display a message box that is enormous (10,000+ characters).
  6. Add the file count to the end of the file list.
  7. Display a message box with the file list.

You can also download a copy of the completed script here (right-click and select "Save Target As..."). Let's get started...

Figure 7 -- Tutorial 3

  1. Create the user function:
    1. Create tasks #11-13 shown on in Figure 7. Chain them to each other (but not to the start task).
    2. Select those three tasks, right-click them and select "Create Function From Selected Tasks". Name your function "Count Files in Folder".
    3. You now have a new user function task in your script (task #3). Double-click this task and chain the "Start" task to the "Registry" Task.
    4. Return to the top level by clicking "Go to top most level" in the toolbar ().
  2. Create the main application script:
    1. Create tasks #2 and #4-9 (task #3 is already there). To add task #5, go to the "User Functions" panel to the left of the interface and drag the user function in that panel onto the design surface.
    2. Chain up all of the tasks as seen in the "Main Application" view in Figure 7.
  3. Create the For Each subtasks:
    1. Double-click on either user function (task #3 or #5).
    2. Double-click on the "For Each" task inside of the user function (task #13).
    3. Add task #15 to the design surface and chain it to the start task (task #14).
    4. Return to the top level by clicking "Go to top most level" in the toolbar ().
  4. Declare the following variables in the variables window (make sure you are in the top most level of your script):
    1. i (Int32, Default = 0)
    2. FileList (String)
    3. FilePath (String)
    4. RegistryValueName (String)
    5. Filename (String)
  5. Set properties for the main application script:
    1. Task #2 (Variables)
      1. Variables -- Add a variable, with Variable = RegistryValueName, Expression = Personal
    2. Task #4 (Variables)
      1. Variables -- Add a variable, with Variable = RegistryValueName, Expression = Desktop
    3. Task #6 (If Then Else)
      1. If Condition -- FileList.Length > 1000
    4. Task #7 (Variables)
      1. Variables -- Add a variable, with Variable = FileList, Expression = FileList.Substring(0, 1000) + "..."
    5. Task #8 (Variables)
      1. Variables -- Add a variable, with Variable = FileList, Expression = FileList + "\n\nFile Count = " + i
      2. Input Connections Behavior -- ProceedAfterEachInput
    6. Task #9 (Message Box)
      1. Expressions -> Message -- FileList
  6. Set properties for the user function. Note that you can do this in either of the two user function... editing either one will affect the other.
    1. Task #11 (Registry)
      1. Key Path -- Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
      2. Resulting Variable -- FilePath
      3. Expressions -> Value Name -- RegistryValueName
    2. Task #12 (Create File Collection)
      1. File Collection -> Include Subfolders -- false. When you click on the "File Collection" property's button, a dialog will be presented, in which you can uncheck the "Include Subfolders" checkbox.
      2. Variable Name -- Files
      3. Expressions -> Source Folder -- FilePath
    3. Task #13 (For Each)
      1. Collection Type -- IEnumerableObject
      2. Loop Variable -- Filename
      3. Expressions -> Collection -- Files
  7. Set properties for the For Each subtasks:
    1. Task #15 (Variables)
      1. Variables -- Add a variable, with Variable = i, Expression = i+1
      2. Variables -- Add a variable, with Variable = FileList, Expression = FileList + "\n" + Filename

Note-worthy in this tutorial are the following items:

  • The RegistryValueName variable is set twice (once in task #2 and once in task #4). Setting this feeds the user function, which uses this variable to determine what directory to look in.
  • The "For Each" task (#13) iterates the file collection that was created by task #12. Most collections that are passed to a "For Each" task are of the type "IEnumerableObject", which is why our For Each task's Collection Type property is set to IEnumerableObject. You can pass any object that implements IEnumerableObject to the For Each task (create the objects with tasks that support the creation of such objects... we'll discuss creating your own task types in Chapter 5).
  • The first time that the FileList variable is referenced is in task #15, which is in level 3 of your script. Note also that tasks #7, #8 and #9 all reference the variable as well. If you had not defined the variable in level 1 before the script ran, the variable would have been initially created in level 3 (by task #15), and because variables defined in child levels are not accessible to parent levels, the variable would not have been accessible in level 1 by tasks #7-9. A very similar scenario is true for the variable i.
  • Task #17 calls the "System.String.Substring(...)" method in an expression to ensure that our message box will not contain more than 1000 characters (just so it's not too big).
  • You can optionally choose not to predefine these variables in step 4 above: RegistryValueName, Filename, FilePath.

Chapter 4 -- Advanced Concepts

You now have a pretty good overview of how to use Script Studio. This chapter will discuss a few more features options that add flexibility to your scripts.

Threading

Just as in any Windows program, you can create separate threads in your SS script. Creating new threads is exceedingly easy. There are two primary ways to create new threads:

  1. Chain the output of a task to the input of multiple other tasks (see Figure 8). This creates a thread for each linked task and runs the tasks simultaneously. All threads will continue to run until the chain of tasks completes for each individual thread.
  2. Create multiple Start tasks (see Figure 8). All Start tasks will start at the same time when you execute your script (there is no "priority" associated with the Start tasks... they all start at the same time).

Figure 8-- Creating Threads

When you have multiple threads and want them to rejoin back into a single path, simply chain a single task on each thread to the first task in the rejoined path (see Figure 9).

Note that the Input Connections Behavior property will play an important role here. If that property is set to "WaitForAllInputs", your script will consolidate threads coming into that new task, proceeding only once all connected thread chains have arrived. When the property is set to "ProceedAfterEachInput", your script will immediately execute the new task (and tasks following it) after EACH thread that arrives. So if you have multiple chains connected to a task's input and you know that not every chain will have a thread that will arrive through that chain (such as in Tutorial #2), you will need to set the property to "ProceedAfterEachInput".

Figure 9 -- Rejoining Threads

Commandline Variables

You can define and set variables in your script from the command line. Variables passed on the command line can then be accessed in your script just like any other script. The syntax for defining variables on the command line is as follows:

ScriptStudio.exe VariableA=SomeValueWithoutSpaces VariableB="Some Value With Spaces" ...

The example above defines two variables, "VariableA" and "VariableB", with values "SomeValueWithoutSpaces" and "Some Value With Spaces" respectively.

You can pass as many variables as you like on the commandline. If you use quotes to surround your variable's value (as is necessary when the value contains spaces), the quotes will be stripped from the value. Note that you can only supply variables of type String on the command line.

Chapter 5 -- Creating Your Own Tasks

Script Studio probably wouldn't be very useful at all if you couldn't create your own tasks. As it turns out, it takes only a few seconds to create a new task and have it fully integrated into Script Studio. You can then add your new task's functionality by simply filling out a single method. This, of course, is the simplest scenario, but Script Studio provides a variety of options and functionality in its framework for you to leverage in your task.

It is worth mentioning that, with the exception of the Start task, every task in the Script Studio toolbox was created using the same framework and patterns that will be detailed in this chapter. The download includes the code for all of the tasks (again, except the Start task). This means that any behavior you see in the supplied tasks can be recreated using the methods described here. Take time to look through the other tasks' code when you need examples of how to accomplish certain behaviors.

This chapter will discuss creating your own Script Studio tasks in detail. The first thing you should do is verify that the Item Templates that come with the Script Studio installer were installed properly into Visual Studio for you.

Installing the Item Templates

Script Studio comes with C# and VB.NET item templates. These templates let you easily create a new task in your Script Studio library project. The installer should have installed these templaces into the proper location for you. For informational purposes, the files should be located here after the install:

  • \My Documents\Visual Studio 2005\Templates\ItemTemplates\Visual C#\ScriptStudioTaskCS.zip
  • \My Documents\Visual Studio 2005\Templates\ItemTemplates\Visual Basic\ScriptStudioTaskVB.zip

Now when you select "Add -> New Item..." in a project, a template named "Script Studio Task" should be available in the list, located at the very bottom under the "My Templates" heading.

Create Your First Task

Let's create a task that plays a sound. The script writer should be able to select the wav file containing the sound. Follow these steps:

  1. Create a new class library project (File -> New -> Project -> Class Library).
  2. Add a Script Studio Task to the project (Project -> Add New Item -> Script Studio Task)
  3. In the new task code, find the TaskTypeName property and have it return "Play Sound" in its getter.
  4. Find the TreePath property and have it return "User Interface" in its getter.
  5. Compile your project and copy the DLL to the directory where Script Studio is located.
  6. Run Script Studio.

You should now have a "Play Sound" task in your task toolbox under the "User Interface" category. Drag this task onto your design surface and connect it to the Start task. Run the script. You'll notice that the new task generates an error with an error message of "Not Implemented". This is because we've not filled out the code for the task to do anything. Let's do that now:

  1. Find the "Run" method in your task code.
  2. Make the Run method contain the following code:
public void Run()
{
    String path = Environment.ExpandEnvironmentVariables( "%SystemRoot%" );
    path = Path.Combine( path, @"Media\Chimes.wav" );
    SoundPlayer soundPlayer = new SoundPlayer( path );
    soundPlayer.Play();
}

Compile, run the program, and chain up your task again. This time the program will make a sound. Ok, let's let the user select the wav file rather than hard-coding it:

  1. Add a new property to your task's code:
private String _wavFile;
[DisplayName( "WAV File" )]
[Description( "The sound file that will be played" )]
[Category( "Behavior" )]
[Editor( typeof( OpenFileEditor ), typeof( UITypeEditor ) )]
public String WavFile
{
    get { return _wavFile; }
    set { _wavFile = value; }
}
  1. Change the Run method's code again, to this:
public void Run()
{
    SoundPlayer soundPlayer = new SoundPlayer( this.WavFile );
    soundPlayer.Play();
}

Compile, run the program, and chain up your task again. This time your task should have a new "WAV File" property. Browse to a file in your task's "WAV File" property and then run. Better yet, try adding a "Variables" task before your sound task, and in it, set a variable named wavfile to the expression SystemRoot + "\\media\\chimes.wav". Open the Expressions property in your "Play Sound" task and set the "WAV File" property expression to wavfile. Run, and see that expressions already work seamlessly in your new task.

You may have noticed that some things don't work right in your task. Loading, saving, undo and redo all fail to work right now. We'll address those functionalities in the sections to come, but first we'll cover the main interfaces through which you'll communicate with Script Studio's framework.

The ITaskImplementation Interface

The ITaskImplementation interface is the interface that Script Studio looks for when determining what tasks are available. This is the only interface you must implement for your task to appear in the SS Toolbox. The interface is declared as follows:

public interface ITaskImplementation : ISerializable
{
    void Initialize( ITaskCore taskCore );
    void Run();
    void Reset();

    string TaskTypeName { get; }
    string TreePath{ get; }
}

As we've already seen, the TaskTypeName and TreePath properties allow us to place our task in the SS Toolbox, and the Run() method is the main method that will perform your task's intended behavior. That leaves Initialize(ITaskCore taskCore) and Reset() to discuss.

The Initialize(ITaskCore taskCore) method is called only once when your task is first created. You can do an initialization that you need to do in this method. More importantly, this method supplies you with an object that implements the ITaskCore interface, which is used to communicate with the Script Studio framework (more on that interface in the next section). You should save off the ITaskCore interface instance locally so you can use it as needed. Note that the task item template already does this for you.

The most common use for the Initialize(ITaskCore taskCore) method, beyond obtaining the ITaskCore instance of course, is to add properties to the ITaskCore.HiddenProperties collection (more in this in the next section).

The Reset() method is called when the SS script first starts. You should set your local properties back to a state that is prepared for running the task in this method. This is only necessary your task's properties obtain/retain values when the task is run, and those values are not appropriate for a second run of the task. Your changed properties should be reset to default values to prepare for the second run.

The ITaskCore Interface

For each task you create, a corresponding instance of the main "TaskCore" class in the Script Studio framework is created. That instance is passed to your task in the form of an ITaskCore interface through the Initialize(ITaskCore taskCore) method mentioned in the previous section. You will communicate through this interface when you need to talk with the Script Studio framework.

The ITaskCore interface is defined as follows:

public interface ITaskCore
{
    // properties
    ITaskImplementation TaskImplementation { get; }
    Int32 ID { get; }
    String Name { get; set; }
    Boolean StopOnError { get; set; }
    String Condition { get; set; }
    RunResult RunResult { get; set; }
    Boolean CreateErrorOutputSocket { get; set; }
    DateTime? TimeFinished { get; set; }
    Exception Exception { get; set; }
    Boolean Disable { get; set; }
    PointF Location { get; set; }

    // visuals
    Single ZoomFactor { get; }
    void Invalidate();

    // inputs/outputs
    IInput AddNewInput();
    IOutput AddNewOutput();
    void RemoveInput( Int32 index );
    void RemoveOutput( Int32 index );
    NotifyingCollection Inputs { get; }
    NotifyingCollection Outputs { get; }

    // expression methods
    T EvaluateExpression( String expression );
    List HiddenProperties { get; }
	
    // logging
    void Log( String logText );
    void Log( String format, params Object[] args );

    // undo/redo
    void AddUndoItem( 
		String displayText, 
		Object undoObjectInstance, 
		String undoMethodOrPropertyName, 
		Object[] undoArguments, 
		Object redoObjectInstance, 
		String redoMethodOrPropertyName, 
		Object[] redoArguments );
    void AddUndoItem( 
		String displayText, 
		Object undoObjectInstance, 
		String undoMethodOrPropertyName, 
		Object[] undoArguments, 
		Object[] redoArguments );
    void AddUndoItem( 
		String displayText, 
		Object undoObjectInstance, 
		String undoMethodOrPropertyName, 
		Object undoArguments, 
		Object redoArguments );
    void AddUndoPropertyChangedItem( 
		String propertyName, 
		Object undoObjectInstance, 
		Object previousValue, 
		Object newValue );
    void BeginUndoChain( String displayText );
    void EndUndoChain( String displayText );

    // variable methods
    Object SetVariable( String name, Object newValue );
    void RemoveVariable( String name );
    VariableCollection GetVariables( VariableScopes scopes );
    Variable GetVariable( String name );

    // tooltip methods
    String GetToolTip();
    void SetToolTip( String message, String title, ToolTipIcon icon );
}

All methods and parameters are defined in intellisense while you are coding, so I won't repeat that information here. We will cover some of the methods in a little more detail in the sections below though, as they relate to various features available in the Script Studio framework.

Property Attributes

When you define public properties in your class that implements ITaskImplementation, these properties will be made visible to the user in the property grid in Script Studio, and they will also be available in the Expressions property so the user can set dynamic values on them at runtime.

You can define several attributes on your properties in order to make them appear and behave in the property grid as you see fit. Most of the attributes you will want to define are detailed below, though several more are available in the .NET Framework (consult MSDN documentation for details).

[DisplayName(String value)]
The name that will be shown in the property grid for this property. This name can contain any characters (such as spaces and non-alphanumeric characters).
[Category(String value)]
The category under which the property will be displayed. By convention, you should typically set the category of your task's properties to "Behavior".
[Description(String value)]
A description of your property. This description will be shown in the definition panel located at the bottom of the property grid in the UI. The description is only displayed when the user has clicked on the property.
[ExpressionEditorResultType(Type type)]
This attribute is used in conjunction with the [Editor(...)] attribute (see next paragraph below). Occasionally you will want the user to define an expression that will be evaluated at runtime directly into your property (rather than going through the Expressions property). When the user invokes the expression editor for this property, the type provided in the ExpressionEditorResultType attribute will be used by the expression editor to determine whether or not the expression defined by the user evaluates to the correct type. Said more simply, pass the type that the expression should evaluate to the ExpressionEditorResultType attribute.
[ExpressionOverrideAllowed(Boolean value)]
By default, your public properties will be available in the Expressions property so the user can dynamically set them at runtime. If you do not want the user to be able to set expressions for your property, use this attribute (pass in false) to hide the property from the Expressions property.
[EmbeddedControl(Type type)]
Tasks that display another control inside the task control on the design surface use this attribute to do so. Pass the type of your user control that is to be displayed on the design surface to this attribute. Note that your control MUST implement the IEmbeddedControl interface. Note also that this attribute is set on your task class that implements ITaskImplementation, not on a property in that class.

There is one last attribute that needs to be discussed, the [Editor(...)] attribute. This attribute is defined by the .NET Framework and is documented in the MSDN. It allows the developer to define advanced functionality for editing individual properties shown in the property grid, such as displaying complex modal dialogs or dropdown lists. See the MSDN documentation on System.Drawing.Design.UITypeEditor for details on creating your own functionality. You can also see an example of how to do this in the Script Studio GenericTasks project by searching for RegistryValueEditor.

The Script Studio framework defines several of these property editors for you in the RoundPolygons.ScriptStudio.Core.Design.PropertyEditors namespace, and you can use them on your task properties. They are as follows:

[Editor( typeof( EnumEditor ), typeof( UITypeEditor ) )]
Creates a dropdown with the enum values defined in the property's enum type. This differs from the standard dropdown in that, if the enum has the [Flags] attribute set, then the dropdown list enables multi-select.
[Editor( typeof( ExpressionEditor ), typeof( UITypeEditor ) )]
Shows a modal dialog that contains the Script Studio expression editor for editing runtime expressions.
[Editor( typeof( VariableEditor ), typeof( UITypeEditor ) )]
Shows a modal dialog that contains the Script Studio variable list for selecting variables defined in your script. The dialog also allows editing and creating variables.
[Editor( typeof( FolderEditor ), typeof( UITypeEditor ) )]
Shows the standard Windows folder browser dialog.
[Editor( typeof( OpenFileEditor ), typeof( UITypeEditor ) )]
Shows the standard Windows open file dialog.
[Editor( typeof( SaveFileEditor ), typeof( UITypeEditor ) )]
Shows the standard Windows save file dialog.
[Editor( typeof( StringEditor ), typeof( UITypeEditor ) )]
Shows a modal dialog with a large edit box for editing long strings. The editor optionally supports word wrap.

Supporting Serialization and Versioning

In any application I write that I expect will need to be able load older versions of saved files, I follow the design pattern that I will outline in this section. When I say. "load older versions of saved files", I mean the ability to upgrade the application to a newer version and still load files that were saved with a previous version (much like you can do with Microsoft Word when you upgrade to the latest version).

To accomplish this, the developer of Script Studio tasks has to do a little extra work. Specifically, you will need to fill out a serialization method (GetObjectData(...)) and a deserialization constructor (MyTask(...)). We'll go through an example to illustrate the pattern.

Let's say that you have a single property MyOnlyProperty in your MyTask task that you want serialized. You later upgrade your task to also have a second property, MyAdditionalProperty. The code below shows the code in the first version of your task:

01  /// <summary>
02  /// This field holds the current version of the task.  Increment this value when 
    /// you upgrade your task to have new functionality.
03  /// </summary>
04  private const String _version = "1";
05  
06  /// <summary>
07  /// Deserialization constructor.  Use this method to recreate your 
    /// task's state when the user loads a file from disk.
08  /// </summary>
09  protected MyTask( SerializationInfo info, StreamingContext context )
10  {
11      CommonConstructor();
12      String version = info.GetString( "MyTaskVersion" );
13      switch( version )
14      {
15          case _version:
16              this.TaskCore = (ITaskCore)info.GetValue( "TaskCore", typeof( ITaskCore ) );
17              this.MyOnlyProperty = info.GetString( "MyOnlyProperty" );
18              break;
19
20          default:
21              throw new SerializationException( "Version not recognized" );
22      }
23  }
24
25  /// <summary>
26  /// Serialization method.  Use this method to save your task's state when 
    /// the user saves a file to disk.
27  /// </summary>
28  public void GetObjectData( SerializationInfo info, StreamingContext context )
29  {
30      info.AddValue( "MyTaskVersion", _version );
31      info.AddValue( "MyOnlyProperty", this.MyOnlyProperty );
32  }

You'll notice that in the GetObjectData(...) method, no versioning takes place... we always save the latest version of the file, including saving the version of the task on line 30. In the MyTask(...) constructor however, we load properties from the SerializationInfo object based on the version that was saved to disk. We determine what version was saved on line 12, and execute the switch statement to ensure that we load only data that we know was saved with that version.

The following code shows the same snippet of code after the task has been upgraded to version "2" (red lines are changed, blue ones are new):

01  /// <summary>
02  /// This field holds the current version of the task.  Increment this value when you upgrade 
    /// your task to have new functionality.
03  /// </summary>
04  private const String _version = "2";
05  
06  /// <summary>
07  /// Deserialization constructor.  Use this method to recreate your task's state 
    /// when the user loads a file from disk.
08  /// </summary>
09  protected MyTask( SerializationInfo info, StreamingContext context )
10  {
11      CommonConstructor();
12      String version = info.GetString( "MyTaskVersion" );
13      switch( version )
14      {
15          case _version:
16              this.TaskCore = (ITaskCore)info.GetValue( "TaskCore", typeof( ITaskCore ) );
17              this.MyOnlyProperty = info.GetString( "MyOnlyProperty" );
18              this.MyAdditionalProperty = info.GetString( "MyAdditionalProperty" );
19              break;
20
21          case "1": // was previously "case _version:"
22              this.TaskCore = (ITaskCore)info.GetValue( "TaskCore", typeof( ITaskCore ) );
23              this.MyOnlyProperty = info.GetString( "MyOnlyProperty" );
24              break;
25
26          default:
27              throw new SerializationException( "Version not recognized" );
28      }
29  }
30
31  /// <summary>
32  /// Serialization method.  Use this method to save your task's state 
    /// when the user saves a file to disk.
33  /// </summary>
34  public void GetObjectData( SerializationInfo info, StreamingContext context )
35  {
36      info.AddValue( "MyTaskVersion", _version );
37      info.AddValue( "MyOnlyProperty", this.MyOnlyProperty );
38      info.AddValue( "MyAdditionalProperty", this.MyAdditionalProperty );
39  }

Notice that we are now saving our new property during serialization (line 38) and loading it on deserialization (line 18). If the user happens to load a file that was saved with the previous version of the task, we will not attempt to load the new property because the older loading code will do the deserialization (lines 21-24). This means that it is important for us to set a default value for our new property in the declaration of the property's storage field.

Supporting Undo/Redo

The Script Studio framework supports unlimited undo/redo functionality, but the task developer has to do some extra work to support it in his task. The Undo/Redo framework keeps track of a stack of "undo items". Each time the user performs an action that can be undone, an undo item is added to this stack. When the user selects "Undo", the framework pops the top undo item off of the stack, executes it, and adds it to the redo stack. If the user selects "Redo", the reverse occurs.

It is the task developer's job to add the undo items to the Undo/Redo stack. An undo item contains the following information:

Display Text
The text that will be displayed in the undo list for this item.
Undo Object Instance
The instance of the object that will handle your undo when it is invoked.
Undo Method or Property Name
A public method or property name on the undoObjectInstance that will be invoked to handle your undo.
Undo Arguments
A list of arguments that will be passed to the undoMethodOrPropertyName method when undo is invoked.
Redo Object Instance
The instance of the object that will handle your redo when it is invoked.
Redo Method or Property Name
A public method or property name on the redoObjectInstance that will be invoked to handle your redo.
Redo Arguments
A list of arguments that will be passed to the redoMethodOrPropertyName method when redo is invoked.

So basically, you let the framework know what method you want to be called when the undo and redo events occur, and what arguments should be passed in each case. You then create the methods to handle those undo and redo events.

To create a new unto item, the ITaskCore interface provides 4 methods to supply the proper information, 3 of which are named AddUndoItem, and one of which is named AddUndoPropertyChangedItem. The first AddUndoItem override takes all pieces of information mentioned above. It is the most flexible, allowing separate objects to handle the undo and redo operations, and also allowing different arguments to be supplied to each of those two methods. The second AddUndoItem override assumes that a single object and method will handle both the undo and the redo operations, requiring a different set of parameters for each operation. The third AddUndoItem override is the same as the second, except that it assumes that the method that will be invoked to do undo/redo operations only require a single parameter rather than a list of them.

Finally, the AddUndoPropertyChangedItem method assumes that the user simply changed a property's value, and undoing that action involves just setting the value back to the original value. The method takes the property name, an object instance, and the previous and new values of the property (the property's setter method will be the handler for both the undo and the redo operations).

You can search the GenericTasks project for AddUndoItem and AddUndoPropertyChangedItem to see examples of how to use these methods.

The IEmbeddedControl Interface

You may have noticed that a few tasks have various user controls embedded into their main task control on the design surface (Sleep and Gauge for example). You can easily place a System.Windows.Forms.UserControl in your task's main control using the IEmbeddedControl interface and the EmbeddedControl class attribute. You can then interact with those controls at runtime, or you can even let the user set properties on your task through that user control right on the design surface (as opposed to or in addition to using the property grid).

There are only 3 steps needed to embed your own UserControl into the task's control:

  1. Create your UserControl.
  2. Implement the IEmbeddedControl interface on your UserControl.
  3. Add the EmbeddedControl attribute on your ITaskImplementation class, passing your UserControl's type to the attribute.

See the GaugeControl class for an example of implementing the IEmbeddedControl interface, and see the GaugeTask class for an example of using the EmbeddedControl attribute.

The IEmbeddedControl interface is defined as follows:

public interface IEmbeddedControl
{
    void SetTask( ITaskImplementation task );
    void ResetChildrenToOriginalSizeAndPosition( ITaskImplementation task );
}

The following is how the GaugeControl embedded control (for the GaugeTask) implements that interface:

public void SetTask( ITaskImplementation task )
{
    // save the task instance passed to us
    this.Task = (GaugeTask)task; 
}

public void ResetChildrenToOriginalSizeAndPosition( ITaskImplementation task )
{
    // do nothing
}

As you can see, there is not much to do when implementing this interface. You will typically save off the task instance passed to you in the SetTask( ITaskImplementation task ) method so your control and task can interact. Not really anything else to do there.

The ResetChildrenToOriginalSizeAndPosition( ITaskImplementation task ) method often does not require any code at all. However, if you have several child controls in your user control, you may find that after zooming and then unzooming in the design surface, your child controls do not return to their proper size and position. If this happens, you will need to add code to this method to return your child controls to the proper size and position. The easiest way to do this is to simply copy the code in the WinForms designer file's InitializeComponent() method for your UserControl into the ResetChildrenToOriginalSizeAndPosition( ITaskImplementation task ) method (copy only those parts that are necessary of course, not the whole thing).

The IScriptFlow Interface

Some tasks require non-standard script flow behavior. Standard tasks have a single input and a single output, and script flow always proceeds out of that single output. The If/Then/Else task, though, has two outputs, and script flow proceeds out of one or the other based on the result of a boolean expression. The Switch task is another good example of a task that implements a non-standard script flow, because the flow will proceed out of one of any number of outputs based on an expression.

You can create alternate script flow behaviors by implementing the IScriptFlow interface. That interface is defined as follows:

public interface IScriptFlow
{
    List<IConnection> NextConnections { get; }
}

The NextConnections property's getter is called by the framework when the task has finished running and it is time to proceed to the next set of tasks (which is typically only a single task, unless the user has connected more than one task to your task's output socket. See the section on Threading for details on connecting multiple tasks to an output). It is in that NextConnections property's getter that you decide which connections should be run next. When the getter method is called, the steps are basically as follows:

  1. Create a new List<IConnection> collection to return as your result of this method.
  2. Loop through your ITaskCore.Outputs collection, making decisions about which outputs you want executed.
  3. For outputs that you want executed, loop through the IOutput.Connections property, adding each IConnection to your result list of connections.
  4. Return your result list of connections.

As an example, the following is a slightly simplified version of the If/Then/Else task's implementation of IScriptFlow (see the full version by looking in the download code):

[Browsable( false )]
public List<IConnection> NextConnections
{
    get
    {
        // evaluate the IfCondition
        if( String.IsNullOrEmpty( this.IfCondition ) )
            throw new InvalidOperationException( "You must supply a value in 
                the 'If Condition' property for this task to run properly." );

        // create our result collection
        List<IConnection> results = new List<IConnection>();

        // evaluate our IfCondition expression
        Boolean expressionResult = this.TaskCore.EvaluateExpression<Boolean>( this.IfCondition );

        // determine which output to follow based on the expressionResult
        if( expressionResult )
        {
            // expression was 'true'
            this.TaskCore.RunResult = RunResult.SucceededTrue;
            foreach( IConnection connection in this.TaskCore.Outputs[0].Connections )
                results.Add( connection );
        }
        else
        {
            // expression was 'false'
            this.TaskCore.RunResult = RunResult.SucceededFalse;
            foreach( IConnection connection in this.TaskCore.Outputs[1].Connections )
                results.Add( connection );
        }
    }

    // return our results
    return results;
}

As a final note, you will want to add the [Browsable(false)] attribute to the NextConnections property so that it does not appear in Script Studio's property grid when the task is selected by the user.

The IContainerTaskImplementation Interface

As you are aware by now, some tasks can have child tasks. The child tasks run before the parent tasks runs, and the children can pass their error states up to the parent (see Chapter 3 for details on parent and child tasks). Any task that can contain child tasks implements the IContainerTaskImplementation instead of the ITaskImplementation interface. The IContainerTaskImplementation interface is defined as follows:

public interface IContainerTaskImplementation : ITaskImplementation
{
}

The simplicity is almost disappointing, no? The interface derives from the ITaskImplementation interface and does not add any functionality. The only reason for the interface to exist is to signal the framework that this task is allowed to have children. Other than this, implementing a container task is identical to implementing a regular vanilla non-container task. Well, except for one additional option: the option to implement the IChildrenScriptFlow interface.

The IChildrenScriptFlow Interface

By default, all container tasks will execute their child tasks once (actually, they will just start the Start tasks once, and flow will proceed as normal from the Start tasks). After the child tasks are done, the parent task executes it's Run() method and then script flow proceeds to the next task(s).

Sometimes you will want to alter this behavior. For example, the "For Each" task executes its child tasks once for each item in a collection. In order to achieve this kind of functionality, you should implement the IChildrenScriptFlow interface on your task class (the same class that implements the IContainerTaskImplementation interface).

The IChildrenScriptFlow interface is defined as follows:

public interface IChildrenScriptFlow
{
    void RunChildSnippet( IProgramEngine engine, String parentTaskPath );
}

Unlike the in IScriptFlow interface's NextConnections property, your job in the IChildrenScriptFlow interface's RunChildSnippet( IProgramEngine engine, String parentTaskPath ) method is not to supply which tasks to run, but instead, to execute your parent task's child tasks as a group as you see fit. For example, the "While" task checks a boolean expression for true/false, and when true, it executes the child tasks. It then checks the expression again and executes the child tasks again. This continues until the expression returns false. Here is the "While" task's implementation of the RunChildSnippet(...) method:

01  public void RunChildSnippet( IProgramEngine engine, String parentTaskPath )
02  {
03      if( String.IsNullOrEmpty( this.ConditionExpression ) )
04          throw new InvalidOperationException( "You must supply a value in 
                the 'Condition Expression' property for this task to run properly." );
05      
06      // check the loop condition 
07      Boolean conditionResult = 
            this.TaskCore.EvaluateExpression<Boolean>( this.ConditionExpression );
08	
09      // loop!
10      while( conditionResult )
11      {
12          // run the child tasks
13          engine.RunSnippet( this.TaskCore.ChildSnippet, parentTaskPath, false );
14        
15          // check the loop condition
16          conditionResult = 
                this.TaskCore.EvaluateExpression<Boolean>( this.ConditionExpression );
17      }
18  }

The key thing to note here is that you execute the child tasks with the code on line 13. This line will basically never change... you can typically just cut and paste it into your own task's code as is.

Extra Tasks Website

The best thing to do when you begin creating new tasks is just to look through the tasks in the GenericTasks project. You can see how anything is accomplished in that project's code.

Additionally, there is a website for you to upload and download tasks to share with the community. If you have a task that is useful, please take the time to share it with the rest of us!

Thanks, and happy scripting!

History

  • Version 0.9.15.0 -- 12/13/2008

    • Initial Revision
    • This is a beta
    • This application will remain free after beta

Roadmap

These are some of the features that are planned (and in some cases, already nearly complete) for future versions of Script Studio:

  • Full Skinning Capabilities -- Create your own task control look and feel.
  • Task events -- Publish events in your tasks. Users can subscribe to your events and add tasks that respond to the event being fired.
  • Debugging -- Integrated debugging with "step into", "step over", breakpoints, and other typical debugger capabilities.
  • Compiled Scripts -- After creating and debugging your scripts, compile your script into a stand-alone executable that does not need Script Studio to run.
  • User Interface Support -- Substantially better support for dynamically displaying user interfaces with your scripts. Wizard-creation tasks, etc.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here