diff --git a/docs/docs.php b/docs/docs.php
index d29cd5e..f5bc8ba 100644
--- a/docs/docs.php
+++ b/docs/docs.php
@@ -286,7 +286,7 @@ function hook_views_handlers() {
 function hook_views_api() {
   return array(
     'api' => 2,
-    'path' => drupal_get_path('module', 'example') . '/includes/views', 
+    'path' => drupal_get_path('module', 'example') . '/includes/views',
   );
 }
 
@@ -298,13 +298,13 @@ function hook_views_api() {
  * auto-loaded. This must either be in the same directory as the .module file
  * or in a subdirectory named 'includes'.
  *
- * This hook requires an array of views, where each array has key/value pair and must 
+ * This hook requires an array of views, where each array has key/value pair and must
  * have $value == $view->name, it is invalid if the keys not match.
- *  
+ *
  * The $view->disabled boolean flag indicates whether the View should be
  * enabled or disabled by default.
- * 
- *  
+ *
+ *
  * @return
  *   An associative array containing the structures of views, as generated from
  *   the Export tab, keyed by the view name. A best practice is to go through
@@ -578,6 +578,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['storage']['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['storage']['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/help/api-forms.html b/help/api-forms.html
new file mode 100644
index 0000000..39d50b8
--- /dev/null
+++ b/help/api-forms.html
@@ -0,0 +1,80 @@
+Views allows handlers to output form elements, wrapping them automatically in a form, and handling validation / submission.
+The form is multistep by default, allowing other modules to add additional steps, such as confirmation screens.
+
+<h2>Implementation</h2>
+A views handler outputs a special placeholder in render(), and the real form with matching structure in views_form().
+When the View is being preprocessed for the theme file, all placeholders get replaced with the rendered form elements.
+
+Note that views_form() gets the whole form, and needs to return the whole form.
+
+The views handler can also implement views_form_validate() and views_form_submit().
+<pre>
+  function render($values) {
+    return '&lt;!--form-item-' . $this-&gt;options['id'] . '--' . $this-&gt;view-&gt;row_index . '--&gt;';
+  }
+
+  function views_form($form, $form_state) {
+    // The view is empty, abort.
+    if (empty($this-&gt;view-&gt;result)) {
+      return $form;
+    }
+
+    $field_name = $this-&gt;options['id'];
+    $form[$field_name] = array(
+      '#tree' => TRUE,
+    );
+    // At this point, the query has already been run, so we can access the results
+    foreach ($this-&gt;view-&gt;result as $row_id => $row) {
+      $form[$field_name][$row_id] = array(
+        '#type' => 'textfield',
+        '#title' => t('Your name'),
+        '#default_value' => '',
+      );
+    }
+    return $form;
+  }
+
+  // Optional validate function.
+  function views_form_validate($form, $form_state) {
+    $field_name = $this-&gt;options['id'];
+    foreach ($form_state['values'][$field_name] as $row_id => $value) {
+      if ($value == 'Bojan') {
+        form_set_error($field_name . '][' . $row_id, "You can't be named Bojan. That's my name.");
+      }
+    }
+  }
+
+  // Optional submit function.
+  function views_form_submit($form, $form_state) {
+    // Do something here
+  }
+</pre>
+
+Modules can implement hook_views_form_validate($form, &$form_state) and hook_views_form_submit($form, &$form_state).
+
+The form is multistep by default, with one step: 'views_form_views_form'.
+A "form_example" module could add a confirmation step by setting:
+<pre>
+ $form_state['storage']['step'] = 'form_example_confirmation';
+</pre>
+in form_example_views_form_submit().
+Then, views_form would call form_example_confirmation($form, $form_state, $view, $output) to get that step.
+
+<h2>Relevant Views functions</h2>
+<ul>
+<li>template_preprocess_views_view()</li>
+<li>views_form()</li>
+<li>views_form_validate()</li>
+<li>views_form_submit()</li>
+<li>views_form_views_form()</li>
+<li>views_form_views_form_validate()</li>
+<li>views_form_views_form_submit()</li>
+<li>theme_views_form_views_form()</li>
+</ul>
+
+<h2>Hooks</h2>
+<ul>
+<li>hook_views_form_substitutions()</li>
+<li>hook_views_form_validate($form, &$form_state)</li>
+<li>hook_views_form_submit($form, &$form_state)</li>
+</ul>
\ No newline at end of file
diff --git a/help/views.help.ini b/help/views.help.ini
index 428fd0d..fd35687 100644
--- a/help/views.help.ini
+++ b/help/views.help.ini
@@ -84,12 +84,12 @@ parent = display
 
 [style-settings]
 title = "Style settings"
-weight = 30 
+weight = 30
 
 [style]
 title = "Style"
 parent = style-settings
-weight = -20 
+weight = -20
 
 [style-grid]
 title = "Grid (output style)"
@@ -118,7 +118,7 @@ weight = 50
 
 [style-row]
 title = "Row styles"
-weight = -10 
+weight = -10
 parent = style-settings
 
 [style-jump]
@@ -156,7 +156,7 @@ parent = performance
 [analyze-theme]
 title = "Theme information"
 parent = style-settings
-weight = 30 
+weight = 30
 
 [using-theme]
 title = "Using Views templates"
@@ -166,7 +166,7 @@ weight = 40
 [theme-css]
 title = "Using CSS with Views"
 parent = style-settings
-weight = 20 
+weight = 20
 
 [menu]
 title = "Menu options (page display)"
@@ -182,11 +182,11 @@ weight = 45
 
 [header]
 title = "Header"
-weight = 50 
+weight = 50
 
 [footer]
 title = "Footer"
-weight = 60 
+weight = 60
 
 [empty-text]
 title = "Empty Text"
@@ -194,15 +194,15 @@ weight = 70
 
 [field]
 title = "Fields"
-weight = 80 
+weight = 80
 
 [relationship]
 title = "Relationships"
-weight = 90 
+weight = 90
 
 [argument]
 title = "Arguments"
-weight = 100 
+weight = 100
 
 [style-summary-unformatted]
 title = "Summary Style: Unformatted (output style)"
@@ -214,7 +214,7 @@ parent = argument
 
 [sort]
 title = "Sort criteria"
-weight = 110 
+weight = 110
 
 [filter]
 title = "Filters"
@@ -265,6 +265,11 @@ title = "How Views plugins work"
 weight = -40
 parent = api
 
+[api-forms]
+title = "Outputting form elements from handlers"
+weight = -30
+parent = api
+
 [api-upgrading]
 title = "Upgrading your module Views 1 to Views 2"
 parent = api
diff --git a/theme/theme.inc b/theme/theme.inc
index ecb6729..3aaf728 100644
--- a/theme/theme.inc
+++ b/theme/theme.inc
@@ -144,6 +144,29 @@ 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) {
+    $vars['rows']  = 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 ($view->show_view_elements == FALSE) {
+      $vars['header'] = '';
+      $vars['exposed'] = '';
+      $vars['pager'] = '';
+      $vars['footer'] = '';
+      $vars['more'] = '';
+      $vars['feed_icon'] = '';
+    }
+  }
 }
 
 /**
@@ -846,6 +869,39 @@ function template_preprocess_views_exposed_form(&$vars) {
   $vars['button'] = drupal_render($form);
 }
 
+/**
+  * Theme function for a View with form elements: replace the placeholders.
+  */
+function theme_views_form_views_form($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']['#value'] = str_replace($search, $replace, $form['output']['#value']);
+
+  // Render and add remaining form fields.
+  return drupal_render($form);
+}
+
 function theme_views_mini_pager($tags = array(), $limit = 10, $element = 0, $parameters = array(), $quantity = 9) {
   global $pager_page_array, $pager_total;
 
diff --git a/views.module b/views.module
index 97d7f39..7bbfd91 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() {
@@ -104,6 +135,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__',
@@ -1100,6 +1135,128 @@ 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_state, $view, $output) {
+  $form_state['storage']['step'] = isset($form_state['storage']['step']) ? $form_state['storage']['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...
+  $view->show_view_elements = ($form_state['storage']['step'] == 'views_form_views_form') ? TRUE : FALSE;
+
+  $form = $form_state['storage']['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) {
+  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) {
+  foreach (module_implements('views_form_submit') as $module) {
+    $function = $module . '_views_form_submit';
+    $function($form, $form_state);
+  }
+}
+
+/**
+ * Callback for the main step of a Views form.
+ * Invoked by views_form().
+ */
+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(
+    '#value' => $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['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+    '#weight' => 100,
+  );
+
+  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
 
 /**
