diff --git a/core/core.services.yml b/core/core.services.yml
index 69e3ef2..415cb3b 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -405,13 +405,15 @@ services:
     arguments: ['@controller_resolver', '@string_translation', '@title_resolver']
   controller.ajax:
     class: Drupal\Core\Controller\AjaxController
-    arguments: ['@controller_resolver']
+    arguments: ['@controller_resolver', '@ajax_response_renderer']
   controller.entityform:
     class: Drupal\Core\Entity\HtmlEntityFormController
     arguments: ['@controller_resolver', '@service_container', '@entity.manager']
   controller.dialog:
     class: Drupal\Core\Controller\DialogController
     arguments: ['@controller_resolver', '@title_resolver']
+  ajax_response_renderer:
+    class: Drupal\Core\Ajax\AjaxResponseRenderer
   router_listener:
     class: Symfony\Component\HttpKernel\EventListener\RouterListener
     tags:
@@ -423,7 +425,7 @@ services:
     class: Drupal\Core\EventSubscriber\ViewSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@content_negotiation', '@title_resolver']
+    arguments: ['@content_negotiation', '@title_resolver', '@ajax_response_renderer']
   html_view_subscriber:
     class: Drupal\Core\EventSubscriber\HtmlViewSubscriber
     tags:
diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 5c4bb99..1f51732 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -5,8 +5,6 @@
  * Functions for use with Drupal's Ajax framework.
  */
 
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-
 /**
  * @defgroup ajax Ajax framework
  * @{
@@ -50,7 +48,8 @@
  *     #ajax['callback'], which returns the form element that has been updated
  *     and needs to be returned to the browser, or alternatively, an array of
  *     custom Ajax commands.
- *   - The array is serialized using ajax_render() and sent to the browser.
+ *   - The array is serialized using
+ *     \Drupal\Core\Ajax\AjaxResponse::ajaxRender() and sent to the browser.
  *   - The browser unserializes the returned JSON string into an array of
  *     command objects and executes each command, resulting in the old page
  *     content within and including the HTML element specified by
@@ -125,11 +124,12 @@
  *   executed by the calling code.
  * - #ajax['path']: The menu path to use for the request. This is often omitted
  *   and the default is used. This path should map
- *   to a menu page callback that returns data using ajax_render(). Defaults to
- *   'system/ajax', which invokes \Drupal\system\FormAjaxController::content(),
- *   eventually calling the function named in #ajax['callback']. If you use a
- *   custom path, you must set up the menu entry and handle the entire callback
- *   in your own code.
+ *   to a controller that returns data using
+ *   \Drupal\Core\Ajax\AjaxResponse::ajaxRender(). Defaults to 'system/ajax',
+ *   which invokes \Drupal\system\FormAjaxController::content(), eventually
+ *   calling the function named in #ajax['callback']. If you use a custom path,
+ *   you must set up the menu entry and handle the entire callback in your own
+ *   code.
  * - #ajax['wrapper']: The CSS ID of the area to be replaced by the content
  *   returned by the #ajax['callback'] function. The content returned from
  *   the callback will replace the entire element named by #ajax['wrapper'].
@@ -224,151 +224,6 @@
  */
 
 /**
- * Renders a commands array into JSON.
- *
- * @param $commands
- *   A list of macro commands generated by the use of ajax_command_*()
- *   functions.
- */
-function ajax_render($commands = array()) {
-  // Ajax responses aren't rendered with html.html.twig, so we have to call
-  // drupal_get_css() and drupal_get_js() here, in order to have new files added
-  // during this request to be loaded by the page. We only want to send back
-  // files that the page hasn't already loaded, so we implement simple diffing
-  // logic using array_diff_key().
-  foreach (array('css', 'js') as $type) {
-    // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty,
-    // since the base page ought to have at least one JS file and one CSS file
-    // loaded. It probably indicates an error, and rather than making the page
-    // reload all of the files, instead we return no new files.
-    if (!\Drupal::request()->request->get("ajax_page_state[$type]", NULL, TRUE)) {
-      $items[$type] = array();
-    }
-    else {
-      $function = 'drupal_add_' . $type;
-      $items[$type] = $function();
-      drupal_alter($type, $items[$type]);
-      // @todo Inline CSS and JS items are indexed numerically. These can't be
-      //   reliably diffed with array_diff_key(), since the number can change
-      //   due to factors unrelated to the inline content, so for now, we strip
-      //   the inline items from Ajax responses, and can add support for them
-      //   when drupal_add_css() and drupal_add_js() are changed to use a hash
-      //   of the inline content as the array key.
-      foreach ($items[$type] as $key => $item) {
-        if (is_numeric($key)) {
-          unset($items[$type][$key]);
-        }
-      }
-      // Ensure that the page doesn't reload what it already has.
-      $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]);
-    }
-  }
-
-  // Render the HTML to load these files, and add AJAX commands to insert this
-  // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the
-  // data from being altered again, as we already altered it above. Settings are
-  // handled separately, afterwards.
-  if (isset($items['js']['settings'])) {
-    unset($items['js']['settings']);
-  }
-  $styles = drupal_get_css($items['css'], TRUE);
-  $scripts_footer = drupal_get_js('footer', $items['js'], TRUE);
-  $scripts_header = drupal_get_js('header', $items['js'], TRUE);
-
-  $extra_commands = array();
-  if (!empty($styles)) {
-    $extra_commands[] = ajax_command_add_css($styles);
-  }
-  if (!empty($scripts_header)) {
-    $extra_commands[] = ajax_command_prepend('head', $scripts_header);
-  }
-  if (!empty($scripts_footer)) {
-    $extra_commands[] = ajax_command_append('body', $scripts_footer);
-  }
-  if (!empty($extra_commands)) {
-    $commands = array_merge($extra_commands, $commands);
-  }
-
-  // Now add a command to merge changes and additions to drupalSettings.
-  $scripts = drupal_add_js();
-  if (!empty($scripts['settings'])) {
-    $settings = drupal_merge_js_settings($scripts['settings']['data']);
-    array_unshift($commands, ajax_command_settings($settings, TRUE));
-  }
-
-  // Allow modules to alter any Ajax response.
-  drupal_alter('ajax_render', $commands);
-
-  return drupal_json_encode($commands);
-}
-
-/**
- * Converts the return value of a page callback into an Ajax commands array.
- *
- * @param $page_callback_result
- *   The result of a page callback. Can be one of:
- *   - NULL: to indicate no content.
- *   - An integer menu status constant: to indicate an error condition.
- *   - A string of HTML content.
- *   - A renderable array of content.
- *
- * @return
- *   An Ajax commands array that can be passed to ajax_render().
- */
-function ajax_prepare_response($page_callback_result) {
-  $commands = array();
-  if (!isset($page_callback_result)) {
-    // Simply delivering an empty commands array is sufficient. This results
-    // in the Ajax request being completed, but nothing being done to the page.
-  }
-  elseif (is_int($page_callback_result)) {
-    switch ($page_callback_result) {
-      case MENU_NOT_FOUND:
-        $commands[] = ajax_command_alert(t('The requested page could not be found.'));
-        break;
-
-      case MENU_ACCESS_DENIED:
-        $commands[] = ajax_command_alert(t('You are not authorized to access this page.'));
-        break;
-
-      case MENU_SITE_OFFLINE:
-        $commands[] = ajax_command_alert(filter_xss_admin(t(\Drupal::config('system.maintenance')->get('message'), array('@site' => \Drupal::config('system.site')->get('name')))));
-        break;
-    }
-  }
-  elseif (is_array($page_callback_result) && isset($page_callback_result['#type']) && ($page_callback_result['#type'] == 'ajax')) {
-    // Complex Ajax callbacks can return a result that contains an error message
-    // or a specific set of commands to send to the browser.
-    $page_callback_result += element_info('ajax');
-    $error = $page_callback_result['#error'];
-    if (isset($error) && $error !== FALSE) {
-      if ((empty($error) || $error === TRUE)) {
-        $error = t('An error occurred while handling the request: The server received invalid input.');
-      }
-      $commands[] = ajax_command_alert($error);
-    }
-    else {
-      $commands = $page_callback_result['#commands'];
-    }
-  }
-  else {
-    // Like normal page callbacks, simple Ajax callbacks can return HTML
-    // content, as a string or render array. This HTML is inserted in some
-    // relationship to #ajax['wrapper'], as determined by which jQuery DOM
-    // manipulation method is used. The method used is specified by
-    // #ajax['method']. The default method is 'replaceWith', which completely
-    // replaces the old wrapper element and its content with the new HTML.
-    $html = is_string($page_callback_result) ? $page_callback_result : drupal_render($page_callback_result);
-    $commands[] = ajax_command_insert(NULL, $html);
-    // Add the status messages inside the new content's wrapper element, so that
-    // on subsequent Ajax requests, it is treated as old content.
-    $commands[] = ajax_command_prepend(NULL, theme('status_messages'));
-  }
-
-  return $commands;
-}
-
-/**
  * Form element processing handler for the #ajax form property.
  *
  * @param $element
@@ -558,266 +413,3 @@ function ajax_pre_render_element($element) {
 /**
  * @} End of "defgroup ajax".
  */
-
-/**
- * @defgroup ajax_commands Ajax framework commands
- * @{
- * Functions to create various Ajax commands.
- *
- * These functions can be used to create arrays for use with the
- * ajax_render() function.
- */
-
-/**
- * Creates a Drupal Ajax 'alert' command.
- *
- * The 'alert' command instructs the client to display a JavaScript alert
- * dialog box.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.alert()
- * defined in misc/ajax.js.
- *
- * @param $text
- *   The message string to display to the user.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- */
-function ajax_command_alert($text) {
-  return array(
-    'command' => 'alert',
-    'text' => $text,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'insert' command using the method in #ajax['method'].
- *
- * This command instructs the client to insert the given HTML using whichever
- * jQuery DOM manipulation method has been specified in the #ajax['method']
- * variable of the element that triggered the request.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.insert()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- * @param $html
- *   The data to use with the jQuery method.
- * @param $settings
- *   An optional array of settings that will be used for this command only.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- */
-function ajax_command_insert($selector, $html, $settings = NULL) {
-  return array(
-    'command' => 'insert',
-    'method' => NULL,
-    'selector' => $selector,
-    'data' => $html,
-    'settings' => $settings,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'insert/prepend' command.
- *
- * The 'insert/prepend' command instructs the client to use jQuery's prepend()
- * method to prepend the given HTML content to the inside each element matched
- * by the given selector.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.insert()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- * @param $html
- *   The data to use with the jQuery prepend() method.
- * @param $settings
- *   An optional array of settings that will be used for this command only.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- *
- * @see http://docs.jquery.com/Manipulation/prepend#content
- */
-function ajax_command_prepend($selector, $html, $settings = NULL) {
-  return array(
-    'command' => 'insert',
-    'method' => 'prepend',
-    'selector' => $selector,
-    'data' => $html,
-    'settings' => $settings,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'insert/append' command.
- *
- * The 'insert/append' command instructs the client to use jQuery's append()
- * method to append the given HTML content to the inside of each element matched
- * by the given selector.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.insert()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- * @param $html
- *   The data to use with the jQuery append() method.
- * @param $settings
- *   An optional array of settings that will be used for this command only.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- *
- * @see http://docs.jquery.com/Manipulation/append#content
- */
-function ajax_command_append($selector, $html, $settings = NULL) {
-  return array(
-    'command' => 'insert',
-    'method' => 'append',
-    'selector' => $selector,
-    'data' => $html,
-    'settings' => $settings,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'remove' command.
- *
- * The 'remove' command instructs the client to use jQuery's remove() method
- * to remove each of elements matched by the given selector, and everything
- * within them.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.remove()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- *
- * @see http://docs.jquery.com/Manipulation/remove#expr
- */
-function ajax_command_remove($selector) {
-  return array(
-    'command' => 'remove',
-    'selector' => $selector,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'changed' command.
- *
- * This command instructs the client to mark each of the elements matched by the
- * given selector as 'ajax-changed'.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.changed()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- * @param $asterisk
- *   An optional CSS selector which must be inside $selector. If specified,
- *   an asterisk will be appended to the HTML inside the $asterisk selector.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- */
-function ajax_command_changed($selector, $asterisk = '') {
-  return array(
-    'command' => 'changed',
-    'selector' => $selector,
-    'asterisk' => $asterisk,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'css' command.
- *
- * The 'css' command will instruct the client to use the jQuery css() method
- * to apply the CSS arguments to elements matched by the given selector.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.css()
- * defined in misc/ajax.js.
- *
- * @param $selector
- *   A jQuery selector string. If the command is a response to a request from
- *   an #ajax form element then this value can be NULL.
- * @param $argument
- *   An array of key/value pairs to set in the CSS for the selector.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- *
- * @see http://docs.jquery.com/CSS/css#properties
- */
-function ajax_command_css($selector, $argument) {
-  return array(
-    'command' => 'css',
-    'selector' => $selector,
-    'argument' => $argument,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'settings' command.
- *
- * The 'settings' command instructs the client either to use the given array as
- * the settings for ajax-loaded content or to extend drupalSettings with the
- * given array, depending on the value of the $merge parameter.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.settings()
- * defined in misc/ajax.js.
- *
- * @param $argument
- *   An array of key/value pairs to add to the settings. This will be utilized
- *   for all commands after this if they do not include their own settings
- *   array.
- * @param $merge
- *   Whether or not the passed settings in $argument should be merged into the
- *   global drupalSettings on the page. By default (FALSE), the settings that
- *   are passed to Drupal.attachBehaviors will not include the global
- *   drupalSettings.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- */
-function ajax_command_settings($argument, $merge = FALSE) {
-  return array(
-    'command' => 'settings',
-    'settings' => $argument,
-    'merge' => $merge,
-  );
-}
-
-/**
- * Creates a Drupal Ajax 'add_css' command.
- *
- * This method will add css via ajax in a cross-browser compatible way.
- *
- * This command is implemented by Drupal.AjaxCommands.prototype.add_css()
- * defined in misc/ajax.js.
- *
- * @param $styles
- *   A string that contains the styles to be added.
- *
- * @return
- *   An array suitable for use with the ajax_render() function.
- *
- * @see misc/ajax.js
- */
-function ajax_command_add_css($styles) {
-  return array(
-    'command' => 'add_css',
-    'data' => $styles,
-  );
-}
diff --git a/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
new file mode 100644
index 0000000..7c4a344
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/AjaxResponseRenderer.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Ajax\AjaxResponseRenderer.
+ */
+
+namespace Drupal\Core\Ajax;
+
+use Drupal\Core\Page\HtmlFragment;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Converts a controller result into an Ajax response object.
+ */
+class AjaxResponseRenderer {
+
+  /**
+   * Converts a controller result into an Ajax response object.
+   *
+   * @var mixed $content
+   *   The result of a controller, so for example a string, a render array, a
+   *   HtmlFragment object, a Response object or even an AjaxResponse itself.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An Ajax response containing the controller result.
+   */
+  public function render($content) {
+    // If there is already an AjaxResponse, then return it without manipulation.
+    if ($content instanceof AjaxResponse && $content->isOk()) {
+      return $content;
+    }
+
+    // Allow controllers to return a HtmlFragment or a Response object directly.
+    if ($content instanceof HtmlFragment) {
+      $content = $content->getContent();
+    }
+    elseif ($content instanceof Response) {
+      $content = $content->getContent();
+    }
+    // Most controllers return a render array, but some return a string.
+    if (!is_array($content)) {
+      $content = array(
+        '#markup' => $content,
+      );
+    }
+
+    $response = new AjaxResponse();
+
+    if (isset($content['#type']) && ($content['#type'] == 'Ajax')) {
+      // Complex Ajax callbacks can return a result that contains an error
+      // message or a specific set of commands to send to the browser.
+      $content += element_info('Ajax');
+      $error = $content['#error'];
+      if (isset($error) && $error !== FALSE) {
+        if ((empty($error) || $error === TRUE)) {
+          $error = 'An error occurred while handling the request: The server received invalid input.';
+        }
+        $response->addCommand(new AlertCommand($error));
+      }
+    }
+
+    $html = drupal_render($content);
+
+    // The selector for the insert command is NULL as the new content will
+    // replace the element making the Ajax call. The default 'replaceWith'
+    // behavior can be changed with #ajax['method'].
+    $response->addCommand(new InsertCommand(NULL, $html));
+    $status_messages = array('#theme' => 'status_messages');
+    $output = drupal_render($status_messages);
+    if (!empty($output)) {
+      $response->addCommand(new PrependCommand(NULL, $output));
+    }
+    return $response;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Controller/AjaxController.php b/core/lib/Drupal/Core/Controller/AjaxController.php
index 2f8498e..f0c480e 100644
--- a/core/lib/Drupal/Core/Controller/AjaxController.php
+++ b/core/lib/Drupal/Core/Controller/AjaxController.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Controller;
 
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\AjaxResponseRenderer;
 use Drupal\Core\Ajax\InsertCommand;
 use Drupal\Core\Ajax\PrependCommand;
 use Drupal\Core\Page\HtmlFragment;
@@ -29,13 +30,23 @@ class AjaxController extends ContainerAware {
   protected $controllerResolver;
 
   /**
+   * The ajax response renderer.
+   *
+   * @var \Drupal\Core\Ajax\AjaxResponseRenderer
+   */
+  protected $ajaxRenderer;
+
+  /**
    * Constructs a new AjaxController instance.
    *
    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
    *   The controller resolver.
+   * @param \Drupal\Core\Ajax\AjaxResponseRenderer $ajax_renderer
+   *   The ajax response renderer.
    */
-  public function __construct(ControllerResolverInterface $controller_resolver) {
+  public function __construct(ControllerResolverInterface $controller_resolver, AjaxResponseRenderer $ajax_renderer) {
     $this->controllerResolver = $controller_resolver;
+    $this->ajaxRenderer = $ajax_renderer;
   }
 
   /**
@@ -46,45 +57,12 @@ public function __construct(ControllerResolverInterface $controller_resolver) {
    * @param callable $_content
    *   The callable that returns the content of the ajax response.
    *
-   * @return \Symfony\Component\HttpFoundation\Response
+   * @return \Drupal\Core\Ajax\AjaxResponse
    *   A response object.
    */
   public function content(Request $request, $_content) {
     $content = $this->getContentResult($request, $_content);
-    // If there is already an AjaxResponse, then return it without
-    // manipulation.
-    if ($content instanceof AjaxResponse && $content->isOk()) {
-      return $content;
-    }
-
-    // Allow controllers to return a HtmlFragment or a Response object directly.
-    if ($content instanceof HtmlFragment) {
-      $content = $content->getContent();
-    }
-    if ($content instanceof Response) {
-      $content = $content->getContent();
-    }
-
-    // Most controllers return a render array, but some return a string.
-    if (!is_array($content)) {
-      $content = array(
-        '#markup' => $content,
-      );
-    }
-
-    $html = drupal_render($content);
-
-    $response = new AjaxResponse();
-    // The selector for the insert command is NULL as the new content will
-    // replace the element making the ajax call. The default 'replaceWith'
-    // behavior can be changed with #ajax['method'].
-    $response->addCommand(new InsertCommand(NULL, $html));
-    $status_messages = array('#theme' => 'status_messages');
-    $output = drupal_render($status_messages);
-    if (!empty($output)) {
-      $response->addCommand(new PrependCommand(NULL, $output));
-    }
-    return $response;
+    return $this->ajaxRenderer->render($content);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
index 6fb011d..dc9ef35 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ViewSubscriber.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Ajax\AjaxResponseRenderer;
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Page\HtmlPage;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
@@ -43,16 +44,26 @@ class ViewSubscriber implements EventSubscriberInterface {
   protected $titleResolver;
 
   /**
+   * The ajax response renderer.
+   *
+   * @var \Drupal\Core\Ajax\AjaxResponseRenderer
+   */
+  protected $ajaxRenderer;
+
+  /**
    * Constructs a new ViewSubscriber.
    *
    * @param \Drupal\Core\ContentNegotiation $negotiation
    *   The content negotiation.
    * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
    *   The title resolver.
+   * @param \Drupal\Core\Ajax\AjaxResponseRenderer $ajax_renderer
+   *   The ajax response renderer.
    */
-  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver) {
+  public function __construct(ContentNegotiation $negotiation, TitleResolverInterface $title_resolver, AjaxResponseRenderer $ajax_renderer) {
     $this->negotiation = $negotiation;
     $this->titleResolver = $title_resolver;
+    $this->ajaxRenderer = $ajax_renderer;
   }
 
   /**
@@ -118,26 +129,8 @@ public function onJson(GetResponseForControllerResultEvent $event) {
     return $response;
   }
 
-  public function onAjax(GetResponseForControllerResultEvent $event) {
-    $page_callback_result = $event->getControllerResult();
-
-    // Construct the response content from the page callback result.
-    $commands = ajax_prepare_response($page_callback_result);
-    $json = ajax_render($commands);
-
-    // Build the actual response object.
-    $response = new JsonResponse();
-    $response->setContent($json);
-
-    return $response;
-  }
-
   public function onIframeUpload(GetResponseForControllerResultEvent $event) {
-    $page_callback_result = $event->getControllerResult();
-
-    // Construct the response content from the page callback result.
-    $commands = ajax_prepare_response($page_callback_result);
-    $json = ajax_render($commands);
+    $response = $event->getResponse();
 
     // Browser IFRAMEs expect HTML. Browser extensions, such as Linkification
     // and Skype's Browser Highlighter, convert URLs, phone numbers, etc. into
@@ -145,7 +138,7 @@ public function onIframeUpload(GetResponseForControllerResultEvent $event) {
     // JSON data by making it the value of a textarea.
     // @see http://malsup.com/jquery/form/#file-upload
     // @see http://drupal.org/node/1009382
-    $html = '<textarea>' . $json . '</textarea>';
+    $html = '<textarea>' . $response->getContent() . '</textarea>';
 
     return new Response($html);
   }
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php
index 4fcd9ea..b9aae48 100644
--- a/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php
+++ b/core/lib/Drupal/Core/Routing/Enhancer/AjaxEnhancer.php
@@ -37,7 +37,7 @@ public function __construct(ContentNegotiation $negotiation) {
    * {@inheritdoc}
    */
   public function enhance(array $defaults, Request $request) {
-    if (empty($defaults['_content']) && $defaults['_controller'] != 'controller.ajax:content' && $this->negotiation->getContentType($request) == 'drupal_ajax') {
+    if (empty($defaults['_content']) && $defaults['_controller'] != 'controller.ajax:content' && in_array($this->negotiation->getContentType($request), array('drupal_ajax', 'ajax', 'iframeupload'))) {
       $defaults['_content'] = isset($defaults['_controller']) ? $defaults['_controller'] : NULL;
       $defaults['_controller'] = 'controller.ajax:content';
     }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 265012e..17373ae 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -1698,7 +1698,8 @@ protected function drupalProcessAjaxResponse($content, array $ajax_response, arr
           }
           // @todo Ajax commands can target any jQuery selector, but these are
           //   hard to fully emulate with XPath. For now, just handle 'head'
-          //   and 'body', since these are used by ajax_render().
+          //   and 'body', since these are used by
+          // \Drupal\Core\Ajax\AjaxResponse::ajaxRender().
           elseif (in_array($command['selector'], array('head', 'body'))) {
             $wrapperNode = $xpath->query('//' . $command['selector'])->item(0);
           }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxTestBase.php
index a196f85..24a572a 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/AjaxTestBase.php
@@ -24,22 +24,22 @@
   /**
    * Asserts the array of Ajax commands contains the searched command.
    *
-   * The Ajax framework, via the ajax_render() function, returns an array of
-   * commands. This array sometimes includes commands automatically provided by
-   * the framework in addition to commands returned by a particular page
-   * callback. During testing, we're usually interested that a particular
-   * command is present, and don't care whether other commands precede or
-   * follow the one we're interested in. Additionally, the command we're
-   * interested in may include additional data that we're not interested in.
-   * Therefore, this function simply asserts that one of the commands in
-   * $haystack contains all of the keys and values in $needle. Furthermore, if
-   * $needle contains a 'settings' key with an array value, we simply assert
-   * that all keys and values within that array are present in the command we're
-   * checking, and do not consider it a failure if the actual command contains
-   * additional settings that aren't part of $needle.
+   * An AjaxResponse object stores an array of Ajax commands. This array
+   * sometimes includes commands automatically provided by the framework in
+   * addition to commands returned by a particular controller. During testing,
+   * we're usually interested that a particular command is present, and don't
+   * care whether other commands precede or follow the one we're interested in.
+   * Additionally, the command we're interested in may include additional data
+   * that we're not interested in. Therefore, this function simply asserts that
+   * one of the commands in $haystack contains all of the keys and values in
+   * $needle. Furthermore, if $needle contains a 'settings' key with an array
+   * value, we simply assert that all keys and values within that array are
+   * present in the command we're checking, and do not consider it a failure if
+   * the actual command contains additional settings that aren't part of
+   * $needle.
    *
    * @param $haystack
-   *   An array of Ajax commands returned by the server.
+   *   An array of rendered Ajax commands returned by the server.
    * @param $needle
    *   Array of info we're expecting in one of those commands.
    * @param $message
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
index eced538..a6d6615 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FrameworkTest.php
@@ -27,14 +27,14 @@ public static function getInfo() {
   }
 
   /**
-   * Ensures ajax_render() returns JavaScript settings from the page request.
+   * Ensures that AjaxController adds JavaScript settings from the page request.
    */
   public function testAJAXRender() {
     // Verify that settings command is generated when JavaScript settings are
     // set via drupal_add_js().
     $commands = $this->drupalGetAJAX('ajax-test/render');
     $expected = new SettingsCommand(array('ajax' => 'test'), TRUE);
-    $this->assertCommand($commands, $expected->render(), 'ajax_render() loads settings added with drupal_add_js().');
+    $this->assertCommand($commands, $expected->render(), '\Drupal\Core\Ajax\AjaxResponse::ajaxRender() loads settings added with drupal_add_js().');
   }
 
   /**
@@ -101,7 +101,7 @@ public function testOrder() {
   }
 
   /**
-   * Tests behavior of ajax_render_error().
+   * Tests the behavior of an error alert command.
    */
   public function testAJAXRenderError() {
     // Verify custom error message.
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index cabea75..2b5402c 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -399,17 +399,18 @@ function hook_css_alter(&$css) {
 }
 
 /**
- * Alter the commands that are sent to the user through the Ajax framework.
+ * Alter the Ajax command data that is sent to the client.
  *
- * @param $commands
- *   An array of all commands that will be sent to the user.
+ * @param \Drupal\Core\Ajax\CommandInterface[] $data
+ *   An array of all the rendered commands that will be sent to the client.
  *
- * @see ajax_render()
+ * @see \Drupal\Core\Ajax\AjaxResponse::ajaxRender()
  */
-function hook_ajax_render_alter($commands) {
+function hook_ajax_render_alter(array &$data) {
   // Inject any new status messages into the content area.
   $status_messages = array('#theme' => 'status_messages');
-  $commands[] = ajax_command_prepend('#block-system-main .content', drupal_render($status_messages));
+  $command = new \Drupal\Core\Ajax\PrependCommand('#block-system-main .content', drupal_render($status_messages));
+  $data[] = $command->render();
 }
 
 /**
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index eba9efb..c9e7918 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -21,11 +21,12 @@ function ajax_test_system_theme_info() {
 }
 
 /**
- * Menu callback: Returns an element suitable for use by ajax_render().
+ * Menu callback: Returns an element suitable for use by
+ * \Drupal\Core\Ajax\AjaxResponse::ajaxRender().
  *
- * Additionally ensures that ajax_render() incorporates JavaScript settings
- * generated during the page request by invoking drupal_add_js() with a dummy
- * setting.
+ * Additionally ensures that \Drupal\Core\Ajax\AjaxResponse::ajaxRender()
+ * incorporates JavaScript settings generated during the page request by
+ * invoking drupal_add_js() with a dummy setting.
  *
  * @deprecated \Drupal\ajax_test\Controller\AjaxTestController::render()
  */
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Form/Ajax/ViewsFormBase.php b/core/modules/views_ui/lib/Drupal/views_ui/Form/Ajax/ViewsFormBase.php
index cfb0a8b..b331a13 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/Form/Ajax/ViewsFormBase.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/Form/Ajax/ViewsFormBase.php
@@ -123,8 +123,9 @@ public function getForm(ViewStorageInterface $view, $display_id, $js) {
     // With the below logic, we may end up rendering a form twice (or two forms
     // each sharing the same element ids), potentially resulting in
     // drupal_add_js() being called twice to add the same setting. drupal_get_js()
-    // is ok with that, but until ajax_render() is (http://drupal.org/node/208611),
-    // reset the drupal_add_js() static before rendering the second time.
+    // is ok with that, but until \Drupal\Core\Ajax\AjaxResponse::ajaxRender()
+    // is (http://drupal.org/node/208611), reset the drupal_add_js() static
+    // before rendering the second time.
     $drupal_add_js_original = drupal_add_js();
     $drupal_add_js = &drupal_static('drupal_add_js');
     $response = views_ajax_form_wrapper($form_state['form_id'], $form_state);
