Custom Search Dialog¶
This guide shows how to build a WinForms search dialog that lets users find and insert components from an external system. The dialog is triggered by a menu item with resultType: "component".
How it works¶
- User clicks "Search ERP..." in the Plugin menu.
- HydroSym calls
OnMenuActionwithmenuItemId = "erp-search"andparentWindowHandle. - Your plugin shows a WinForms dialog, modal over HydroSym.
- User searches, selects a component, and clicks OK.
- You return the selected component in the
OnMenuActionresponse. - HydroSym inserts the component into the schematic.
Declare the menu item¶
new MenuItem
{
Id = "erp-search",
Label = "&Search ERP Parts...",
Location = "plugin",
Icon = "search",
ResultType = "component",
Shortcut = "Ctrl+Shift+F",
}
Implement OnMenuAction¶
public override OnMenuActionResponse OnMenuAction(OnMenuActionRequest request)
{
// Use STA thread for WinForms
OnMenuActionResponse? result = null;
var thread = new Thread(() =>
{
Application.EnableVisualStyles();
var owner = new NativeWindowWrapper(new IntPtr(request.ParentWindowHandle));
var dialog = new ErpSearchDialog(_db, request.Context);
if (dialog.ShowDialog(owner) == DialogResult.OK && dialog.Result is not null)
{
result = new OnMenuActionResponse
{
ArticleCode = dialog.Result.ArticleCode,
Description = dialog.Result.Description,
Manufacturer = dialog.Result.Manufacturer,
ManufacturerPartNo = dialog.Result.ManufacturerPartNo,
Properties = dialog.Result.Properties,
};
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return result ?? new OnMenuActionResponse(); // empty = user cancelled
}
STA thread requirement
WinForms requires a Single-Threaded Apartment (STA) thread. HydroSym's main thread is MTA. Always create a new STA thread for showing dialogs.
Modal ownership¶
Pass parentWindowHandle to the dialog so it is modal over HydroSym's window. Without this, the dialog can go behind HydroSym.
// Helper wrapper so WinForms ShowDialog accepts a native HWND as owner
internal class NativeWindowWrapper : IWin32Window
{
public NativeWindowWrapper(IntPtr handle) => Handle = handle;
public IntPtr Handle { get; }
}
The search dialog¶
public partial class ErpSearchDialog : Form
{
private readonly SqlConnection _db;
private readonly PluginContext _context;
public ErpSearchResult? Result { get; private set; }
public ErpSearchDialog(SqlConnection db, PluginContext context)
{
_db = db;
_context = context;
InitializeComponent();
}
private void SearchButton_Click(object sender, EventArgs e)
{
string query = searchBox.Text.Trim();
if (query.Length < 2) return;
resultsGrid.DataSource = SearchErp(query);
}
private List<ErpSearchResult> SearchErp(string query)
{
const string sql = """
SELECT TOP 100
ArticleCode, Description, Manufacturer, StatusCode
FROM Articles
WHERE ArticleCode LIKE @q OR Description LIKE @q
ORDER BY ArticleCode
""";
using var cmd = new SqlCommand(sql, _db);
cmd.Parameters.AddWithValue("@q", $"%{query}%");
var results = new List<ErpSearchResult>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
results.Add(new ErpSearchResult
{
ArticleCode = reader.GetString(0),
Description = reader.GetString(1),
Manufacturer = reader.GetString(2),
Status = reader.GetString(3),
});
}
return results;
}
private void OkButton_Click(object sender, EventArgs e)
{
if (resultsGrid.CurrentRow?.DataBoundItem is ErpSearchResult selected)
{
Result = selected;
DialogResult = DialogResult.OK;
Close();
}
}
private void ResultsGrid_DoubleClick(object sender, EventArgs e)
=> OkButton_Click(sender, e);
}
public class ErpSearchResult
{
public string ArticleCode { get; set; } = "";
public string Description { get; set; } = "";
public string Manufacturer { get; set; } = "";
public string Status { get; set; } = "";
public string? ManufacturerPartNo { get; set; }
public Dictionary<string, string>? Properties { get; set; }
}
Using context in the dialog¶
The PluginContext passed to OnMenuAction contains the currently open project and selected component. Use it to pre-filter results:
// Pre-filter to components relevant to the current project's pressure class
string? pressureClass = _context.ProjectPath is not null
? GetProjectPressureClass(_context.ProjectPath)
: null;
Handling cancellation¶
If the user closes the dialog or clicks Cancel, return an empty response:
HydroSym does not insert anything when the response is empty.
Keyboard navigation tips¶
- Implement
AcceptButton(OK) andCancelButton(Cancel) on the form for Enter/Escape support. - Handle
KeyDownon the search box to trigger search on Enter. - Support double-click on the results grid to immediately confirm selection.