HandleAction
HandleAction is a lifecycle hook for returning non-HTML responses from a page. It runs after Authorize but before the DOM lifecycle (ConfigureStyles, CreateDocument, Render), giving you an early exit point to return file downloads, JSON, or any other response type.
Where It Fits
The full BasePage lifecycle:
- Authorize() - Authentication check
- HandleAction() - Non-HTML response hook (file downloads, etc.)
- ConfigureStyles() - Build style library
- CreateDocument() - Build DOM structure
- Render output - Combine Document + Styles into HTML
HandleAction returns null to continue with normal page rendering, or returns an HttpResponse to short-circuit the lifecycle and skip the DOM entirely.
How It Works
When HandleAction returns an HttpResponse, the server sends that response directly to the browser. If the response includes a Content-Disposition: attachment header, the browser saves it as a file without navigating away from the current page. The user sees their page stay put and gets a file download.
If the operation fails, call ShowAlert with an error message and return null. The page will render normally and display the error to the user.
Example: CSV Download
This example adds a Download CSV button to a page that exports the current data as a file.
Step 1: Override HandleAction
In your page class, override HandleAction to intercept the download postback. This runs before BuildContent, so no DOM is created if we return a file.
// Const identifiers — matched in two places (check + control),
// so a typo here causes silent failure with no compiler error.
private const string ActionField = "action";
private const string ActionDownload = "download";
// Override HandleAction to intercept download requests before the DOM lifecycle
protected override HttpResponse HandleAction()
{
// Check if this is a POST with our download action
if (IsPostBack() && GetFormValue(ActionField) == ActionDownload)
{
// Get the current filter so we export what the user is looking at
string app = GetQueryValue("app");
if (string.IsNullOrEmpty(app)) app = "All";
// Fetch data using the same adapter the page uses
var results = MyAdapter.GetData(app);
if (!results.Success)
{
// On failure, show error and fall through to normal page render
ShowAlert($"Error exporting data: {results.Message}", AlertType.Error);
return null;
}
// CsvDownload converts the DataTable to CSV bytes and returns
// an HttpResponse with Content-Disposition: attachment
return CsvDownload(results.DataTable, "Export.csv");
}
// Not a download request — continue with normal page rendering
return base.HandleAction();
}
Step 2: Add the Download Button
Add a separate form with a hidden action field. The button submits this form, which POSTs to the same URL. HandleAction intercepts it before BuildContent ever runs.
// Create a dedicated form for the download button.
// This keeps it separate from other forms on the page
// (each Form renders its own hidden __EVENTTARGET field,
// so using a hidden 'action' field avoids duplicate ID issues).
var downloadForm = content.Add(new Form("", "POST"));
// Hidden field identifies this postback as a download request
downloadForm.Add(new Input(InputType.Hidden, ActionField, ActionDownload));
// Submit button — no WithName needed since we use the hidden field
downloadForm.Add(new Button(ButtonType.Submit, "Download CSV")
.WithClass(OpsOCss.Button));
Why a Separate Form?
Each Form element renders its own hidden __EVENTTARGET input. If your page already has a form (for editing, filtering, etc.), adding a download button inside that form would conflict. A separate form with a hidden action field keeps things clean and avoids duplicate ID issues.
Built-In Helpers
BasePage provides two helper methods for building download responses:
| Method | Purpose |
|---|---|
| CsvDownload(DataTable dt, string filename) | Converts a DataTable to CSV and returns a download response. Uses CsvExporter internally. |
| FileDownload(byte[] content, string contentType, string filename) | General-purpose download for any content type (Excel, PDF, etc.). |
CsvExporter
CsvExporter is a static utility in FlightPlan.Core that converts any DataTable to CSV format. It handles column headers, proper quoting of values containing commas or quotes, and iterates the DefaultView so any active sort or filter on the DataTable is respected.
// CsvExporter can be used independently of the download helpers string csv = CsvExporter.ToCsv(dt); // Returns CSV as a string byte[] bytes = CsvExporter.ToCsvBytes(dt); // Returns CSV as UTF-8 bytes
Other Use Cases
HandleAction can return any HttpResponse, not just CSV downloads. Use FileDownload with different content types for other formats:
// Example: return a JSON response
var response = new HttpResponse
{
StatusCode = 200,
Body = JsonSerializer.Serialize(data)
};
response.Headers["Content-Type"] = "application/json";
return response;
// Example: return an arbitrary file download
byte[] excelBytes = GenerateExcel(dt);
return FileDownload(excelBytes,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Report.xlsx");