![]() |
Desktop Development »
Miscellaneous »
General
Intermediate
License: The Code Project Open License (CPOL)
Script Studio, a Drag-n-Drop Programming InterfaceBy Steve SchanevilleUse 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. |
C#, Windows, Dev
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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:

The download is an MSI installer package. After install:
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).
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.
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.
Navigating SS is pretty easy. The interface consists of 3 primary windows:
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.
) 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. Consider practicing using the functions listed above until you are comfortable with them before moving on to the next section.
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.
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.
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.

Figure 2 -- Completed Tutorial #1 Script
Perform these steps for Tutorial #1:
You should probably check this box! isChecked OneCheckBox isChecked = true Thanks, you checked the box. SS Tutorial #1 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). I told you to check the box! Unchecked Message 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.
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.
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:
The Flow group has properties that affect script flow:
The Run Results group has properties that record the result of the task once it has completed:
The Expression Overrides group has a single property that allows dynamic manipulation of virtually all of a task's other properties:
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?).
At any given time, a task will have one of the following "Run Results":
Figure 3 -- Run Results
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.
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:
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.
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):
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:
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:
True. This is so the task results in an error if the user cancels. Filename True. This is so the task will have a second output, out of which the script will flow if the user cancels the dialog. 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. False. This is so the script does not stop if the user cancels (causing the task to result in an error). Pick something! FileContents Filename All \b[s]\w*\b SS AlteredContents FileContents TEMP + "\\SSTemp.txt" AlteredContents notepad.exe TEMP + "\\SSTemp.txt" SSTemp.txt DeleteFiles temp "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:
TEMP + "\\SSTemp.txt", used by several tasks, uses the TEMP environment variable. UserFileContents.Length property. Any public method or property on objects stored in variables can be accessed directly in your expressions. 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:
Each type will be discussed in the sections that follow.
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.
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:
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).
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:
RegistryValueName with the value "Personal", which will be used to query the registry for the path to the user's "My Documents" folder. RegistryValueName with the value "Desktop", which will be used to query the registry for the path to the user's desktop. 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
i (Int32, Default = 0) FileList (String) FilePath (String) RegistryValueName (String) Filename (String) RegistryValueName, Expression = Personal RegistryValueName, Expression = Desktop FileList.Length > 1000 FileList, Expression = FileList.Substring(0, 1000) + "..." FileList, Expression = FileList + "\n\nFile Count = " + i ProceedAfterEachInput FileList Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders FilePath RegistryValueName 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. Files FilePath IEnumerableObject Filename Files i, Expression = i+1 FileList, Expression = FileList + "\n" + Filename Note-worthy in this tutorial are the following items:
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. 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. RegistryValueName, Filename, FilePath. 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.
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:
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
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.
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.
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:
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.
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:
TaskTypeName property and have it return "Play Sound" in its getter. TreePath property and have it return "User Interface" in its getter. 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:
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:
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; } }
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 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.
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 ); NotifyingCollectionInputs { 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.
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)] [Category(String value)] [Description(String value)] [ExpressionEditorResultType(Type type)] [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)] 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)] 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 ) )] [Flags] attribute set, then the dropdown list enables multi-select. [Editor( typeof( ExpressionEditor ), typeof( UITypeEditor ) )] [Editor( typeof( VariableEditor ), typeof( UITypeEditor ) )] [Editor( typeof( FolderEditor ), typeof( UITypeEditor ) )] [Editor( typeof( OpenFileEditor ), typeof( UITypeEditor ) )] [Editor( typeof( SaveFileEditor ), typeof( UITypeEditor ) )] [Editor( typeof( StringEditor ), typeof( UITypeEditor ) )] 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.
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 Undo Object Instance Undo Method or Property Name Undo Arguments Redo Object Instance Redo Method or Property Name Redo Arguments 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.
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:
IEmbeddedControl interface on your UserControl. 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).
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:
List<IConnection> collection to return as your result of this method. ITaskCore.Outputs collection, making decisions about which outputs you want executed. IOutput.Connections property, adding each IConnection to 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.
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.
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.
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!
Version 0.9.15.0 -- 12/13/2008
These are some of the features that are planned (and in some cases, already nearly complete) for future versions of Script Studio:
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 2 Jan 2009 Editor: Sean Ewington |
Copyright 2009 by Steve Schaneville Everything else Copyright © CodeProject, 1999-2010 Web10 | Advertise on the Code Project |