Create a Custom Action with JavaScript

Prerequisites

Note: Before you attempt to create a custom action, you need to be familiar with creating UI Model projects and creating a UI Model on a data form. The following content also assumes familiarity with object-oriented concepts in JavaScript and how JavaScript can enhance UIModel-based data forms and other features. JavaScript Starters Guide explains how encapsulation, polymorphism, and inheritance, the three tenets of object-oriented programming, are accomplished in JavaScript, with VB.NET examples provided for comparison. Writing JavaScript for the Infinity Platform explains how to setup your Visual Studio development and how to write custom JavaScript specifically for a UI model form.  This sample also assumes familiarity with Search lists, and Tasks, and Data Forms.   

Note: You can download the source code for this sample here. The code sample is within the Blackbaud.CustomFx.FoodBank.UIModel project within a JavaScript file named ChooseCorrectFoodBankConstituentPageAction.js. The JavaScript file lives within a the project's htmlforms\custom\blackbaud.customfx.foodbank folder.

Figure: The JavaScript file that defines the behavior for the custom action

Food Bank Sample Custom Action

Let's create a custom action for our food bank customization within Blackbaud CRM. This example assumes you already downloaded the food bank source code and loaded the specs referenced by the Food Bank Package Spec (FoodBank.Package.xml) into the catalog system. This loads the prerequisite, tables, data forms, data lists, pages, etc. into the database. This example also assumes you used the Add Food Bank task to add a few food bank records into the USR_FOODBANK custom table

Our custom action will be called from a new task spec that allows the end user to search for an organization. This new task spec uses the out-of-the-box Organization Search list to retrieve the action context (Constituent ID) and then call a custom JavaScript action to determine of the constituent (organization) is a food bank. If so, the action directs the user to the Food Bank page. Otherwise, the action directs the user to the traditional Constituent page.

Before we build the custom action and TaskSpec, let's take a peek and review what we are going to build. First, we click the TaskSpec to call upon a search list to retrieve the action context value, which is an organization’s unique identifier (constituent ID):

Figure: Figure 18: Invoke the task

Figure: Retrieve the context value (constituent ID) for the custom action

In the figure above, if we select an organization that is also a food bank, the custom action directs us to the Food Bank page.

Figure: The custom action can pivot to either the Food Bank page (shown here) or the Constituent page

On the other hand, if we select "East Cooper Fire Department," which is not a food bank, the custom action directs us to the familiar Constituent page.

Figure: The custom action can pivot to either the Food Bank page or the Constituent page (shown here)

Create a Custom Action

Now, let's dig in and create the custom action followed by a task spec to call upon the custom action. Within the UIModel Project, add a new item into the appropriate htmlforms subfolder. This way the JavaScript file is copied along with the other HTML and JavaScript files. With the Add New Item dialog window open, select Web Shell Custom Action as the template.  

Figure: Add the Web Shell Custom Action item template to the UI model project

After you create your JavaScript file (in my case, I named the file "ChooseCorrectFoodBankConstituentPageAction.js" and placed it in the htmlforms\custom\blackbaud.customfx.foodbank folder within in my UI model project), you are ready to code. Open and review the JavaScript file. Notice that the item template provides some boilerplate code. Let's review this code.

Add JSLint Validation Rules

At the top of the file, you should note the JSLint validation rules used to validate the JavaScript code for errors. The first line indicates to JSLint the various options to turn on when validating the JavaScript (all possible options are documented here). 

/*jslint bitwise: true, browser: true, eqeqeq: true, onevar: true, undef: true, white: true */
/*extern BBUI, Ext, $ */
/*JSLint documentation: http://www.jslint.com/lint.html */

We will reference the latest version of JSLint, which provides for stricter rule checks for our JS code. Add the following above the JSLint options at the top of the JS file:

// See "Setting up Visual Studio for JavaScript Development" within the on-line development guides
// use the latest version of jsLint with stricter rule checks
/*lintversion 2*/

/*jslint bitwise: true, browser: true, eqeqeq: true, onevar: true, undef: true, white: true */
/*extern BBUI, Ext, $ */
/*JSLint documentation: http://www.jslint.com/lint.html */

Indicate Global Variables

The next step is indicate the global variables that are defined outside of our JavaScript file. The second line (/*global BBUI, Ext, $ */) indicates global variables that are defined outside of this particular JavaScript file so the "undef" rule doesn't fail when it encounters these variables in use. If a variable is not explicitly declared, then JavaScript assumes that the variable is global. A good discussion on the evils of global variables in JavaScript can be found here

Replace "/*extern BBUI, Ext, $ */ " with" /*global BBUI, Ext, $ */".

The top of your code should now look like this:

// See "Setting up Visual Studio for JavaScript Development" within the on-line development guides
// use the latest version of jsLint with stricter rule checks
/*lintversion 2*/
 
// add jslint validation rules to help validate the code for errors
// use Blackbaud.AppFx.UIModeling.HtmlLint.exe to validate both JavaScript and HTML
// 3rd party developers can obtain Blackbaud.AppFx.UIModeling.HtmlLint.exe can be obtain
// from the on-line developer guides
/*jslint bitwise: true, browser: true, eqeqeq: true, undef: true, white: true, indent: 4*/
 
// the /*global BBUI...line below indicates global variables that are 
// defined outside of this particular JavaScript file so the “undef” rule 
// doesn’t fail when it encounters these variables in use.  
// Typically all variables should be defined with a “var“ statement.
// this is what the  undef: true within the jslist options above checks 
/*global BBUI, Ext, $ */

Self-invoking Function

The next thing to note is the self-invoking function where your JavaScript code should reside, just as if you were writing custom JavaScript for a UI model form. Below are the beginnings of the self invoking function:

// Encapsulate all logic inside a self-invoking function so objects are not added to the 
// global (window) object.
(function () {

Let's add the ECMAScript 5 strict mode option by adding the "use strict" line of code just below the beginning of the function. Since JavaScript is a very loosely defined language, use strict helps to catch common errors and prevents access to global objects. You can read more about it here.

// Encapsulate all logic inside a self-invoking function so objects are not added to the 
// global (window) object.
(function () {
 'use strict'; //enforces latest ECMAScript5

The next piece of code to review is the declaration of variables within the self-invoking function. Note the usage of the BBUI global variable to help set the value of ActionUtil. This will not violate "undef: true" or "use strict" since we indicated we wanted to use the BBUI global variable via "/*global BBUI, Ext, $ */".  

We will not utilize the BBUI.pages.ActionUtility in this sample; but, we will use the BBUI.forms.Utility. Remove the reference to the ActionUtil variable. See  below.

 
// Declare any static objects or constants here.  Variables you declare here are shared 
 // across multiple instances of this action and should never be assigned during the 
 // execution of an action.
 //
 // Useful utility methods can be found in BBUI.forms.Utility (usually aliased as "Util") 
 // and BBUI.pages.ActionUtility (usually aliased as "ActionUtil").  See the BBUI 
 // documentation for more information about methods available on those utility classes.
 varActionUtil = BBUI.pages.ActionUtility,
        ns,
        Util = BBUI.forms.Utility;

With the removal of ActionUtil, your variable declarations should look like this:

// Encapsulate all logic inside a self-invoking function so objects are not added to the 
// global (window) object.
(function () {
 'use strict'//enforces latest ECMAScript5
 // Declare any static objects or constants here.  Variables you declare here are shared 
 // across multiple instances of this action and should never be assigned during the 
 // execution of an action.
 //
 // Useful utility methods can be found in BBUI.forms.Utility (usually aliased as "Util") 
 // and BBUI.pages.ActionUtility (usually aliased as "ActionUtil").  See the BBUI 
 // documentation for more information about methods available on those utility classes.
 
 // Immediately using BBUI is an exception to the 
 //   "define variables before we use them" rule
 //   since it is a global variable... see /*global BBUI...line above.
 var ns,
 Util = BBUI.forms.Utility;

"Lint" Your Code

Let’s "lint" our code. “Linting” is slang for checking the code using a JSLint-based tool. We will use the Blackbaud.AppFx.UIModeling.HtmlLint.exe utility, which is a JavaScript syntax checker and validator. "Linting" is the next best thing to compiling. Assuming you have set up your development environment appropriately for JavaScript, run the Blackbaud.AppFx.UIModeling.HtmlLint.exe utility from the Tools menu within Visual Studio to check our JavaScript file. If you receive no errors, your Output window will look like this:

Figure: Lint the code…. No errors!

Create a Namespace

Next you need to ensure the namespace where you'll create your custom action object exists. Because you add your object to an already-global object (namely the BBUI object), it is very important that you choose a unique name. To ensure that you don't stomp on existing BBUI objects, the namespace "BBUI.customactions" is reserved for custom actions. All custom actions should be added to the BBUI.customactions" namespace. Ensure the namespace for your action exists. The naming convention for custom action namespaces is "BBUI.customactions.yournamespace" where "yournamespace" is the lower-case type of the UI model project where this file is stored. For instance, if you create a custom action in the Blackbaud.AppFx.FoodBank.UIModel project, your action's namespace would be "BBUI.customactions.foodbank." Note that namespace components after the "BBUI" component should be all lower-case, not camel-case or proper-case. Ensuring the existence of a namespace can be accomplished by calling the BBUI.ns() function along with the namespace's name. If the namespace exists, the function does nothing; if it doesn't exist, the function creates it. 

 (function () {
 'use strict'//enforces latest ECMAScript5
 
 // Declare any static objects or constants here.  Variables you declare here are shared 
 // across multiple instances of this action and should never be assigned during the 
 // execution of an action.
 //
 // Useful utility methods can be found in BBUI.forms.Utility (usually aliased as "Util") 
 // and BBUI.pages.ActionUtility (usually aliased as "ActionUtil").  See the BBUI 
 // documentation for more information about methods available on those utility classes.
 // must define variables before we use them.
 
 // Immediately using BBUI is an exception to the 
 //   "define variables before we use them" rule
 //   since it is a global variable... see /*global BBUI...line above.
 var ns, 
        Util = BBUI.forms.Utility;
 
 // Ensure the namespace for your action exists.  The naming convention for custom action 
 // namespaces is "BBUI.customactions.yournamespace" where "yournamespace" is the lower-case 
 // type of the UI model project where this file is stored.  For instance, if you are creating
 // a custom action in the Blackbaud.AppFx.FoodBank.UIModel project, your action's namespace 
 // would be "BBUI.customactions.foodbank".
 ns = BBUI.ns("BBUI.customactions.foodbank");

Best Practices

Create the Custom Action's Constructor

Now you're ready to create your JavaScript object that represents the custom action. This requires two steps: create the constructor and add methods to its prototype.

JavaScript uses a prototype inheritance-based system where objects are created on the fly. To create a constructor, you create a function that is really no different from a regular JavaScript function. The main thing to note with JavaScript constructors is the assigning of the host object to "this." When defining a constructor, "this" refers to the object that we are currently working with, and in this case, the object we are working with is the ChooseCorrectFoodBankConstituentPageAction object.

// This is the constructor for the custom action.  The constructor will always be passed a
// host object.  See the BBUI documentation for BBUI.pages.ActionHost for information about
// properties and methods available on the host object.
// http://helephant.com/2009/01/18/javascript-object-prototype/
ns.ChooseCorrectFoodBankConstituentPageAction = function (host) {
// Cache the host object so it can be referenced later.
this.host = host;
};

The constructor takes one argument, "host," that is passed in from the platform code that invokes it. The host object is of type BBUI.pages.ActionHost and provides methods to custom JavaScript actions for interacting with the page. At run time, the platform passes a pre-defined instance of BBUI.pages.ActionHost to the constructor of your custom action. Below is a quick sample of some of the properties and methods the host object makes available to you:

As you can see, the host object has lots of useful properties, so I'll be sure to cache it in my constructor for later use:

(function () {
'use strict';  //enforces latest ECMAScript5
 
// Declare any static objects or constants here.  Variables you declare here are shared
// across multiple instances of this action and should never be assigned during the
// execution of an action.
//
// Useful utility methods can be found in BBUI.forms.Utility (usually aliased as "Util")
// and BBUI.pages.ActionUtility (usually aliased as "ActionUtil").  See the BBUI
// documentation for more information about methods available on those utility classes.
// must define variables before we use them.
 
// Immediately using BBUI is an exception to the
//   "define variables before we use them" rule
//   since it is a global variable... see /*global BBUI...line above.
var ns,
Util = BBUI.forms.Utility;
 
// Ensure the namespace for your action exists.  The naming convention for custom action
// namespaces is "BBUI.customactions.yournamespace" where "yournamespace" is the lower-case
// type of the UI model project where this file is stored.  For instance, if you are creating
// a custom action in the Blackbaud.AppFx.FoodBank.UIModel project, your action's namespace
// would be "BBUI.customactions.foodbank".
ns = BBUI.ns("BBUI.customactions.foodbank");
// This is the constructor for the custom action.  The constructor will always be passed a
// host object.  See the BBUI documentation for BBUI.pages.ActionHost for information about
// properties and methods available on the host object.
// http://helephant.com/2009/01/18/javascript-object-prototype/
ns.ChooseCorrectFoodBankConstituentPageAction = function (host) {
// Cache the host object so it can be referenced later.
this.host = host;
};

JavaScript Objects Are Prototyped-based and Defined at Run Time

Now that I created my constructor, I'm ready to add properties to its prototype. JavaScript is prototype-based. Objects are implemented differently than in other object oriented languages such as VB.NET, Java, C#, or C++. In VB.NET for example, objects are defined at design time by coding a class that represents the blueprint or cookie cutter of the object that is created from the class at runtime. JavaScript doesn't work this way. In JavaScript, we use a constructor function to define our object, such as ChooseCorrectFoodBankConstituentPageAction. Then we can utilize the prototype property of the constructor function to extend the constructor function by adding functions and properties at runtime. For more information, see Inheritance in the JavaScript Starters Guide.

Add the executeAction Property to the Prototype

Now that I created my constructor, I'm ready to add properties to its prototype. The first function I'll add is the executeAction() function. Remember that since JavaScript is a loosely typed language, I can add any method to any object, and as long as that method exists at runtime, it can be called by code that depends on a method with that name.

Below is a simple example of test executeAction() function that takes an argument called "callback." Because the platform assumes you might perform some potentially long-running process like fetching data from the database, the function is called asynchronously with a callback function as its argument that should be called when the action is done executing. In the example below, I simply show an alert and immediately call the callback function.

BBUI.customactions.webshelltest.MyCustomAction.prototype = {
 
executeAction: function (callback) {
alert('hi');
callback();
}
 
};
}());

Now that you have seen a simple example of adding the executeAction property to a custom action's prototype, lets continue building our food bank custom action. Let's add the executeAction function using the prototype property of our object. Remember that since JavaScript is a loosely typed language, I can add any method to any object, and as long as that method exists at runtime, it can be called by code that depends on a method with that name. The Infinity platform invokes executeAction when the user clicks the action on a page or a task within a functional area.

Before we delve into the code for the custom action, let me explain the task's and custom action's logic via pseudocode:

The entire code for the JS file is listed below.

// Many thanks to Paul Crowder for his help with this sample.
// Creating a Custom Action with JavaScript let's you create your own action beyond the
// Infinity Platform's predefined action types.  Custom Actions are the Webshell replacement
// for CLR Actions.  CustomActions only work in the Webshell and will not be visible
// within the ClickOnce Smart Client.
// this Custom Action is called from FoodBankOrgSearchCustomAction.Task.xml within the food
// bank catalog project.
// See "Setting up Visual Studio for JavaScript Development" within the on-line development guides
// use the latest version of jsLint with stricter rule checks
/*lintversion 2*/
// add jslint validation rules to help validate the code for errors
// use Blackbaud.AppFx.UIModeling.HtmlLint.exe to validate both JavaScript and HTML
// 3rd party developers can obtain Blackbaud.AppFx.UIModeling.HtmlLint.exe can be obtain
// from the on-line developer guides
/*jslint bitwise: true, browser: true, eqeqeq: true, undef: true, white: true, indent: 4*/
// the /*global BBUI...line below indicates global variables that are
// defined outside of this particular JavaScript file so the “undef” rule
// doesn’t fail when it encounters these variables in use.
// Typically all variables should be defined with a “var“ statement.
// this is what the  undef: true within the jslist options above checks
/*global BBUI, Ext, $ */
// Encapsulate all logic inside a self-invoking function so objects are not added to the
// global (window) object.
// See "Don’t add objects to the global namespace"  and
//  "Wrapping your code in a self-invoking function" within the on-line
//   development guides
(function () {
'use strict';  //enforces latest ECMAScript5
 
// Declare any static objects or constants here.  Variables you declare here are shared
// across multiple instances of this action and should never be assigned during the
// execution of an action.
//
// Useful utility methods can be found in BBUI.forms.Utility (usually aliased as "Util")
// and BBUI.pages.ActionUtility (usually aliased as "ActionUtil").  See the BBUI
// documentation for more information about methods available on those utility classes.
// must define variables before we use them.
 
// immediately using BBUI is an exception to the
//   "define variables before we use them" rule
//   since it is a global variable... see /*global BBUI...line above.
var ns,
Util = BBUI.forms.Utility;
 
// Ensure the namespace for your action exists.  The naming convention for custom action
// namespaces is "BBUI.customactions.yournamespace" where "yournamespace" is the lower-case
// type of the UI model project where this file is stored.  For instance, if you are creating
// a custom action in the Blackbaud.AppFx.FoodBank.UIModel project, your action's namespace
// would be "BBUI.customactions.foodbank".
ns = BBUI.ns("BBUI.customactions.foodbank");
// This is the constructor for the java script object that represents our custom action.
// The constructor will always be passed a host object (BBUI.pages.ActionHost) by the Infinity platform when the custom
// action is called upon at run time, by a TaskSpec, for example.
// See the BBUI documentation for BBUI.pages.ActionHost for information about
// properties and methods available on the host object.
// https://www.blackbaud.com/files/support/guides/infinitytechref/Content/apidocs-BB_2-91/index.html?class=BBUI.pages.ActionHost
// Helpful javascript articles
// http://helephant.com/2009/01/18/javascript-object-prototype/
ns.ChooseCorrectFoodBankConstituentPageAction = function (host) {
// Cache the host object so it can be referenced later.
// "this" keyword is a special javascript operator that provides a reference to the object
//you are creating
this.host = host;
};
// Instance methods are declared as properties of the action's prototype object.
// So below we have a property named executeAction
// the Infinity platform will invoke executeAction when the user clicks the button or
// link to which the action is mapped or when Task is clicked.
ns.ChooseCorrectFoodBankConstituentPageAction.prototype = {
 
executeAction: function (callback) {
 
// Note that callback is deliberately not called since it does not
// make sense in the context of this script, analogous to ShowPage
// actions not having a PostAction.
 
// we are going to use a view data form to tell us if a
// constituent (organization) is a food bank.
var dataFormInstanceId = "23119D4E-3F04-4931-8219-A5AE9E3587B3",
options,
constituentId,
hostRef;
 
// Prior to this JavaScripts execution, TaskSpec will call upon the Organization Search
// to determine the constituent id/org id/context value.
// Grab the action context (constituent ID/org ID) from the
// search list's return value
constituentId = this.host.getContextRecordId();
hostRef = this.host;
 
// Soon we will call upon the view data form to determine if the
// searched constituent is a food bank.  If all goes well with the
// call to the view data form, the successCallback function will be called upon
// to direct the user to the appropriate page.
// So let's define the successCallback function before we load the data form
function successCallback(result) {
 
var isFoodBankField,
isFoodBank,
constituentPageId,
foodBankPageId;
 
isFoodBankField = BBUI.findByProp(result.values, "name", "ISFOODBANK");
 
isFoodBank = false;
if (BBUI.is(isFoodBankField)) {
isFoodBank = isFoodBankField.value;
}
 
// grab the static parameter value from the task.
// check out the parameters defined within the ParameterList element
// within FoodBankOrgSearchCustomAction.Task.xml
constituentPageId = hostRef.getParameterValue("ConstituentPageID");
foodBankPageId = hostRef.getParameterValue("FoodBankPageID");
 
// use nav to redirect the user to the final destination page.
// Note that callback is deliberately not called since it does not
// make sense in the context of this script, analogous to ShowPage
// actions not having a PostAction.
if (isFoodBank) {
hostRef.nav.goToPage(foodBankPageId, null, constituentId);
}
else {
hostRef.nav.goToPage(constituentPageId, null, constituentId);
}
}
// Soon we will call upon the view data form to determine if the
// searched constituent is a food bank.  If all does NOT goes well with the
// call to the view data form (like no data returned by the view data form),
// the failureCallback function will be called upon.
// So let's define the failureCallback function here before we load the data form
function failureCallback(request, error) {
Util.alert(error);
}
 
// lets define the options for the view data form. The data form will need to know
// the record to retrieve from the database.  pass the value of the constituentId
// as the recordId for the view data form.
options = {
recordId: constituentId
};
 
// Finally, once the success and fail callback methods defined and we have prepared the
// options object with a value for the recordId property. call the dataFormLoad to
// determine if the constituentId is a food bank.
// Use callbacks to asynchronously branch the code depending on whether the load was
// successful or unsuccessful.  Pass the options into the call which contains the value of our
// searched constituent id.
this.host.webShellSvc.dataFormLoad(dataFormInstanceId, successCallback, failureCallback, options);
 
}
 
};
 
}());

Call a Custom Action from a Task

Using a Task Spec to call a custom action is very similar to how CLR actions are declared, except that instead of a ComponentIdentifier element, I used a ScriptIdentifier element. The ScriptIdentifier element has two properties:

By specifying these two pieces of information, the platform will download the script file, instantiate the custom action object, and call its executeAction() method when a user clicks on the action.

After you load the spec, the task should show up in Web Shell as normal and you'll be ready to test our your custom action. Below is the XML for the task spec that calls our custom action. As you review the XML, note the StaticParameters and ActionContext and how the values related to each of the elements are utilized within the custom action.

<TaskSpec
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
ID="2D9AA05E-C8A1-4BE3-924C-7B73EF0DDD6A"
Name="Food Bank Org Custom Action Search"
Description="Uses the stock org search list to retrieve
the action context (constituent id) and then calls a custom action (JavaScript)
to redirect to either the constituent page or the food bank page."
Author="Blackbaud Product Development"
FunctionalAreaID="2b32333c-5cd7-4e54-a3d3-a41b382cdfa9"
Sequence="3"
xmlns="bb_appfx_task">
 
<ExecuteCLRAction xmlns="bb_appfx_commontypes">
<!--
To calling a custom action within a task place a ScriptIdentifier tag within
the ExecuteCLRAction tag.  ScriptIdentifier is the replacement for the
winform based ComponentIdentifier element.  ComponentIdentifier elements
are not compatible with the WebShell web based user interface.
 
The URL is relative to the location where the JS file resides on the
Infinity web application's virtual directory.  The ObjectName is the
fully qualified name of your JavaScript custom action object.
The ParameterList values from the StaticParameters will be available
within the custom action ChooseCorrectFoodBankConstituentPageAction object.
In addition, the context record id value provided by the ActionContext
tag will also be available.
-->
<ScriptIdentifier Url="browser/htmlforms/custom/blackbaud.customfx.foodbank/ChooseCorrectFoodBankConstituentPageAction.js"
ObjectName="BBUI.customactions.foodbank.ChooseCorrectFoodBankConstituentPageAction">
 
<!--
The static parameter values are GUIDs which uniquely identify two pages.
The custom action will use either of these ids to navigate to the correct
page depending on whether the organization is a food bank.
-->
	<StaticParameters>
		<ParameterList>
           	<Param ID="ConstituentPageID">
				<Value>88159265-2b7e-4c7b-82a2-119d01ecd40f</Value>
			</Param>
			<Param ID="FoodBankPageID">
				<Value>2ca4f7de-41d0-4131-8c1b-5a532385794a</Value>
			</Param>
		</ParameterList>
	</StaticParameters>
</ScriptIdentifier>
 
<!--
ActionContext is used to retrieve a context record id BEFORE the heart of the
action is executed.  The ActionContext below calls upon the Organization Search list
which allows the user to search for an org.  The ID returned by the search list
is then passed to the custom action by the platform.  The code within the
ChooseCorrectFoodBankConstituentPageAction custom action object will utilize
this context record id value.
-->
<ActionContext>
	<SearchListReturnValue SearchListID="ef1da4e7-0631-49de-bd60-7d084cb7cb2b">
		<AddDataForms>
			<AddDataForm ID="ca3ed110-a5f0-4b5b-8eb7-0616e0a82e8e" Caption="Add"/>
		</AddDataForms>
	</SearchListReturnValue>
</ActionContext>
<!--
Note that this task does not utilize a PostAction which tells the task to do
something else after the primary function of the task has been completed.
If it did then we would have to call the callback function within our action action.
<PostActionEvent>
</PostActionEvent>
-->
</ExecuteCLRAction>
 
</TaskSpec>

Conclusion

Just like the ClickOnce Smart Client, Web Shell provides the ability to go above and beyond the Infinity platform's pre-defined action types to create a rich user experience. Just like the rest of the platform, new features str more than likely be added to custom actions as developers need them, but this should be enough to get most people started writing custom actions for Web Shell.