Developing external tools add-in with ProcessStartInfo and Process classes for PragmaSQL Editor


Download Source Code

Download PragmaSQL External Tools Add-In Source

 

Introduction

PragmaSQL T-SQL editor has very extensive add-in support. External Tools add-in presented in this article has two goals

1. Serve as PragmaSQL add-in development example.
2. Provide a very common feature included in all development IDEs and editors.

IC#Code Add-In Architecture Overview

PragmaSQL makes use of IC#Code's Add-In architecture. This architecture provides
fantastic fatures that enables us to develop plugins for our applications easly thus
making our applications extendable. IC#Code Add-In architecture basicly provides

1 - Simple and neat XML add-in definition via .addin files
2 - Loading of add-in assemblies
3 - Expose add-in functionality through menus and toolbars of the host application
4 - And many utility services like MenuService and MessageService

For details about the architecture please refer to SODA - SharpDevelop Open Development Architecture by Mike Krueger

PragmaSQL Services Overview

PragmaSQL exposes many of the built-in host features to the add-in developers with a core librarary.


Exposed features include

1- Host Options
2- Editor Services: Access to T-SQL script editor and text editor
3- Object Explorer Service: Access to database object explorer
4- Project Explorer Service
5- Shared Scripts and Code Snippets
6- Internal web browser
7- Text Diff Service
8- Code completion lists
9- Application messages service

Entry point to all these services is HostServicesSingleton, as its name mimics this class is a singleton.

HostServicesSingleton Usage Example From PragmaSQL.ExternalTools

Example code provided below shows

1) How we can use Application Message Service to print messages into Host Application Messages window.
2) Evaluate macros with HostServices EvalMacro function and prepare tool arguments

private void process_Exited(object sender, EventArgs e)

{

        Process p = sender as Process;

        if (p == null)

               return;

 

        long handle = p.Handle.ToInt64();

        if (!_runningToolDefs.ContainsKey(handle))

               return;

 

        try

        {

               ExternalToolDef def = _runningToolDefs[handle];

              

               //Here we clear application messages window

                if (chkClearOutput.Checked)

                       HostServicesSingleton.HostServices.MsgService.ClearMessages();

 

               bool shallShow = false;

 

               while (p.StandardOutput.Peek() > -1)

               {

                       string info = p.StandardOutput.ReadLine();

                       if (!String.IsNullOrEmpty(info))

                       {

                               // Print info message to Applicatin Messages Window

                               HostServicesSingleton.HostServices.MsgService.InfoMsg(info, "Tool : " + def.Title, String.Empty, String.Empty);

                               shallShow = true;

                       }

               }

 

               while (p.StandardError.Peek() > -1)

               {

                       string error = p.StandardError.ReadLine();

                       if (!String.IsNullOrEmpty(error))

                       {

                               // Print error message to Applicatin Messages Window

                               HostServicesSingleton.HostServices.MsgService.ErrorMsg(error, "Tool : " + def.Title, String.Empty, String.Empty);

                               shallShow = true;

                       }

               }

 

               if (shallShow == true)

                       HostServicesSingleton.HostServices.MsgService.ShowMessages();

        }

        finally

        {

               _runningToolDefs.Remove(handle);

        }

}





private void RenderExternalToolDef(ExternalToolDef exDef)

{

        tbCmd.Text = String.Empty;

        tbArgs.Text = String.Empty;

        tbWorkingDir.Text = String.Empty;

       

        if (exDef == null)

               return;

              

        tbCmd.Text = exDef.Command;

        // Here we prepare arguments by evaluating macros

        tbArgs.Text = HostServicesSingleton.HostServices.EvalMacro(exDef.Args);

        tbWorkingDir.Text = exDef.WorkingDir;

}

 

              

NOTE: In order to develop PragmaSQL Add-Ins you need to get PragmaSQL.Core.dll. All host functionality and many utility classes are hosted in this assembly. Download from here

PragmaSQL.ExternalTools

Add-In Definition

<AddIn name        = "External Tools AddIn for PragmaSQL"

       author      = "Ali Özgür"

       description = "Enables you to define external tools for PragmaSQL">

        <Manifest>

               <Identity name = "PragmaSQL.ExternalTools"/>

        </Manifest>

 

        <Runtime>

               <Import assembly="PragmaSQL.ExternalTools.dll"/>

        </Runtime>

 

        <Path name = "/Workspace/ToolsMenu">

               <MenuItem id = "ExtTools.Configure"

                                               label = "External Tools..."

                                               class ="PragmaSQL.ExternalTools.ConfigureTools"/>

               <MenuItem id = "ExtTools.Run"

                                               label = "Run External Tool"

                                               shortcut     = "Control|Shift|E"

                                               class ="PragmaSQL.ExternalTools.RunExternalTool"/>

        </Path>

</AddIn>

 

In the add-in definition file above we provide the description of our add-in and how our add-in integrates to PragmaSQL. Most important part of this add-in definition is the Path tag. We provide the predefined host path along with the MenuItems we want to be created for our add-in.

Another very important tag is Class. IC#Code Add-In architecture makes use of Command Pattern. We define commands associated to the specified menu/toolbar items with Command classes through this tag. In the above example for External Tools... menu item you can see that we want PragmaSQL.ExternalTools.ConfigureTools command to be invoked. ConfigureTools command class inherits from AbstractMenuCommand and has Run() method.

NOTE: Predefined host paths can be found in Base.addin file that comes with PragmaSQL installation.

ExternalTools Add-In Specific Classes

·  ExternalToolDef: Serialazable class which is used to hold external tool configuration data for a single tool

·  ExternalToolsCfg: Static class which is used to load and save serialized tool configuration data from a file into an IList<ExternalToolDef>
static instance.Tool configuration items are accessible through Current public static property.

·  ConfigureTools: Command class inherited from AbstractMenuCommand. This command is used to show tool configuration form

·  RunExternalTool: Command class inherited from AbstractMenuCommand. This command is used to show run tool form.

·  ConfigForm: Tool configuration form. Form is opened with ConfigureExternalTools public static method.
This method returns one of these DialogResult enumeration values

OK: User pressed OK button and changes to tool configuration applied

Ignore: User pressed OK button and no changes to tool configuration exist

Cancel: User pressed Cancel button.

·  RunToolForm: The form that lists external tool definitions and used to run a selected tool.

 

Running a tool by using Process and ProcessStartInfo

.NET Framework provides ProcessStartInfo and Process classes under System.Diagnostics namespace that can be used to run an external process.
from our code. ExternalTools add-in makes use of these classes from RunToolForm.

RunTool function looks like:

private void RunTool()

{

        if (CurrentDef == null)

               return;

 

        ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo();

        tbArgs.Text = HostServicesSingleton.HostServices.EvalMacro(CurrentDef.Args);

        psi.FileName = CurrentDef.Command;

        psi.Arguments = tbArgs.Text;

        psi.WorkingDirectory = CurrentDef.WorkingDir;

        psi.RedirectStandardOutput = CurrentDef.UseOuput;

        psi.RedirectStandardError = CurrentDef.UseOuput;

        psi.CreateNoWindow = CurrentDef.UseOuput;

        psi.UseShellExecute = !CurrentDef.UseOuput;

       

        Process p = new Process();

        p.EnableRaisingEvents = CurrentDef.UseOuput;

        if (CurrentDef.UseOuput)

               p.Exited += new EventHandler(p_Exited);

       

        p.StartInfo = psi;

        p.Start();

 

        if (CurrentDef.UseOuput)

               _runningToolDefs.Add(p.Handle.ToInt64(), CurrentDef);

}

 

As you can see there is nothing special about starting a process. We simply define the fileName, arguments and working directory of the process with a ProcessStartInfo class and then create a Process instance using this ProcessStartInfo.


Interesting points in this implementation are

1) Standard output and error redirecting.

You can redirect standard output/error from a process anywhere you like providing these

  • RedirectStandardOutput and RedirectStandardError properties of the ProcessStartInfo instance set to true
  • UseShellExecute property of ProcessStartInfo instance set to false
  • External tool prints output and errors to standard output. For example you can not redirect output from a windows application
    however you can redirect output from a console application.

2) Synchronous vs Asynchronous Output Reading

Process class provides synchronous vs asynchronous reading of redirected output/error. However in implementation choosing between one of the methods makes a big difference. In our implementation it seems as if we have chosen synchronous output reading. But that is not true. Take a look at the RunTool method again. We attach to the Exited event of the Process instance with p.Exited += new EventHandler(p_Exited); on line 17. After implementing synchronous read in mind p_Exited method threw cross-thread call exception. This exception indicated two problems

  • 1- p_Exited method was called from a different thread thus indicating the operation was asynchronous
  • 2- PragmaSQL Message Service did not supported cross thread calls.

Simply I fixed PragmaSQL Message Service to support cross thread calls by adding some Invoke() related code and that was it.

Conclusion

External tool support was a general requirement for PragmaSQL. In this article we covered IC#Code Add-In support, how PragmaSQL makes use of IC#Code Add-In architecture, what services does PragmaSQL exposes to provide a pluggable/extendable application and some initial insight to PragmaSQL Add-In development with source code examples.

 


Similar Articles