diff --git a/docs/views.api.php b/docs/views.api.php
index b2bcc62..6725627 100644
--- a/docs/views.api.php
+++ b/docs/views.api.php
@@ -555,6 +555,32 @@ function hook_views_query_substitutions() {
 }
 
 /**
+ * This hook is called to get a list of placeholders and their substitutions,
+ * used when preprocessing a View with form elements.
+ */
+function hook_views_form_substitutions() {
+  return array(
+    '<!--views-form-example-substitutions-->' => 'Example Substitution',
+  );
+}
+
+/**
+ * Views form (View with form elements) validate handler.
+ * Called for all steps ($form_state['step']) of the multistep form.
+ */
+function hook_views_form_validate($form, &$form_state) {
+  // example code here
+}
+
+/**
+ * Views form (View with form elements) submit handler.
+ * Called for all steps ($form_state['step']) of the multistep form.
+ */
+function hook_views_form_submit($form, &$form_state) {
+  // example code here
+}
+
+/**
  * This hook is called at the very beginning of views processing,
  * before anything is done.
  *
diff --git a/theme/theme.inc b/theme/theme.inc
index b481cf7..bf772b2 100644
--- a/theme/theme.inc
+++ b/theme/theme.inc
@@ -140,6 +140,30 @@ function template_preprocess_views_view(&$vars) {
   }
   // Flatten the classes to a string for the template file.
   $vars['classes'] = implode(' ', $vars['classes_array']);
+
+  // Check the fields on the View to see if any are adding form elements.
+  $has_form_fields = FALSE;
+  foreach ($view->field as $field_name => $field) {
+    if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
+      $has_form_fields = TRUE;
+      break;
+    }
+  }
+  // If form fields were found in the View, reformat the View output as a form.
+  if ($has_form_fields) {
+    $form = drupal_get_form(views_form_id($view), $view, $vars['rows']);
+    // The form is requesting that all non-essential views elements be hidden,
+    // usually because the rendered step is not a view result.
+    if ($form['show_view_elements']['#value'] == FALSE) {
+      $vars['header'] = '';
+      $vars['exposed'] = '';
+      $vars['pager'] = '';
+      $vars['footer'] = '';
+      $vars['more'] = '';
+      $vars['feed_icon'] = '';
+    }
+    $vars['rows'] = drupal_render($form);
+  }
 }
 
 /**
@@ -844,6 +868,40 @@ function template_preprocess_views_exposed_form(&$vars) {
   $vars['button'] = drupal_render_children($form);
 }
 
+/**
+ * Theme function for a View with form elements: replace the placeholders.
+ */
+function theme_views_form_views_form($variables) {
+  $form = $variables['form'];
+  $view = $form['view']['#value'];
+
+  // Placeholders and their substitutions (usually rendered form elements).
+  $search = array();
+  $replace = array();
+
+  // Prepare substitutions for views form elements.
+  foreach ($view->field as $field_name => $field) {
+    if (property_exists($field, 'views_form_callback') || method_exists($field, 'views_form')) {
+      foreach ($view->result as $row_id => $row) {
+        $search[] = '<!--form-item-' . $field_name . '--' . $row_id . '-->';
+        $replace[] = isset($form[$field_name][$row_id]) ? drupal_render($form[$field_name][$row_id]) : '';
+      }
+    }
+  }
+  // Add in substitutions from hook_views_form_substitutions().
+  $substitutions = module_invoke_all('views_form_substitutions');
+  foreach ($substitutions as $placeholder => $substitution) {
+    $search[] = $placeholder;
+    $replace[] = $substitution;
+  }
+
+  // Apply substitutions to the rendered output.
+  $form['output']['#markup'] = str_replace($search, $replace, $form['output']['#markup']);
+
+  // Render and add remaining form fields.
+  return drupal_render_children($form);
+}
+
 function theme_views_mini_pager($vars) {
   global $pager_page_array, $pager_total;
 
diff --git a/views.module b/views.module
index 355a464..b6756ed 100644
--- a/views.module
+++ b/views.module
@@ -16,6 +16,37 @@ function views_api_version() {
 }
 
 /**
+ * Implements hook_forms().
+ *
+ * To provide distinct form IDs for Views forms, the View name and
+ * specific display name are appended to the base ID,
+ * views_form_views_form. When such a form is built or submitted, this
+ * function will return the proper callback function to use for the given form.
+ */
+function views_forms($form_id, $args) {
+  if (strpos($form_id, 'views_form_') === 0) {
+    return array(
+      $form_id => array(
+        'callback' => 'views_form',
+      ),
+    );
+  }
+}
+
+/**
+ * Returns a form ID for a Views form using the name and display of the View.
+ */
+function views_form_id($view) {
+  $parts = array(
+    'views_form',
+    $view->name,
+    $view->current_display,
+  );
+
+  return implode('_', $parts);
+}
+
+/**
  * Views will not load plugins advertising a version older than this.
  */
 function views_api_minimum_version() {
@@ -100,6 +131,10 @@ function views_theme($existing, $type, $theme, $path) {
     }
   }
 
+  $hooks['views_form_views_form'] = $base + array(
+    'render element' => 'form',
+  );
+
   $hooks['views_exposed_form'] = $base + array(
     'template' => 'views-exposed-form',
     'pattern' => 'views_exposed_form__',
@@ -1437,6 +1472,138 @@ function vpr_trace() {
 }
 
 // ------------------------------------------------------------------
+// Views form (View with form elements)
+
+/**
+ * This is the entry function. Just gets the form for the current step.
+ * The form is always assumed to be multistep, even if it has only one
+ * step (the default 'views_form_vews_form' step). That way it is actually
+ * possible for modules to have a multistep form if they need to.
+ */
+function views_form($form, &$form_state, $view, $output) {
+  $form_state['step'] = isset($form_state['step']) ? $form_state['step'] : 'views_form_views_form';
+
+  $form = array();
+  $form['#validate'] = array('views_form_validate');
+  $form['#submit'] = array('views_form_submit');
+  // The view is stored in $form and not $form_state because
+  // the theme functions only get $form.
+  $form['view'] = array(
+    '#type' => 'value',
+    '#value' => $view,
+  );
+  // Tell the preprocessor whether it should hide the header, footer, pager...
+  $form['show_view_elements'] = array(
+    '#type' => 'value',
+    '#value' => ($form_state['step'] == 'views_form_views_form') ? TRUE : FALSE,
+  );
+
+  $form = $form_state['step']($form, $form_state, $view, $output);
+  return $form;
+}
+
+/**
+ * The basic form validate handler.
+ * Fires the hook_views_form_validate() function.
+ */
+function views_form_validate($form, &$form_state) {
+  // module_invoke_all() doesn't support references, hence this approach.
+  foreach (module_implements('views_form_validate') as $module) {
+    $function = $module . '_views_form_validate';
+    $function($form, &$form_state);
+  }
+}
+
+/**
+ * The basic form submit handler.
+ * Fires the hook_views_form_submit() function.
+ */
+function views_form_submit($form, &$form_state) {
+  // module_invoke_all() doesn't support references, hence this approach.
+  foreach (module_implements('views_form_submit') as $module) {
+    $function = $module . '_views_form_submit';
+    $function($form, &$form_state);
+  }
+}
+
+/**
+ * This function was sponsored by the Department of Redundancy Department.
+ */
+function views_form_views_form($form, &$form_state, $view, $output) {
+  $form['#prefix'] = '<div class="views-form">';
+  $form['#suffix'] = '</div>';
+  $form['#theme'] = 'views_form_views_form';
+  $form['#validate'][] = 'views_form_views_form_validate';
+  $form['#submit'][] = 'views_form_views_form_submit';
+
+  // Add the output markup to the form array so that it's included when the form
+  // array is passed to the theme function.
+  $form['output'] = array(
+    '#type' => 'markup',
+    '#markup' => $output,
+    // This way any additional form elements will go before the view
+    // (below the exposed widgets).
+    '#weight' => 50,
+  );
+
+  foreach ($view->field as $field_name => $field) {
+    // Retrieve the form array for this item from the handler.
+    if (property_exists($field, 'views_form_callback')) {
+      $callback = $field->views_form_callback;
+      $form = $callback($view, $field, $form, $form_state);
+    }
+    elseif (method_exists($field, 'views_form')) {
+      $form = $field->views_form($form, $form_state);
+    }
+  }
+
+  $form['actions'] = array(
+    '#type' => 'container',
+    '#attributes' => array('class' => array('form-actions')),
+    '#weight' => 100,
+  );
+
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate handler for the first step of the views form.
+ * Calls any existing views_form_validate functions located
+ * on the views fields.
+ */
+function views_form_views_form_validate($form, &$form_state) {
+  $view = $form['view']['#value'];
+
+  // Call the validation method on every field handler that has it.
+  foreach ($view->field as $field_name => $field) {
+    if (method_exists($field, 'views_form_validate')) {
+      $field->views_form_validate($form, $form_state);
+    }
+  }
+}
+
+/**
+ * Submit handler for the first step of the views form.
+ * Calls any existing views_form_submit functions located
+ * on the views fields.
+ */
+function views_form_views_form_submit($form, &$form_state) {
+  $view = $form['view']['#value'];
+
+  // Call the submit method on every field handler that has it.
+  foreach ($view->field as $field_name => $field) {
+    if (method_exists($field, 'views_form_submit')) {
+      $field->views_form_submit($form, $form_state);
+    }
+  }
+}
+
+// ------------------------------------------------------------------
 // Exposed widgets form
 
 /**
