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.