diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py
index b4cdeba..0acca94 100644
--- a/django/contrib/formtools/preview.py
+++ b/django/contrib/formtools/preview.py
@@ -1,39 +1,45 @@
 """
 Formtools Preview application.
 """
-
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
-
 from django.conf import settings
-from django.http import Http404
-from django.shortcuts import render_to_response
-from django.template.context import RequestContext
 from django.utils.crypto import constant_time_compare
 from django.contrib.formtools.utils import form_hmac
+from django.views.generic import FormView
 
+PREVIEW_STAGE = 'preview'
+POST_STAGE = 'post'
 AUTO_ID = 'formtools_%s' # Each form here uses this as its auto_id parameter.
+STAGE_FIELD = 'stage'
+HASH_FIELD = 'hash'
 
-class FormPreview(object):
+class FormPreview(FormView):
     preview_template = 'formtools/preview.html'
     form_template = 'formtools/form.html'
 
     # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
 
-    def __init__(self, form):
+    def __init__(self, form_class, *args, **kwargs):
+        super(FormPreview, self).__init__(*args, **kwargs)
         # form should be a Form class, not an instance.
-        self.form, self.state = form, {}
+        self.form_class = form_class
+        self._preview_stage = PREVIEW_STAGE
+        self._post_stage = POST_STAGE
+        self._stages = {'1': self._preview_stage, '2': self._post_stage}
+        # A relic of the past; override get_context_data to pass extra context
+        # to the template. Left in for backwards compatibility.
+        self.state = {}
 
     def __call__(self, request, *args, **kwargs):
-        stage = {'1': 'preview', '2': 'post'}.get(request.POST.get(self.unused_name('stage')), 'preview')
+        return self.dispatch(request, *args, **kwargs)
+
+    def dispatch(self, request, *args, **kwargs):
+        posted_stage = request.POST.get(self.unused_name(STAGE_FIELD))
+        self._stage = self._stages.get(posted_stage, self._preview_stage)
+
+        # For backwards compatiblity
         self.parse_params(*args, **kwargs)
-        try:
-            method = getattr(self, stage + '_' + request.method.lower())
-        except AttributeError:
-            raise Http404
-        return method(request)
+
+        return super(FormPreview, self).dispatch(request, *args, **kwargs)
 
     def unused_name(self, name):
         """
@@ -45,47 +51,54 @@ class FormPreview(object):
         """
         while 1:
             try:
-                f = self.form.base_fields[name]
+                self.form_class.base_fields[name]
             except KeyError:
                 break # This field name isn't being used by the form.
             name += '_'
         return name
 
-    def preview_get(self, request):
+    def _get_context_data(self, form):
+        """ For backwards compatiblity. """
+        context = self.get_context_data(form=form)
+        context.update(self.get_context(self.request, form))
+        return context
+
+    def get(self, request, *args, **kwargs):
         "Displays the form"
-        f = self.form(auto_id=self.get_auto_id(), initial=self.get_initial(request))
-        return render_to_response(self.form_template,
-            self.get_context(request, f),
-            context_instance=RequestContext(request))
+        form_class = self.get_form_class()
+        form = self.get_form(form_class)
+        context = self._get_context_data(form)
+        self.template_name = self.form_template
+        return self.render_to_response(context)
+
+    def _check_security_hash(self, token, form):
+        expected = self.security_hash(self.request, form)
+        return constant_time_compare(token, expected)
 
     def preview_post(self, request):
-        "Validates the POST data. If valid, displays the preview page. Else, redisplays form."
-        f = self.form(request.POST, auto_id=self.get_auto_id())
-        context = self.get_context(request, f)
-        if f.is_valid():
-            self.process_preview(request, f, context)
-            context['hash_field'] = self.unused_name('hash')
-            context['hash_value'] = self.security_hash(request, f)
-            return render_to_response(self.preview_template, context, context_instance=RequestContext(request))
+        """ For backwards compatibility. failed_hash calls this method by
+        default. """
+        self._stage = self._preview_stage
+        return self.post(request)
+
+    def form_valid(self, form):
+        context = self._get_context_data(form)
+        if self._stage == self._preview_stage:
+            self.process_preview(self.request, form, context)
+            context['hash_field'] = self.unused_name(HASH_FIELD)
+            context['hash_value'] = self.security_hash(self.request, form)
+            self.template_name = self.preview_template
+            return self.render_to_response(context)
         else:
-            return render_to_response(self.form_template, context, context_instance=RequestContext(request))
+            form_hash = self.request.POST.get(self.unused_name(HASH_FIELD), '')
+            if not self._check_security_hash(form_hash, form):
+                return self.failed_hash(self.request) # Security hash failed.
+            return self.done(self.request, form.cleaned_data)
 
-    def _check_security_hash(self, token, request, form):
-        expected = self.security_hash(request, form)
-        return constant_time_compare(token, expected)
-
-    def post_post(self, request):
-        "Validates the POST data. If valid, calls done(). Else, redisplays form."
-        f = self.form(request.POST, auto_id=self.get_auto_id())
-        if f.is_valid():
-            if not self._check_security_hash(request.POST.get(self.unused_name('hash'), ''),
-                                             request, f):
-                return self.failed_hash(request) # Security hash failed.
-            return self.done(request, f.cleaned_data)
-        else:
-            return render_to_response(self.form_template,
-                self.get_context(request, f),
-                context_instance=RequestContext(request))
+    def form_invalid(self, form):
+        context = self._get_context_data(form)
+        self.template_name = self.form_template
+        return self.render_to_response(context)
 
     # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
 
@@ -96,22 +109,41 @@ class FormPreview(object):
         """
         return AUTO_ID
 
-    def get_initial(self, request):
+    def get_initial(self, request=None):
         """
         Takes a request argument and returns a dictionary to pass to the form's
         ``initial`` kwarg when the form is being created from an HTTP get.
         """
-        return {}
+        return self.initial
 
     def get_context(self, request, form):
         "Context for template rendering."
-        return {'form': form, 'stage_field': self.unused_name('stage'), 'state': self.state}
-
+        context = {
+            'form': form,
+            'stage_field': self.unused_name(STAGE_FIELD),
+            'state': self.state
+        }
+        return context
+
+    def get_form_kwargs(self):
+        """ This is overriden to maintain backward compatibility and pass
+        the request to get_initial. """
+        kwargs = {
+            'initial': self.get_initial(self.request),
+            'auto_id': self.get_auto_id()
+        }
+        if self.request.method in ('POST', 'PUT'):
+            kwargs.update({
+                'data': self.request.POST,
+                'files': self.request.FILES,
+            })
+        return kwargs
 
     def parse_params(self, *args, **kwargs):
         """
-        Given captured args and kwargs from the URLconf, saves something in
-        self.state and/or raises Http404 if necessary.
+        Called in dispatch() prior to delegating the request to get() or post().
+        Given captured args and kwargs from the URLconf, allows the ability to
+        save something on the instance and/or raises Http404 if necessary.
 
         For example, this URLconf captures a user_id variable:
 
diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
index 7084386..4fba189 100644
--- a/django/contrib/formtools/tests/__init__.py
+++ b/django/contrib/formtools/tests/__init__.py
@@ -17,12 +17,18 @@ warnings.filterwarnings('ignore', category=PendingDeprecationWarning,
 
 success_string = "Done was called!"
 
+
 class TestFormPreview(preview.FormPreview):
     def get_context(self, request, form):
         context = super(TestFormPreview, self).get_context(request, form)
         context.update({'custom_context': True})
         return context
 
+    def get_context_data(self, **kwargs):
+        context = super(TestFormPreview, self).get_context_data(**kwargs)
+        context['is_bound_form'] = context['form'].is_bound
+        return context
+
     def get_initial(self, request):
         return {'field1': 'Works!'}
 
@@ -67,8 +73,15 @@ class PreviewTests(TestCase):
         stage = self.input % 1
         self.assertContains(response, stage, 1)
         self.assertEqual(response.context['custom_context'], True)
+        self.assertEqual(response.context['is_bound_form'], False)
         self.assertEqual(response.context['form'].initial, {'field1': 'Works!'})
 
+    def test_invalid_form(self):
+        """ Verifies that an invalid form displays the correct template. """
+        self.test_data.pop('field1')
+        response = self.client.post('/preview/', self.test_data)
+        self.assertTemplateUsed(response, 'formtools/form.html')
+
     def test_form_preview(self):
         """
         Test contrib.formtools.preview form preview rendering.
@@ -86,6 +99,10 @@ class PreviewTests(TestCase):
         stage = self.input % 2
         self.assertContains(response, stage, 1)
 
+        # Check that the correct context was passed to the template
+        self.assertEqual(response.context['custom_context'], True)
+        self.assertEqual(response.context['is_bound_form'], True)
+
     def test_form_submit(self):
         """
         Test contrib.formtools.preview form submittal.
@@ -140,7 +157,6 @@ class PreviewTests(TestCase):
         response = self.client.post('/preview/', self.test_data)
         self.assertEqual(response.content, success_string)
 
-
     def test_form_submit_bad_hash(self):
         """
         Test contrib.formtools.preview form submittal does not proceed
@@ -154,7 +170,8 @@ class PreviewTests(TestCase):
         self.assertNotEqual(response.content, success_string)
         hash = utils.form_hmac(TestForm(self.test_data)) + "bad"
         self.test_data.update({'hash': hash})
-        response = self.client.post('/previewpreview/', self.test_data)
+        response = self.client.post('/preview/', self.test_data)
+        self.assertTemplateUsed(response, 'formtools/preview.html')
         self.assertNotEqual(response.content, success_string)
 
 
diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt
index c5d8b9a..d40ee97 100644
--- a/docs/ref/contrib/formtools/form-preview.txt
+++ b/docs/ref/contrib/formtools/form-preview.txt
@@ -17,7 +17,7 @@ Python class.
 Overview
 =========
 
-Given a :class:`django.forms.Form` subclass that you define, this
+Given a :class:`~django.forms.Form` subclass that you define, this
 application takes care of the following workflow:
 
    1. Displays the form as HTML on a Web page.
@@ -36,7 +36,7 @@ on the preview page, the form submission will fail the hash-comparison test.
 How to use ``FormPreview``
 ==========================
 
-    1. Point Django at the default FormPreview templates. There are two ways to
+    1. Point Django to the default FormPreview templates. There are two ways to
        do this:
 
             * Add ``'django.contrib.formtools'`` to your
@@ -89,33 +89,171 @@ How to use ``FormPreview``
 
 .. class:: FormPreview
 
-A :class:`~django.contrib.formtools.preview.FormPreview` class is a simple Python class
-that represents the preview workflow.
-:class:`~django.contrib.formtools.preview.FormPreview` classes must subclass
+.. versionchanged:: 1.4
+   :class:`~django.contrib.formtools.preview.FormPreview` is now based on
+   :doc:`generic class-based views </ref/class-based-views>`.
+
+:class:`~django.contrib.formtools.preview.FormPreview` is a subclass
+of the class-based generic :class:`~django.views.generic.edit.FormView` and
+implements the preview workflow described above. To use this workflow
+in your application, subclass
 ``django.contrib.formtools.preview.FormPreview`` and override the
-:meth:`~django.contrib.formtools.preview.FormPreview.done()` method. They can live
-anywhere in your codebase.
+:meth:`~django.contrib.formtools.preview.FormPreview.done()`
+method. In addition to the
+:meth:`~django.contrib.formtools.preview.FormPreview.done()` method,
+there are other helpful methods you might wish to override described
+below. The class should live in the application's ``views.py`` file.
 
 ``FormPreview`` templates
 =========================
 
-By default, the form is rendered via the template :file:`formtools/form.html`,
-and the preview page is rendered via the template :file:`formtools/preview.html`.
-These values can be overridden for a particular form preview by setting
-:attr:`~django.contrib.formtools.preview.FormPreview.preview_template` and
-:attr:`~django.contrib.formtools.preview.FormPreview.form_template` attributes on the
-FormPreview subclass. See :file:`django/contrib/formtools/templates` for the
-default templates.
+During the form preview process, two templates will be displayed:
+
+    1. The template specified by the attribute
+       :attr:`~django.contrib.foremtools.preview.FormPreview.form_template`,
+       displayed after the initial ``GET`` request. The default
+       template is :file:`formtools/form.html`.
+
+    2. The template specified by the attribute
+       :attr:`~django.contrib.formtools.preview.FormPreview.preview_template`,
+       used to display the preview page. The default template is
+       :file:`formtools/preview.html`.
+
+The template specified by
+:attr:`~django.contrib.formtools.preview.FormPreview.form_template` is
+passed ``stage_field`` in the context. This special field needs to
+be included in the template:
+
+.. code-block:: html+django
+
+    <form action="." method="post">{% csrf_token %}
+    <table>
+    {{ form }}
+    </table>
+    <input type="hidden" name="{{ stage_field }}" value="1" />
+    <input type="submit" value="Preview" />
+    </form>
+
+Note that the value of ``stage_field`` is set to ``1`` to indicate
+that this is the first time the data is being submitted and that the
+view should display a preview.
+
+The template specified by
+:attr:`~django.contrib.formtoolspreview.FormPreview.preview_template`
+is used to preview the user's form selection and provide a way to
+submit the form again. ``stage_field`` will be passed to the template
+and its value should be set to ``2``, telling the
+:class:`~django.contrib.formtools.preview.FormPreview` view that the
+form is being posted a second and final time and that the
+:meth:`~django.contrib.formtools.preview.FormPreview.done()` method
+should be called. Two items related to the security hash of the form
+are also passed to the template and must be submitted:
+
+    1. ``hash_field`` - The name to use for the ``<input>`` which
+       will store the calculated hash value of the
+       :class:`~django.forms.Form` instance.
+
+    2. ``hash_value`` - The actual value of the security hash.
+
+The ``<form>`` part of the preview template might look like:
+
+.. code-block:: html+django
+
+    <form action="" method="post">{% csrf_token %}
+    {% for field in form %}{{ field.as_hidden }}
+    {% endfor %}
+    <input type="hidden" name="{{ stage_field }}" value="2" />
+    <input type="hidden" name="{{ hash_field }}" value="{{ hash_value }}" />
+    <p><input type="submit" value="Submit" /></p>
+    </form>
+
+The above template will hide the form fields from the user although
+this isn't required.
+
+The values of ``stage_field`` and ``hash_field`` are guaranteed not to
+conflict with any of the field names of your
+:class:`~django.forms.Form`.
+
 
 Advanced ``FormPreview`` methods
 ================================
 
+:meth:`~done()` is the only method you are required to implement on
+your subclass of
+:class:`~django.contrib.formtools.preview.FormPreview`. Aside from
+the methods provided by the
+:class:`~django.views.generic.edit.FormView`, you can override a
+number of other methods to customize the form preview process.
+
+.. note::
+
+    Many of :class:`~django.contrib.formtools.preview.FormPreview`'s
+    methods take an :class:`~.django.http.HttpRequest` object as an
+    argument. This is to maintain backward compatibility with previous
+    versions of the class. Instances of
+    :class:`~django.contrib.formtools.preview.FormPreview`, like all
+    class-based generic views, have a ``request`` attribute containing
+    the current request.
+
+.. method:: FormPreview.get_auto_id()
+
+    Returns a value that is passed to the form as the ``auto_id``
+    keyword argument. See the :doc:`Forms API </ref/forms/api>` for
+    more information on ``auto_id.``. The default value is
+    ``formtools``.
+
+.. method:: FormPreview.get_initial(request)
+
+    Returns a dictionary that is passed to the form as ``initial``. The
+    default value is an empty dictionary. Note that unlike the
+    ``get_initial`` method on other class-based generic views, this
+    method takes an :class:`~.django.http.HttpRequest` object as an
+    argument. This is to maintain backwards compatibility with
+    previous versions of the method.
+
+.. method:: FormPreview.parse_params(*args, **kwargs)
+
+    Called prior to dispatching the ``get`` or ``post`` methods, allows
+    you to set attributes on the
+    :class:`~django.contrib.formtools.preview.FormPreview` in a
+    convenient way. It takes the captured ``args`` and ``kwargs`` from
+    the URLconf as arguments. For example, let's say you had the
+    following in your URLconf::
+
+        (r'^contact/(?P<user_id>\d+)/$', MyFormPreview(MyForm)),
+
+    You could do the following to save the user's ID for use when
+    :meth:`~django.contrib.formtools.preview.FormPreview.done()` is
+    called::
+
+        def parse_params(*args, **kwargs):
+            self.user_id = kwargs.get('user_id')
+
+    This method is empty by default.
+
 .. versionadded:: 1.2
 
-.. method:: FormPreview.process_preview
+.. method:: FormPreview.process_preview(request, form, context)
 
     Given a validated form, performs any extra processing before displaying the
     preview page, and saves any extra data in context.
 
     By default, this method is empty.  It is called after the form is validated,
     but before the context is modified with hash information and rendered.
+
+.. method:: FormPreview.security_hash(request, form)
+
+    Calculates the security hash for the given instances of the
+    :class:`~.django.http.HttpRequest` and :class:`~django.forms.Form`.
+
+    Subclasses may want to take into account request-specific information,
+    such as the IP address but this method is rarely overriden.
+
+    By default, this returns a generated SHA1 HMAC using the form
+    instance and your :setting:`SECRET_KEY`.
+
+.. method:: FormPreview.failed_hash(request)
+
+   Returns an :class:`~django.http.HttpResponse` when the security hash
+   check fails. By default this returns the user to the preview stage
+   of the process.
