使用 Jira SOAP API 为 PragmaSQL T-SQL Editor 开发 Jira 客户端
介绍如何使用 PragmaSQL 插件支持开发自定义 Jira 客户端
- 下载二进制文件 - 200 KB
-
下载 JiraAddIn_Source.zip - 1.08 MB
引言
T-SQL 过程会随着时间的推移而快速变化,因为会修复发现的错误,业务规则会改变,并且需要进行重构。必须跟踪所做的更改的原因,最好附加一些额外的信息,此外,还应该能够搜索这些更改和附加的信息。源代码中的注释可能会有所帮助,但过一段时间后,它们会比源代码本身更庞大,外部链接难以浏览且在不进行一些黑客攻击的情况下无法搜索。
解决方案是采用一个 bug 跟踪系统,记录其中的任务、依赖项和过程的历史记录,并用其生成的密钥注释源代码。
背景
PragmaSQL 是一个用 C# 开发的优秀的 T-SQL 编辑器,它利用了 IC#Code 插件架构。它公开了一些底层功能,这些功能被封装并分类到一些优秀的类中。
JIRA 是我最喜欢的 bug 跟踪系统。它提供了一个可以通过自动化其功能的 Web 服务。
当我意识到该编辑器可以扩展时,我决定在编辑器中创建一个 Jira 客户端插件,可以简化管理系统中大量 T-SQL 过程的开发周期。在我深入研究 PragmaSQL.Core 程序集并得到作者 Ali Ozgur 的帮助后,这个想法形成了一个项目。
Using the Code
为了将自定义插件代码集成到 PragmaSQL 应用程序中,需要使用 XML 描述。该格式应符合 Mike Krueger 在 SODA 中定义的规则。在结构良好的定义中,指定了如何集成到主菜单、特定上下文菜单以及启用和禁用集成项的条件。
PragmaSQL.Core 公开了一个服务,可以通过该服务与编辑器内容进行交互。例如,
HostServicesSingleton.HostServices.EditorServices
使插件代码能够查询正在使用的编辑器窗口的状态。.addin 描述文件
SODA 规范详细揭示了约定的内部工作原理,但检查描述文件中的一些片段可能具有实际意义。尽管如此,作者 Ali Ozgur 实现了另一个用于 PragmaSQL 的插件。 他关于外部工具插件的文章 也可能阐明一些实现细节。JIRAClient.addin 描述文件中的以下片段订阅了 PragmaSQL.Core.dll 中与配置服务相关的一些预定义事件。可以通过
HostServicesSingleton.HostServices.ConfigSvc
访问该服务。名为Workspace/Autostart
的路径下的命令在应用程序启动时运行,是挂钩 PragmaSQL.Core 触发的事件的好地方。. . . <Path name = "/Workspace/Autostart"> <Class id = "JIRAClientSubscribeToEventsCommand" class = "JIRAClientAddin.SubscribeToEventsCommand"/> </Path> . . .
下面JIRAClientAddin.SubscribeToEventsCommand
的实现说明ucJIRAOptions
具有一些用于配置服务的处理程序。AbstractMenuCommand
定义在 ICSharpCode.Core.dll 中,自定义命令需要从中派生。需要注意的是,
ucJIRAOptions
实际上需要是一个实现IConfigContentEditor
接口的用户控件。当用户从主菜单中选择工具/选项并渲染用户控件时,事件会相应地触发。如果它是一个窗体对象,则无法将其渲染到包装器窗体中。这为核心应用程序和已安装的插件代码提供了序列化用户首选项的标准方法。public class SubscribeToEventsCommand : AbstractMenuCommand { public override void Run() { HostServicesSingleton.HostServices.ConfigSvc.DialogOpened += ucJIRAOptions.ConfigSvc_DialogOpened; HostServicesSingleton.HostServices.ConfigSvc.DialogClosed += ucJIRAOptions.ConfigSvc_DialogClosed; HostServicesSingleton.HostServices.ConfigSvc.FinalSelection += ucJIRAOptions.ConfigSvc_FinalSelection; } }
通过 JIRAClient.addin 描述文件中的以下片段,插件的功能可以从主菜单中访问。第一个菜单项的声明描述了BrowseIssueCommand
类负责为名为“Browse issue”的功能提供入口点。该类应在 JIRAClientAddin.dll 中实现命令模式。这样,PragmaSQL.Core.dll 期望类中存在一个Run
方法,并保证在主菜单中选择该项时会调用此方法。<Path name = "/Workspace/ToolsMenu"> <MenuItem id ="JIRAClientMainMenu" label="JIRA" type="Menu"> <MenuItem id = "JIRAClientMainMenu_BrowseIssue" label = "Browse issue" class = "JIRAClientAddin.BrowseIssueCommand" shortcut="F4"/> . . . </MenuItem> </Path>
下面是BrowseIssueCommand
类。public class BrowseIssueCommand : AbstractMenuCommand { public override void Run() { IssueBrowser.BrowseIssue(); } }
以下部分将相同的功能注册到编辑器窗口的上下文菜单中。. . . <Path name = "/Workspace/ScriptEditor/ContextMenu"> <MenuItem id ="JIRAClientContextMenu_TopSeparator" type="Separator"/> <MenuItem id ="JIRAClientContextMenu_JIRAContext" label="JIRA" type="Menu"> <MenuItem id = "JIRAClientContextMenu_BrowseIssue" label = "Browse issue" class = "JIRAClientAddin.BrowseIssueCommand" shortcut="F4"/> . . . </MenuItem> </Path> . . .
PragmaSQL.Core 中还有一些路径可用于集成自定义代码。经过一些尝试和错误,就会变得清晰。在 PragmaSQL 应用程序安装所在的文件夹中的 AddIns 文件夹下有一个 Base.addin 文件。Base.addin 文件列出了应用程序公开的可用的路径名称。该软件实际上也利用该架构与自身的特性进行交互。JIRA SOAP API
JIRA 实现了一个功能齐全的 Web 服务,只需向 C# 项目添加 Web 引用即可轻松代理其对象模型。Web 引用应指向您的 JIRA 安装的 /rpc/soap/jirasoapservice-v2?wsdl 路径。提供了关于类及其之间关系的 文档,并且相当完整。在客户端插件中,一个名为
JIRAFacade
的类封装了对 JIRA 服务的调用,提供了简单的缓存机制和一些错误检查。JIRAClientAddin
命名空间包含用户界面,并依赖于JIRAFacade
。在用户界面中,对于可能耗时的操作,首选异步方法,以免应用程序本身挂起。JIRAClient 插件提供的功能
- 创建 JIRA 问题:激活此功能会弹出一个问题输入表单。创建问题后,会在光标位置留下一个引用行。该功能绑定到 ALT-J 组合键。
- 浏览问题:此功能会扫描当前编辑器中的问题引用,并将它们收集到一个列表中,用户可以从中选择一个。双击列表会在内置浏览器中打开并导航到选定的问题。为了方便起见,如果只有一个引用或某个引用已被高亮显示,列表将被抑制,浏览器将直接导航。该功能绑定到 F4 键。
- 循环浏览:这是问题浏览的支持功能。激活它会逐个扫描并高亮显示当前编辑器中的引用,使其易于在源代码中查看引用。按下 F4 键(当引用被高亮显示时),浏览器会直接导航到它。该功能绑定到 CTRL-J 组合键。
- 我的过滤器:这可以被认为是主要功能。截至 3.12.1 版本,JIRA 目前不允许发布动态构建的自定义标准,但可以远程调用先前准备好的自定义标准集(或者可以说成保存的搜索)。准备好的自定义标准集称为过滤器,有一些方法可以检索过滤器、运行它们并检索匹配它们的 issue。插件的功能仅在于为此提供一个 GUI,并在上下文菜单中提供一些额外功能。
-
选项:该插件利用 PragmaSQL.Core 提供的基本
HostServicesSingleton.HostServices.ConfigSvc
来序列化用户凭据和首选项。
关注点
项目的主要方面是为 PragmaSQL 构建一个插件。JIRA SOAP API 也很有趣。它为我提供了一个测试平台来提高我的 C# 技能,但对于 C# 老手来说,实际上并没有什么新东西。包含的代码说明了一切,有些部分可能被认为是巧妙的,而有些部分则很幼稚。由于注释不多,有些部分可能一开始难以理解。我努力修复了我遇到的 bug,但可能仍然散布着一些 bug。尽管如此,一个值得一提的有趣任务可能是双击过滤器时获取 issue。我的过滤器中的异步过滤器获取
以下是JIRAFacade
单例中与此任务相关的一些字段。UserAsyncState
结构实际上没有任何意义,除了外观在给定时间是否有一个实例。Web 服务方法_service.getIssuesFromFilterAsync
仅为匹配的 issue 提供 issue 键。需要进行第二次传递以获取每个返回 issue 的相关详细信息。也就是说,我们应该首先获取过滤器结果,然后请求每个 issue 的详细信息。再次,请求详细信息的异步过程将通过 Web 服务方法_service.getIssueAsync
进行。IssueList
类被设计用来处理时序和事件处理,并且仅仅是IssuelistItem
数据对象的容器。private JiraSoapServiceService _service = new JiraSoapServiceService(); private IssueList _filterAsyncResult; private UserAsyncState _getFromFilterAsyncState = null; public delegate void OnGetFromFilterCompleteEvent(); private OnGetFromFilterCompleteEvent _onGetFromFilterCompleteNotify; public OnGetFromFilterCompleteEvent OnGetFromFilterCompleteNotify { get { return _onGetFromFilterCompleteNotify; } set { _onGetFromFilterCompleteNotify = value; } }
在frmMyFilters
中,当列表框中的某个项被双击时,会解析过滤器名称,并将其与IssueList
实例的引用一起馈送到下面的方法中,该实例最终将保存将要获取的 issue。public UserAsyncState RunSavedFilterAsync(string filterName, IssueList filterResult) { _filterAsyncResult = filterResult; RemoteFilter f = this.FilterByName(filterName); // if it is not null, there is already a filter on the run. // apperantly, the user has double clicked another filter before the previous double clicked filter // finishes its run. cancel it and run the newly clicked filter. if (_getFromFilterAsyncState != null) _service.CancelAsync(_getFromFilterAsyncState); _getFromFilterAsyncState = new UserAsyncState(Guid.NewGuid()); // request from web service _service.getIssuesFromFilterAsync(_auth, f.id, _getFromFilterAsyncState); // let caller to know that a filter is running. return _getFromFilterAsyncState; }
私有方法OnGetFromFilterAsyncComplete
在构造函数中与 _service.getIssuesFromFilterCompleted 事件关联。. . . _service.getIssuesFromFilterCompleted += new getIssuesFromFilterCompletedEventHandler(OnGetFromFilterAsyncComplete); . . .
请求完成后将运行它,并为我们提供进一步处理的机会。private void OnGetFromFilterAsyncComplete(object sender, getIssuesFromFilterCompletedEventArgs e) { // if we still have a container to hold the IssueItems carry on else silently exit. // An appropriate exception might be thrown, also if (_filterAsyncResult == null) return; // release the state object _getFromFilterAsyncState = _getFromFilterAsyncState == e.UserState ? null : _getFromFilterAsyncState; // if the request is cancelled before it completes, cancel further processing if (e.Cancelled) { // cancel if detail for any IssueListItem has been requested and is on its way foreach (IssueListItem item in _filterAsyncResult) _service.CancelAsync(item); // let the container IssueList know that request is cancelled _filterAsyncResult.Cancel(); } else { // empty the container _filterAsyncResult.Clear(); // insert newly fetched issues and request their details foreach (RemoteIssue issue in e.Result) { IssueListItem item = new IssueListItem(); item.IssueKey = issue.key; _filterAsyncResult.Add(item); } _filterAsyncResult.FetchDetailsAsync(); } // notify listeners that we are done with the filter and probably waiting for the details to complete if (_onGetFromFilterCompleteNotify != null) _onGetFromFilterCompleteNotify(); }
“取消”按钮调用以下方法以取消frmMyFilter
中的挂起请求。public void RunSavedFilterAsyncCancel(object userState) { // cancel the getFromFromFilter request // if detail for any IssueListItem has been requested and is on its way, cancel them too if (_filterAsyncResult != null) foreach (IssueListItem item in _filterAsyncResult) _service.CancelAsync(item); _service.CancelAsync(userState); }
在
IssueList
类中,为异步 issue 获取量身定制了类似的布局,并提供了一些基础设施,如进度条。FetchDetailsAsync
方法通过外观发出获取详细信息的请求,IssueListItemCompleteEvenHandler
收集 issue 详细信息已完成的信息,并在所有详细信息完成后触发OnFetchDetailsCompleteEvent
。请注意,通过这种方式,frmMyFilters
中的网格仅刷新一次。插件安装
历史
本文最初伴随 PragmaSQL JIRAClient 插件 的首次发布。