diff --git a/core/core.services.yml b/core/core.services.yml
index 2bc5edc..b7f3585 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -828,6 +828,11 @@ services:
     class: Drupal\Core\EventSubscriber\ContentControllerSubscriber
     tags:
       - { name: event_subscriber }
+  route_form_controller_subscriber:
+    class: Drupal\Core\EventSubscriber\FormControllerSubscriber
+    arguments: ['@form_ajax_response_handler', '@controller_resolver']
+    tags:
+      - { name: event_subscriber }
   route_special_attributes_subscriber:
     class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
     tags:
@@ -874,6 +879,9 @@ services:
   controller.entity_form:
     class: Drupal\Core\Entity\HtmlEntityFormController
     arguments: ['@controller_resolver', '@form_builder', '@entity.manager']
+  form_ajax_response_handler:
+    class: Drupal\Core\Form\FormAjaxResponseHandler
+    arguments: ['@main_content_renderer.ajax', '@current_route_match']
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
diff --git a/core/lib/Drupal/Core/Controller/FormController.php b/core/lib/Drupal/Core/Controller/FormController.php
index 6a9b9d5..a73d931 100644
--- a/core/lib/Drupal/Core/Controller/FormController.php
+++ b/core/lib/Drupal/Core/Controller/FormController.php
@@ -8,6 +8,8 @@
 namespace Drupal\Core\Controller;
 
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
+use Drupal\Core\Form\FormAndFormState;
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -56,8 +58,9 @@ public function __construct(ControllerResolverInterface $controller_resolver, Fo
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The route match.
    *
-   * @return array
-   *   The render array that results from invoking the controller.
+   * @return \Drupal\Core\Form\FormAndFormState
+   *   An object containing the render array that results from invoking the
+   *   controller and the current state of the form.
    */
   public function getContentResult(Request $request, RouteMatchInterface $route_match) {
     $form_arg = $this->getFormArgument($route_match);
@@ -76,7 +79,14 @@ public function getContentResult(Request $request, RouteMatchInterface $route_ma
     unset($args[0], $args[1]);
     $form_state->addBuildInfo('args', array_values($args));
 
-    return $this->formBuilder->buildForm($form_object, $form_state);
+    // If this form is being submitted back to its original URL, disable all
+    // redirects.
+    if ($request->query->get(FormControllerSubscriber::POST_TO_ORIGINAL_URL) === TRUE) {
+      $form_state->disableRedirect();
+    }
+
+    $form_array = $this->formBuilder->buildForm($form_object, $form_state);
+    return new FormAndFormState($form_array, $form_state);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
new file mode 100644
index 0000000..d093fe1
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/FormControllerSubscriber.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\FormControllerSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Ajax\UpdateBuildIdCommand;
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
+use Drupal\Core\Form\FormAndFormState;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Wraps controllers that return forms for their response.
+ */
+class FormControllerSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Request key for a form that will post to original URL on AJAX submission.
+   */
+  const POST_TO_ORIGINAL_URL = '_ajax_request';
+
+  /**
+   * The form AJAX handler.
+   *
+   * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface
+   */
+  protected $formAjaxResponseHandler;
+
+  /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * Constructs a new FormControllerSubscriber.
+   *
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX handler.
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   */
+  public function __construct(FormAjaxResponseHandlerInterface $form_ajax_response_handler, ControllerResolverInterface $controller_resolver) {
+    $this->formAjaxResponseHandler = $form_ajax_response_handler;
+    $this->controllerResolver = $controller_resolver;
+  }
+
+  /**
+   * Wraps an AJAX form submission while still running the original controller.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
+   *   The event to process.
+   */
+  public function onControllerWrapAjaxFormSubmission(FilterControllerEvent $event) {
+    $request = $event->getRequest();
+    if ($request->query->get(static::POST_TO_ORIGINAL_URL) === TRUE) {
+      $original_controller = $event->getController();
+
+      // Set a new controller to wrap the original one, applying AJAX updates.
+      $event->setController(function (Request $request) use ($original_controller) {
+        // Call the original controller for the page (usually either
+        // controller.form:getContentResult or
+        // controller.entity_form:getContentResult).
+        $arguments = $this->controllerResolver->getArguments($request, $original_controller);
+        $response = call_user_func_array($original_controller, $arguments);
+        if (!$response instanceof FormAndFormState) {
+          throw new \LogicException('Form controllers must return an instance of \Drupal\Core\Form\FormAndFormState');
+        }
+
+        $form = $response->getForm();
+        $form_state = $response->getFormState();
+        $form_build_id = $request->get('form_build_id');
+
+        $commands = [];
+        if ($form_build_id !== $form['#build_id']) {
+          // If the form build ID has changed, issue an Ajax command to update it.
+          $commands[] = new UpdateBuildIdCommand($form_build_id, $form['#build_id']);
+        }
+        return $this->formAjaxResponseHandler->buildResponse($request, $form, $form_state, $commands);
+      });
+    }
+  }
+
+  /**
+   * Sets a response of a form render array given a FormAndFormState object.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent $event
+   *   The event to process.
+   */
+  public function onViewFormAndFormState(GetResponseForControllerResultEvent $event) {
+    // If the controller returns a FormAndFormState, only return the form.
+    if (($result = $event->getControllerResult()) instanceof FormAndFormState) {
+      $event->setControllerResult($result->getForm());
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::CONTROLLER][] = ['onControllerWrapAjaxFormSubmission'];
+    // Run after main_content_view_subscriber.
+    $events[KernelEvents::VIEW][] = ['onViewFormAndFormState', 10];
+
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php
new file mode 100644
index 0000000..39919a3
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandler.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseHandler.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Render\MainContent\MainContentRendererInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+/**
+ * Handles AJAX form responses.
+ *
+ * Given the current request, a form render array, its form state, and any AJAX
+ * commands to apply to the form, generate a response object.
+ */
+class FormAjaxResponseHandler implements FormAjaxResponseHandlerInterface {
+
+  /**
+   * The main content to AJAX Response renderer.
+   *
+   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
+   */
+  protected $ajaxRenderer;
+
+  /**
+   * The current route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a new FormAjaxResponseHandler.
+   *
+   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
+   *   The ajax renderer.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   */
+  public function __construct(MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
+    $this->ajaxRenderer = $ajax_renderer;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands) {
+    // We need to return the part of the form (or some other content) that needs
+    // to be re-rendered so the browser can update the page with changed
+    // content. Since this is the generic menu callback used by many Ajax
+    // elements, it is up to the #ajax['callback'] function of the element (may
+    // or may not be a button) that triggered the Ajax request to determine what
+    // needs to be rendered.
+    $callback = NULL;
+    if (($triggering_element = $form_state->getTriggeringElement()) && isset($triggering_element['#ajax']['callback'])) {
+      $callback = $triggering_element['#ajax']['callback'];
+    }
+    $callback = $form_state->prepareCallback($callback);
+    if (empty($callback) || !is_callable($callback)) {
+      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
+    }
+    $result = call_user_func_array($callback, [&$form, &$form_state]);
+
+    // If the callback is an #ajax callback, the result is a render array, and
+    // we need to turn it into an AJAX response, so that we can add any commands
+    // we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
+    // submit from a cached page.
+    if ($result instanceof AjaxResponse) {
+      $response = $result;
+    }
+    else {
+      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
+      $response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
+    }
+
+    foreach ($commands as $command) {
+      $response->addCommand($command, TRUE);
+    }
+    return $response;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php
new file mode 100644
index 0000000..7582d8c
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAjaxResponseHandlerInterface.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAjaxResponseHandlerInterface.
+ */
+
+namespace Drupal\Core\Form;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides an interface for handling AJAX form responses.
+ */
+interface FormAjaxResponseHandlerInterface {
+
+  /**
+   * Builds a response for an AJAX form.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $commands
+   *   An array of AJAX commands to apply to the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response representing the form and its AJAX commands.
+   */
+  public function buildResponse(Request $request, array $form, FormStateInterface $form_state, array $commands);
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAndFormState.php b/core/lib/Drupal/Core/Form/FormAndFormState.php
new file mode 100644
index 0000000..e1c4816
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAndFormState.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAndFormState.
+ */
+
+namespace Drupal\Core\Form;
+
+/**
+ * Defines an object that holds a form array and the form state.
+ */
+class FormAndFormState {
+
+  /**
+   * The form to cache.
+   *
+   * @var array
+   */
+  protected $form;
+
+  /**
+   * The form state.
+   *
+   * @var \Drupal\Core\Form\FormStateInterface
+   */
+  protected $formState;
+
+  /**
+   * Constructs a FormAndFormState object.
+   *
+   * @param array $form
+   *   The form definition.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function __construct(array $form, FormStateInterface $form_state) {
+    $this->form = $form;
+    $this->formState = $form_state;
+  }
+
+  /**
+   * Gets the form definition.
+   *
+   * @return array
+   *   The form structure.
+   */
+  public function getForm() {
+    return $this->form;
+  }
+
+  /**
+   * Gets the form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   *   The current state of the form.
+   */
+  public function getFormState() {
+    return $this->formState;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php
index c886fec..4f52ac8 100644
--- a/core/lib/Drupal/Core/Render/Element/RenderElement.php
+++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Render\Element;
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\PluginBase;
 use Drupal\Core\Render\Element;
@@ -128,9 +130,6 @@ public static function preRenderGroup($element) {
    */
   public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
     $element = static::preRenderAjaxForm($element);
-    if (!empty($element['#ajax_processed'])) {
-      $form_state->setCached();
-    }
     return $element;
   }
 
@@ -237,8 +236,12 @@ public static function preRenderAjaxForm($element) {
       // One such substantial difference is form elements that use
       // #ajax['callback'] for determining which part of the form needs
       // re-rendering. For that, we have a special 'system/ajax' route.
+      if (isset($settings['callback']) && !isset($settings['url'])) {
+        $settings['url'] = Url::fromRoute('<current>');
+        $settings['options']['query'][FormControllerSubscriber::POST_TO_ORIGINAL_URL] = TRUE;
+      }
       $settings += array(
-        'url' => isset($settings['callback']) ? Url::fromRoute('system.ajax') : NULL,
+        'url' => NULL,
         'options' => array(),
         'dialogType' => 'ajax',
       );
diff --git a/core/modules/file/src/Controller/FileWidgetAjaxController.php b/core/modules/file/src/Controller/FileWidgetAjaxController.php
index 90176f8..2252033 100644
--- a/core/modules/file/src/Controller/FileWidgetAjaxController.php
+++ b/core/modules/file/src/Controller/FileWidgetAjaxController.php
@@ -10,7 +10,12 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\system\Controller\FormAjaxController;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@@ -21,6 +26,42 @@
 class FileWidgetAjaxController extends FormAjaxController {
 
   /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a FileWidgetAjaxController object.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The form builder.
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX response handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_response_handler, RendererInterface $renderer) {
+    parent::__construct($logger, $form_builder, $form_ajax_response_handler);
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('logger.factory')->get('ajax'),
+      $container->get('form_builder'),
+      $container->get('form_ajax_response_handler'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
    * Processes AJAX file uploads and deletions.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
diff --git a/core/modules/system/src/Controller/FormAjaxController.php b/core/modules/system/src/Controller/FormAjaxController.php
index 0c29322..ce64867 100644
--- a/core/modules/system/src/Controller/FormAjaxController.php
+++ b/core/modules/system/src/Controller/FormAjaxController.php
@@ -7,20 +7,16 @@
 
 namespace Drupal\system\Controller;
 
-use Drupal\Core\Ajax\AjaxResponse;
 use Drupal\Core\Ajax\UpdateBuildIdCommand;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Form\FormAjaxResponseHandlerInterface;
 use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormBuilderInterface;
-use Drupal\Core\Render\MainContent\MainContentRendererInterface;
-use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\system\FileAjaxForm;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\HttpKernel\Exception\HttpException;
 
 /**
  * Defines a controller to respond to form Ajax requests.
@@ -42,25 +38,11 @@ class FormAjaxController implements ContainerInjectionInterface {
   protected $formBuilder;
 
   /**
-   * The renderer.
+   * The form AJAX response handler.
    *
-   * @var \Drupal\Core\Render\RendererInterface
+   * @var \Drupal\Core\Form\FormAjaxResponseHandlerInterface
    */
-  protected $renderer;
-
-  /**
-   * The main content to AJAX Response renderer.
-   *
-   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface
-   */
-  protected $ajaxRenderer;
-
-  /**
-   * The current route match.
-   *
-   * @var \Drupal\Core\Routing\RouteMatchInterface
-   */
-  protected $routeMatch;
+  protected $formAjaxResponseHandler;
 
   /**
    * Constructs a FormAjaxController object.
@@ -69,19 +51,13 @@ class FormAjaxController implements ContainerInjectionInterface {
    *   A logger instance.
    * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
    *   The form builder.
-   * @param \Drupal\Core\Render\RendererInterface $renderer
-   *   The renderer.
-   * @param \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer
-   *   The main content to AJAX Response renderer.
-   * @param \Drupal\Core\Routing\RouteMatchInterface
-   *   The current route match.
+   * @param \Drupal\Core\Form\FormAjaxResponseHandlerInterface $form_ajax_response_handler
+   *   The form AJAX response handler.
    */
-  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer, MainContentRendererInterface $ajax_renderer, RouteMatchInterface $route_match) {
+  public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, FormAjaxResponseHandlerInterface $form_ajax_response_handler) {
     $this->logger = $logger;
     $this->formBuilder = $form_builder;
-    $this->renderer = $renderer;
-    $this->ajaxRenderer = $ajax_renderer;
-    $this->routeMatch = $route_match;
+    $this->formAjaxResponseHandler = $form_ajax_response_handler;
   }
 
   /**
@@ -91,9 +67,7 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('logger.factory')->get('ajax'),
       $container->get('form_builder'),
-      $container->get('renderer'),
-      $container->get('main_content_renderer.ajax'),
-      $container->get('current_route_match')
+      $container->get('form_ajax_response_handler')
     );
   }
 
@@ -121,38 +95,7 @@ public function content(Request $request) {
 
     $this->formBuilder->processForm($form['#form_id'], $form, $form_state);
 
-    // We need to return the part of the form (or some other content) that needs
-    // to be re-rendered so the browser can update the page with changed content.
-    // Since this is the generic menu callback used by many Ajax elements, it is
-    // up to the #ajax['callback'] function of the element (may or may not be a
-    // button) that triggered the Ajax request to determine what needs to be
-    // rendered.
-    $callback = NULL;
-    if ($triggering_element = $form_state->getTriggeringElement()) {
-      $callback = $triggering_element['#ajax']['callback'];
-    }
-    $callback = $form_state->prepareCallback($callback);
-    if (empty($callback) || !is_callable($callback)) {
-      throw new HttpException(500, 'The specified #ajax callback is empty or not callable.');
-    }
-    $result = call_user_func_array($callback, [&$form, &$form_state]);
-
-    // If the callback is an #ajax callback, the result is a render array, and
-    // we need to turn it into an AJAX response, so that we can add any commands
-    // we got earlier; typically the UpdateBuildIdCommand when handling an AJAX
-    // submit from a cached page.
-    if ($result instanceof AjaxResponse) {
-      $response = $result;
-    }
-    else {
-      /** @var \Drupal\Core\Ajax\AjaxResponse $response */
-      $response = $this->ajaxRenderer->renderResponse($result, $request, $this->routeMatch);
-    }
-
-    foreach ($commands as $command) {
-      $response->addCommand($command, TRUE);
-    }
-    return $response;
+    return $this->formAjaxResponseHandler->buildResponse($request, $form, $form_state, $commands);
   }
 
   /**
diff --git a/core/modules/system/src/FileAjaxForm.php b/core/modules/system/src/FileAjaxForm.php
index 1be14b4..749369c 100644
--- a/core/modules/system/src/FileAjaxForm.php
+++ b/core/modules/system/src/FileAjaxForm.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system;
 
+use Drupal\Core\Form\FormAndFormState;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
@@ -14,21 +15,7 @@
  *
  * @ingroup ajax
  */
-class FileAjaxForm {
-
-  /**
-   * The form to cache.
-   *
-   * @var array
-   */
-  protected $form;
-
-  /**
-   * The form state.
-   *
-   * @var \Drupal\Core\Form\FormStateInterface
-   */
-  protected $formState;
+class FileAjaxForm extends FormAndFormState {
 
   /**
    * The unique form ID.
@@ -66,8 +53,7 @@ class FileAjaxForm {
    *   The ajax commands.
    */
   public function __construct(array $form, FormStateInterface $form_state, $form_id, $form_build_id, array $commands) {
-    $this->form = $form;
-    $this->formState = $form_state;
+    parent::__construct($form, $form_state);
     $this->formId = $form_id;
     $this->formBuildId = $form_build_id;
     $this->commands = $commands;
@@ -84,15 +70,6 @@ public function getCommands() {
   }
 
   /**
-   * Gets the form definition.
-   *
-   * @return array
-   */
-  public function getForm() {
-    return $this->form;
-  }
-
-  /**
    * Gets the unique form build ID.
    *
    * @return string
@@ -110,13 +87,4 @@ public function getFormId() {
     return $this->formId;
   }
 
-  /**
-   * Gets the form state.
-   *
-   * @return \Drupal\Core\Form\FormStateInterface
-   */
-  public function getFormState() {
-    return $this->formState;
-  }
-
 }
diff --git a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
index c5dafbe..03727b6 100644
--- a/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
+++ b/core/modules/system/src/Tests/Ajax/AjaxFormPageCacheTest.php
@@ -54,9 +54,15 @@ public function testSimpleAJAXFormValue() {
    $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
 
    $edit = ['select' => 'red'];
-   $this->drupalPostAjaxForm(NULL, $edit, 'select');
+   $commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
    $build_id_second_ajax = $this->getFormBuildId();
-   $this->assertEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id remains the same on subsequent AJAX submissions');
+   $this->assertNotEqual($build_id_first_ajax, $build_id_second_ajax, 'Build id changes on subsequent AJAX submissions');
+   $expected = [
+     'command' => 'update_build_id',
+     'old' => $build_id_first_ajax,
+     'new' => $build_id_second_ajax,
+   ];
+   $this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
 
    // Repeat the test sequence but this time with a page loaded from the cache.
    $this->drupalGet('ajax_forms_test_get_form');
@@ -77,9 +83,15 @@ public function testSimpleAJAXFormValue() {
    $this->assertCommand($commands, $expected, 'Build id change command issued on first AJAX submission');
 
    $edit = ['select' => 'red'];
-   $this->drupalPostAjaxForm(NULL, $edit, 'select');
+   $commands = $this->drupalPostAjaxForm(NULL, $edit, 'select');
    $build_id_from_cache_second_ajax = $this->getFormBuildId();
-   $this->assertEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id remains the same on subsequent AJAX submissions');
+   $this->assertNotEqual($build_id_from_cache_first_ajax, $build_id_from_cache_second_ajax, 'Build id changes on subsequent AJAX submissions');
+   $expected = [
+     'command' => 'update_build_id',
+     'old' => $build_id_from_cache_first_ajax,
+     'new' => $build_id_from_cache_second_ajax,
+   ];
+   $this->assertCommand($commands, $expected, 'Build id change command issued on subsequent AJAX submissions');
  }
 
   /**
diff --git a/core/modules/system/src/Tests/Ajax/DialogTest.php b/core/modules/system/src/Tests/Ajax/DialogTest.php
index e290977..ed74d0b 100644
--- a/core/modules/system/src/Tests/Ajax/DialogTest.php
+++ b/core/modules/system/src/Tests/Ajax/DialogTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Ajax;
 
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Url;
 
@@ -165,7 +166,7 @@ public function testDialog() {
       'edit-preview' => [
         'callback' => '::preview',
         'event' => 'click',
-        'url' => Url::fromRoute('system.ajax')->toString(),
+        'url' => Url::fromRoute('ajax_test.dialog_form')->setOption('query', [FormControllerSubscriber::POST_TO_ORIGINAL_URL => TRUE])->toString(),
         'dialogType' => 'ajax',
         'submit' => [
           '_triggering_element_name' => 'op',
diff --git a/core/modules/views_ui/admin.inc b/core/modules/views_ui/admin.inc
index 64b9077..6c72b03 100644
--- a/core/modules/views_ui/admin.inc
+++ b/core/modules/views_ui/admin.inc
@@ -51,6 +51,10 @@ function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_pa
   // Add the AJAX behavior to the triggering element.
   $triggering_element = &$wrapping_element[$trigger_key];
   $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form';
+
+  // Specify the #ajax URL in order to retain form caching.
+  $triggering_element['#ajax']['url'] = Url::fromRoute('system.ajax');
+
   // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID
   // for the AJAX wrapper, because it remembers IDs across AJAX requests (and
   // won't reuse them), but in our case we need to use the same ID from request
diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php
new file mode 100644
index 0000000..1ecd724
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/EventSubscriber/FormControllerSubscriberTest.php
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\EventSubscriber\FormControllerSubscriberTest.
+ */
+
+namespace Drupal\Tests\Core\EventSubscriber;
+
+use Drupal\Core\Ajax\UpdateBuildIdCommand;
+use Drupal\Core\EventSubscriber\FormControllerSubscriber;
+use Drupal\Core\Form\FormAndFormState;
+use Drupal\Core\Form\FormState;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
+use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * @coversDefaultClass \Drupal\Core\EventSubscriber\FormControllerSubscriber
+ * @group EventSubscriber
+ */
+class FormControllerSubscriberTest extends UnitTestCase {
+
+  /**
+   * @covers ::onViewFormAndFormState
+   */
+  public function testOnViewFormAndFormState() {
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler, $controller_resolver);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+    $request = new Request();
+    $form = ['#type' => 'form'];
+    $form_state = new FormState();
+
+    // Test with a FormAndFormState.
+    $controller = new FormAndFormState($form, $form_state);
+    $expected = $form;
+    $event = new GetResponseForControllerResultEvent($http_kernel, $request, HttpKernelInterface::MASTER_REQUEST, $controller);
+    $subscriber->onViewFormAndFormState($event);
+    $this->assertSame($expected, $event->getControllerResult());
+
+    // Test with the form.
+    $controller = $form;
+    $expected = $form;
+    $event = new GetResponseForControllerResultEvent($http_kernel, $request, HttpKernelInterface::MASTER_REQUEST, $controller);
+    $subscriber->onViewFormAndFormState($event);
+    $this->assertSame($expected, $event->getControllerResult());
+
+    // Test with NULL.
+    $controller = NULL;
+    $expected = NULL;
+    $event = new GetResponseForControllerResultEvent($http_kernel, $request, HttpKernelInterface::MASTER_REQUEST, $controller);
+    $subscriber->onViewFormAndFormState($event);
+    $this->assertSame($expected, $event->getControllerResult());
+  }
+
+  /**
+   * @covers ::onControllerWrapAjaxFormSubmission
+   */
+  public function testOnControllerWrapAjaxFormSubmission() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $form_state = new FormState();
+    $request = new Request([FormControllerSubscriber::POST_TO_ORIGINAL_URL => TRUE], ['form_build_id' => 'the_build_id']);
+    $commands = [];
+
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $form_ajax_response_handler->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $form, $form_state, $commands)
+      ->willReturn('the ajax result');
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $controller_resolver->expects($this->once())
+      ->method('getArguments')
+      ->willReturn([$form, $form_state]);
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler, $controller_resolver);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+
+    $controller = function ($form, $form_state) {
+      return new FormAndFormState($form, $form_state);
+    };
+
+    $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST);
+
+    $subscriber->onControllerWrapAjaxFormSubmission($event);
+    $expected = $controller;
+    $resulting_controller = $event->getController();
+    $this->assertNotSame($expected, $resulting_controller);
+
+    $result = $resulting_controller($request);
+    $this->assertSame('the ajax result', $result);
+  }
+
+  /**
+   * @covers ::onControllerWrapAjaxFormSubmission
+   */
+  public function testOnControllerWrapAjaxFormSubmissionUsingGet() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $form_state = new FormState();
+    $request = new Request([FormControllerSubscriber::POST_TO_ORIGINAL_URL => TRUE, 'form_build_id' => 'the_build_id']);
+    $commands = [];
+
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $form_ajax_response_handler->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $form, $form_state, $commands)
+      ->willReturn('the ajax result');
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $controller_resolver->expects($this->once())
+      ->method('getArguments')
+      ->willReturn([$form, $form_state]);
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler, $controller_resolver);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+
+    $controller = function ($form, $form_state) {
+      return new FormAndFormState($form, $form_state);
+    };
+
+    $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST);
+
+    $subscriber->onControllerWrapAjaxFormSubmission($event);
+    $expected = $controller;
+    $resulting_controller = $event->getController();
+    $this->assertNotSame($expected, $resulting_controller);
+
+    $result = $resulting_controller($request);
+    $this->assertSame('the ajax result', $result);
+  }
+
+  /**
+   * @covers ::onControllerWrapAjaxFormSubmission
+   */
+  public function testOnControllerWrapAjaxFormSubmissionMismatchedBuildId() {
+    $form = ['#type' => 'form', '#build_id' => 'the_build_id'];
+    $form_state = new FormState();
+    $request = new Request([FormControllerSubscriber::POST_TO_ORIGINAL_URL => TRUE, 'form_build_id' => 'a_new_build_id']);
+    $commands = [];
+    $commands[] = new UpdateBuildIdCommand('a_new_build_id', 'the_build_id');
+
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $form_ajax_response_handler->expects($this->once())
+      ->method('buildResponse')
+      ->with($request, $form, $form_state, $commands)
+      ->willReturn('the ajax result');
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $controller_resolver->expects($this->once())
+      ->method('getArguments')
+      ->willReturn([$form, $form_state]);
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler, $controller_resolver);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+
+    $controller = function ($form, $form_state) {
+      return new FormAndFormState($form, $form_state);
+    };
+
+    $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST);
+
+    $subscriber->onControllerWrapAjaxFormSubmission($event);
+    $expected = $controller;
+    $resulting_controller = $event->getController();
+    $this->assertNotSame($expected, $resulting_controller);
+
+    $result = $resulting_controller($request);
+    $this->assertSame('the ajax result', $result);
+  }
+
+  /**
+   * @covers ::onControllerWrapAjaxFormSubmission
+   *
+   * @expectedException \LogicException
+   * @expectedExceptionMessage Form controllers must return an instance of \Drupal\Core\Form\FormAndFormState
+   */
+  public function testOnControllerWrapAjaxFormSubmissionException() {
+    $request = new Request([FormControllerSubscriber::POST_TO_ORIGINAL_URL => TRUE]);
+
+    $form_ajax_response_handler = $this->getMock('Drupal\Core\Form\FormAjaxResponseHandlerInterface');
+    $form_ajax_response_handler->expects($this->never())
+      ->method('buildResponse');
+    $controller_resolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
+    $controller_resolver->expects($this->once())
+      ->method('getArguments')
+      ->willReturn([]);
+    $subscriber = new FormControllerSubscriber($form_ajax_response_handler, $controller_resolver);
+
+    $http_kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
+
+    $controller = function () {
+      return [];
+    };
+
+    $event = new FilterControllerEvent($http_kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST);
+
+    $subscriber->onControllerWrapAjaxFormSubmission($event);
+    $resulting_controller = $event->getController();
+    $resulting_controller($request);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php
new file mode 100644
index 0000000..3568dfe
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormAjaxResponseHandlerTest.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormAjaxResponseHandlerTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AlertCommand;
+use Drupal\Core\Form\FormAjaxResponseHandler;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormAjaxResponseHandler
+ * @group Form
+ */
+class FormAjaxResponseHandlerTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\Render\MainContent\MainContentRendererInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $renderer;
+
+  /**
+   * @var \Drupal\Core\Routing\RouteMatchInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $routeMatch;
+
+  /**
+   * @var \Drupal\Core\Form\FormAjaxResponseHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->renderer = $this->getMock('Drupal\Core\Render\MainContent\MainContentRendererInterface');
+    $this->routeMatch = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $this->handler = new FormAjaxResponseHandler($this->renderer, $this->routeMatch);
+  }
+
+  /**
+   * @covers ::buildResponse
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function testBuildResponseNoTriggeringElement() {
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $commands = [];
+
+    $expected = [];
+    $this->assertSame($expected, $this->handler->buildResponse($request, $form, $form_state, $commands));
+  }
+
+  /**
+   * @covers ::buildResponse
+   *
+   * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function testBuildResponseNoCallable() {
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $triggering_element = [];
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $expected = [];
+    $this->assertSame($expected, $this->handler->buildResponse($request, $form, $form_state, $commands));
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseRenderArray() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return $form['test'];
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [
+      'test' => [
+        '#type' => 'textfield',
+      ],
+    ];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $this->renderer->expects($this->once())
+      ->method('renderResponse')
+      ->with($form['test'], $request, $this->routeMatch)
+      ->willReturn(new AjaxResponse([]));
+
+    $result = $this->handler->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands, $result->getCommands());
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseResponse() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return new AjaxResponse([]);
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [];
+
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $result = $this->handler->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands, $result->getCommands());
+  }
+
+  /**
+   * @covers ::buildResponse
+   */
+  public function testBuildResponseWithCommands() {
+    $triggering_element = [
+      '#ajax' => [
+        'callback' => function (array $form, FormStateInterface $form_state) {
+          return new AjaxResponse([]);
+        }
+      ],
+    ];
+    $request = new Request();
+    $form = [
+      'test' => [
+        '#type' => 'textfield',
+      ],
+    ];
+    $form_state = new FormState();
+    $form_state->setTriggeringElement($triggering_element);
+    $commands = [
+      new AlertCommand('alert!'),
+    ];
+    $commands_expected = [];
+    $commands_expected[] = ['command' => 'alert', 'text' => 'alert!'];
+
+    $this->renderer->expects($this->never())
+      ->method('renderResponse');
+
+    $result = $this->handler->buildResponse($request, $form, $form_state, $commands);
+    $this->assertInstanceOf('\Drupal\Core\Ajax\AjaxResponse', $result);
+    $this->assertSame($commands_expected, $result->getCommands());
+  }
+
+}
