How do I download a dynamically-created file in a form submission? I've got a form that builds an array of data. On submission, it updates a table displayed in the browser, using AJAX; that works fine.

I want a second submit button that downloads the same data as a CSV file. I've tried a number of variations, but I can't seem to both send the file response and satisfy the AJAX requirement for a returned array.

The button:

$form['download'] = [
  '#type' =&gt; 'submit',
  '#value' =&gt; $this-&gt;t('Download data'),
  '#weight' =&gt; '40',
  '#ajax' =&gt; [
    'callback' =&gt; '::downloadCallback',
    'event' =&gt; 'click',
  ],
];


...and the response section of the callback:

$file_content = $serializer-&gt;serialize($data, 'csv');
$response = new Response($file_content);
$disposition = $response-&gt;headers-&gt;makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'data.csv');
$response-&gt;headers-&gt;set('Content-Disposition', $disposition);
$form_state-&gt;setResponse($response);


I've also tried using this instead of setResponse:

$response-&gt;send();


These all result in an error:


  TypeError: Argument 1 passed to Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse() must be of the type array, null given, called in /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php on line 89 in /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php on line 45 #0 /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php(89): Drupal\Core\Render\MainContent\AjaxRenderer->renderResponse(NULL, Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\CurrentRouteMatch))


I've tried also returning a simple render array with some dummy markup, and that prevents the error, but still doesn't result in a file download. Is there a way to accomplish this? Or should I just not be using AJAX for this purpose?
I was off-track in even trying to do this with AJAX, as Clive mentioned in his comment. Instead, I just made it a normal submit button, and then overrode the form action. The result is that the data is downloaded as a file, the form doesn't reload, and the data output section doesn't update, which is the desired behavior in this case.
The button, along with a new format element:
$form['download']['format'] = [
  '#type' =&gt; 'select',
  '#options' =&gt; $this-&gt;getValidFormats(),
  '#title' =&gt; $this-&gt;t('Select format'),
  '#weight' =&gt; '0',
];
$form['download']['download'] = [
  '#type' =&gt; 'submit',
  '#value' =&gt; $this-&gt;t('Download'),
  '#weight' =&gt; '10',
];

The submit function:
public function submitForm(array &amp;$form, FormStateInterface $form_state) {
  switch ($form_state-&gt;getValue('op')-&gt;__toString()) {
    case 'Download':
      $this-&gt;download($form, $form_state);
      break;
  }
}

And the relevant part of the download function:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

$response = new Response($file_content);
$disposition = $response-&gt;headers-&gt;makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'data.' . $format);
$response-&gt;headers-&gt;set('Content-type', $content_types[$format]);
$response-&gt;headers-&gt;set('Content-Disposition', $disposition);
$response-&gt;headers-&gt;set('Content-Transfer-Encoding', 'binary');
$response-&gt;headers-&gt;set('Content-length', strlen($file_content));
$form_state-&gt;setResponse($response);