diff --git a/trac/admin/web_ui.py b/trac/admin/web_ui.py
--- a/trac/admin/web_ui.py
+++ b/trac/admin/web_ui.py
@@ -33,8 +33,9 @@
 from trac.util.text import to_unicode
 from trac.util.translation import _
 from trac.web import HTTPNotFound, IRequestHandler
-from trac.web.chrome import add_script, add_stylesheet, add_warning, Chrome, \
-                            INavigationContributor, ITemplateProvider
+from trac.web.chrome import add_notice, add_script, add_stylesheet, \
+                            add_warning, Chrome, INavigationContributor, \
+                            ITemplateProvider
 
 try:
     from webadmin import IAdminPageProvider
@@ -190,6 +191,7 @@
             for option in ('name', 'url', 'descr'):
                 self.config.set('project', option, req.args.get(option))
             self.config.save()
+            add_notice(req, _("Your changes have been saved."))
             req.redirect(req.href.admin(cat, page))
 
         data = {
@@ -274,6 +276,7 @@
 
             if changed:
                 self.config.save()
+            add_notice(req, _("Your changes have been saved."))
             req.redirect(req.href.admin(cat, page))
 
         data = {
@@ -321,6 +324,9 @@
                 req.perm.require(action)
                 if (subject, action) not in all_permissions:
                     perm.grant_permission(subject, action)
+                    add_notice(req, _("The user %(user)s has been granted "
+                                      "the permission %(perm)s.", user=subject,
+                                      perm=action))
                     req.redirect(req.href.admin(cat, page))
                 else:
                     add_warning(req,
@@ -338,8 +344,11 @@
                             % (subject, group, action))
                     else:
                         req.perm.require(action)
-                if (subject,group) not in all_permissions:
+                if (subject, group) not in all_permissions:
                     perm.grant_permission(subject, group)
+                    add_notice(req, _("The user '%(user)s' has been added to "
+                                      "the group '%(group)s'.", user=subject,
+                                      group=group))
                     req.redirect(req.href.admin(cat, page))
                 else:
                     add_warning(req, 
@@ -355,6 +364,8 @@
                     subject, action = key.split(':', 1)
                     if (subject, action) in perm.get_all_permissions():
                         perm.revoke_permission(subject, action)
+                add_notice(req, _("The selected permissions have been "
+                                  "revoked."))
                 req.redirect(req.href.admin(cat, page))
 
         return 'admin_perms.html', {
diff --git a/trac/prefs/web_ui.py b/trac/prefs/web_ui.py
--- a/trac/prefs/web_ui.py
+++ b/trac/prefs/web_ui.py
@@ -25,8 +25,8 @@
 from trac.util.datefmt import all_timezones, get_timezone, localtz
 from trac.util.translation import _
 from trac.web import HTTPNotFound, IRequestHandler
-from trac.web.chrome import add_stylesheet, INavigationContributor, \
-                            ITemplateProvider
+from trac.web.chrome import add_notice, add_stylesheet, \
+                            INavigationContributor, ITemplateProvider
 
 
 class PreferencesModule(Component):
@@ -125,6 +125,7 @@
             elif field in req.session and (field in req.args or
                                            field + '_cb' in req.args):
                 del req.session[field]
+        add_notice(req, _("Your preferences have been saved."))
 
     def _do_load(self, req):
         if req.authname == 'anonymous':
diff --git a/trac/ticket/admin.py b/trac/ticket/admin.py
--- a/trac/ticket/admin.py
+++ b/trac/ticket/admin.py
@@ -21,7 +21,7 @@
 from trac.util.datefmt import utc, parse_date, get_date_format_hint, \
                               get_datetime_format_hint
 from trac.util.translation import _
-from trac.web.chrome import add_link, add_script
+from trac.web.chrome import add_link, add_notice, add_script
 
 
 class TicketAdminPanel(Component):
@@ -62,6 +62,7 @@
                     comp.owner = req.args.get('owner')
                     comp.description = req.args.get('description')
                     comp.update()
+                    add_notice(req, _("Your changes have been saved."))
                     req.redirect(req.href.admin(cat, page))
                 elif req.args.get('cancel'):
                     req.redirect(req.href.admin(cat, page))
@@ -82,6 +83,8 @@
                         if req.args.get('owner'):
                             comp.owner = req.args.get('owner')
                         comp.insert()
+                        add_notice(req, _("The component '%(name)s' has been "
+                                          "added.", name=name))
                         req.redirect(req.href.admin(cat, page))
                     else:
                         raise TracError(_('Component %s already exists.') % name)
@@ -98,6 +101,8 @@
                         comp = model.Component(self.env, name, db=db)
                         comp.delete(db=db)
                     db.commit()
+                    add_notice(req, _("The selected components have been "
+                                      "removed."))
                     req.redirect(req.href.admin(cat, page))
 
                 # Set default component
@@ -107,6 +112,7 @@
                         self.log.info('Setting default component to %s', name)
                         self.config.set('ticket', 'default_component', name)
                         self.config.save()
+                        add_notice(req, _("Your changes have been saved."))
                         req.redirect(req.href.admin(cat, page))
 
             default = self.config.get('ticket', 'default_component')
@@ -166,6 +172,7 @@
                                             _('Invalid Completion Date'))
                     mil.description = req.args.get('description', '')
                     mil.update()
+                    add_notice(req, _("Your changes have been saved."))
                     req.redirect(req.href.admin(cat, page))
                 elif req.args.get('cancel'):
                     req.redirect(req.href.admin(cat, page))
@@ -188,6 +195,8 @@
                             mil.due = parse_date(req.args.get('duedate'),
                                                  req.tz)
                         mil.insert()
+                        add_notice(req, _("The milestone '%(name)s' has been "
+                                          "added.", name=name))
                         req.redirect(req.href.admin(cat, page))
                     else:
                         raise TracError(_('Milestone %s already exists.') % name)
@@ -205,6 +214,8 @@
                         mil = model.Milestone(self.env, name, db=db)
                         mil.delete(db=db, author=req.authname)
                     db.commit()
+                    add_notice(req, _("The selected milestones have been "
+                                      "removed."))
                     req.redirect(req.href.admin(cat, page))
 
                 # Set default milestone
@@ -214,6 +225,7 @@
                         self.log.info('Setting default milestone to %s', name)
                         self.config.set('ticket', 'default_milestone', name)
                         self.config.save()
+                        add_notice(req, _("Your changes have been saved."))
                         req.redirect(req.href.admin(cat, page))
 
             # Get ticket count
@@ -258,6 +270,7 @@
                         ver.time = None # unset
                     ver.description = req.args.get('description')
                     ver.update()
+                    add_notice(req, _("Your changes have been saved."))
                     req.redirect(req.href.admin(cat, page))
                 elif req.args.get('cancel'):
                     req.redirect(req.href.admin(cat, page))
@@ -279,6 +292,8 @@
                             ver.time = parse_date(req.args.get('time'),
                                                   req.tz)
                         ver.insert()
+                        add_notice(req, _("The version '%(name)s' has been "
+                                          "added.", name=name))
                         req.redirect(req.href.admin(cat, page))
                     else:
                         raise TracError(_('Version %s already exists.') % name)
@@ -295,6 +310,8 @@
                         ver = model.Version(self.env, name, db=db)
                         ver.delete(db=db)
                     db.commit()
+                    add_notice(req, _("The selected versions have been "
+                                      "removed."))
                     req.redirect(req.href.admin(cat, page))
 
                 # Set default version
@@ -304,6 +321,7 @@
                         self.log.info('Setting default version to %s', name)
                         self.config.set('ticket', 'default_version', name)
                         self.config.save()
+                        add_notice(req, _("Your changes have been saved."))
                         req.redirect(req.href.admin(cat, page))
 
             data = {
@@ -339,6 +357,7 @@
                 if req.args.get('save'):
                     enum.name = req.args.get('name')
                     enum.update()
+                    add_notice(req, _("Your changes have been saved."))
                     req.redirect(req.href.admin(cat, page))
                 elif req.args.get('cancel'):
                     req.redirect(req.href.admin(cat, page))
@@ -357,6 +376,10 @@
                         enum = self._enum_cls(self.env)
                         enum.name = name
                         enum.insert()
+                        add_notice(req, _("The %(field)s '%(name)s' has been "
+                                          "added.",
+                                          field=self._label[0].lower(),
+                                          name=name))
                         req.redirect(req.href.admin(cat, page))
                     else:
                         raise TracError(_('%s %s already exists') % (self._type.title(), name))
@@ -373,6 +396,9 @@
                         enum = self._enum_cls(self.env, name, db=db)
                         enum.delete(db=db)
                     db.commit()
+                    add_notice(req, _("The selected %(fields)s have been "
+                                      "removed.",
+                                      fields=self._label[1].lower()))
                     req.redirect(req.href.admin(cat, page))
 
                 # Appy changes
@@ -403,6 +429,7 @@
                             enum.update(db=db)
                     db.commit()
 
+                    add_notice(req, _("Your changes have been saved."))
                     req.redirect(req.href.admin(cat, page))
 
             data.update(dict(enums=list(self._enum_cls.select(self.env)),
diff --git a/trac/ticket/report.py b/trac/ticket/report.py
--- a/trac/ticket/report.py
+++ b/trac/ticket/report.py
@@ -34,8 +34,8 @@
 from trac.util.text import to_unicode, unicode_urlencode
 from trac.util.translation import _
 from trac.web.api import IRequestHandler, RequestDone
-from trac.web.chrome import add_ctxtnav, add_link, add_stylesheet, \
-                            INavigationContributor, Chrome
+from trac.web.chrome import add_ctxtnav, add_link, add_notice, \
+                            add_stylesheet, INavigationContributor, Chrome
 from trac.wiki import IWikiSyntaxProvider, WikiParser
 
 
@@ -152,6 +152,7 @@
         cursor = db.cursor()
         cursor.execute("DELETE FROM report WHERE id=%s", (id,))
         db.commit()
+        add_notice(req, _("The report has been deleted."))
         req.redirect(req.href.report())
 
     def _do_save(self, req, db, id):
@@ -166,6 +167,7 @@
             cursor.execute("UPDATE report SET title=%s,query=%s,description=%s "
                            "WHERE id=%s", (title, query, description, id))
             db.commit()
+            add_notice(req, _("Your changes have been saved."))
         req.redirect(req.href.report(id))
 
     def _render_confirm_delete(self, req, db, id):
diff --git a/trac/ticket/roadmap.py b/trac/ticket/roadmap.py
--- a/trac/ticket/roadmap.py
+++ b/trac/ticket/roadmap.py
@@ -39,11 +39,12 @@
 from trac.ticket.query import Query
 from trac.timeline.api import ITimelineEventProvider
 from trac.web import IRequestHandler
-from trac.web.chrome import add_link, add_stylesheet, add_warning, \
-                            INavigationContributor
+from trac.web.chrome import add_link, add_notice, add_stylesheet, \
+                            add_warning, INavigationContributor
 from trac.wiki.api import IWikiSyntaxProvider
 from trac.wiki.formatter import format_to
 
+
 class ITicketGroupStatsProvider(Interface):
     def get_ticket_group_stats(ticket_ids):
         """ Gather statistics on a group of tickets.
@@ -585,6 +586,7 @@
             retarget_to = req.args.get('target') or None
         milestone.delete(retarget_to, req.authname)
         db.commit()
+        add_notice(req, _("The milestone has been deleted."))
         req.redirect(req.href.roadmap())
 
     def _do_save(self, req, db, milestone):
@@ -655,6 +657,7 @@
             milestone.insert()
         db.commit()
 
+        add_notice(req, _("Your changes have been saved."))
         req.redirect(req.href.milestone(milestone.name))
 
     def _render_confirm(self, req, db, milestone):
diff --git a/trac/web/api.py b/trac/web/api.py
--- a/trac/web/api.py
+++ b/trac/web/api.py
@@ -180,6 +180,7 @@
             'incookie': Request._parse_cookies,
             '_inheaders': Request._parse_headers
         }
+        self.redirect_handlers = []
 
         self.base_url = self.environ.get('trac.base_url')
         if not self.base_url:
@@ -223,6 +224,13 @@
     server_port = property(fget=lambda self: int(self.environ['SERVER_PORT']),
                            doc='Port number the server is bound to')
 
+    def add_redirect_handler(self, handler):
+        """Add a callable to be called prior to executing a redirect.
+        
+        The callable is passed the arguments to the `redirect()` call.
+        """
+        self.redirect_handlers.append(handler)
+
     def get_header(self, name):
         """Return the value of the specified HTTP header, or `None` if there's
         no such header in the request.
@@ -290,6 +298,9 @@
         `url` may be relative or absolute, relative URLs will be translated
         appropriately.
         """
+        for handler in self.redirect_handlers:
+            handler(self, url, permanent)
+        
         if self.session:
             self.session.save() # has to be done before the redirect is sent
 
diff --git a/trac/web/chrome.py b/trac/web/chrome.py
--- a/trac/web/chrome.py
+++ b/trac/web/chrome.py
@@ -15,6 +15,7 @@
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
 import datetime
+import itertools
 import os.path
 import pkg_resources
 import pprint
@@ -436,6 +437,13 @@
 
         chrome = {'links': {}, 'scripts': [], 'ctxtnav': [], 'warnings': [],
                   'notices': []}
+        def on_redirect(req, url, permanent):
+            """Save warnings and notices in case of redirect, so that they can
+            be displayed after the redirect."""
+            for type_ in ['warnings', 'notices']:
+                for (i, message) in enumerate(req.chrome[type_]):
+                    req.session['chrome.%s.%d' % (type_, i)] = message
+        req.add_redirect_handler(on_redirect)
 
         # This is ugly... we can't pass the real Request object to the
         # add_xxx methods, because it doesn't yet have the chrome attribute
@@ -707,6 +715,16 @@
         method = {'text/html': 'xhtml',
                   'text/plain': 'text'}.get(content_type, 'xml')
 
+        if method == "xhtml":
+            # Retrieve post-redirect messages saved in session
+            for type_ in ['warnings', 'notices']:
+                try:
+                    for i in itertools.count():
+                        req.chrome[type_].append(
+                            req.session.pop('chrome.%s.%d' % (type_, i)))
+                except KeyError:
+                    pass
+
         template = self.load_template(filename, method=method)
         data = self.populate_data(req, data)
 
diff --git a/trac/web/tests/chrome.py b/trac/web/tests/chrome.py
--- a/trac/web/tests/chrome.py
+++ b/trac/web/tests/chrome.py
@@ -56,13 +56,15 @@
 
     def test_htdocs_location(self):
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='')
+                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='',
+                   add_redirect_handler=lambda handler: None)
         info = Chrome(self.env).prepare_request(req)
         self.assertEqual('/trac.cgi/chrome/common/', info['htdocs_location'])
 
     def test_logo(self):
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='')
+                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='',
+                   add_redirect_handler=lambda handler: None)
 
         # Verify that no logo data is put in the HDF if no logo is configured
         self.env.config.set('header_logo', 'src', '')
@@ -99,7 +101,8 @@
 
     def test_default_links(self):
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='')
+                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='',
+                   add_redirect_handler=lambda handler: None)
         links = Chrome(self.env).prepare_request(req)['links']
         self.assertEqual('/trac.cgi/wiki', links['start'][0]['href'])
         self.assertEqual('/trac.cgi/search', links['search'][0]['href'])
@@ -109,7 +112,8 @@
 
     def test_icon_links(self):
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='')
+                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='',
+                   add_redirect_handler=lambda handler: None)
         chrome = Chrome(self.env)
 
         # No icon set in config, so no icon links
@@ -148,7 +152,8 @@
             def get_navigation_items(self, req):
                 yield 'metanav', 'test', 'Test'
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), path_info='/', base_path='/trac.cgi')
+                   href=Href('/trac.cgi'), path_info='/', base_path='/trac.cgi',
+                   add_redirect_handler=lambda handler: None)
         nav = Chrome(self.env).prepare_request(req)['nav']
         self.assertEqual({'name': 'test', 'label': 'Test', 'active': False},
                          nav['metanav'][0])
@@ -161,7 +166,8 @@
             def get_navigation_items(self, req):
                 yield 'metanav', 'test', 'Test'
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), path_info='/', base_path='/trac.cgi')
+                   href=Href('/trac.cgi'), path_info='/', base_path='/trac.cgi',
+                   add_redirect_handler=lambda handler: None)
         handler = TestNavigationContributor(self.env)
         nav = Chrome(self.env).prepare_request(req, handler)['nav']
         self.assertEqual({'name': 'test', 'label': 'Test', 'active': True},
@@ -181,7 +187,8 @@
             def get_navigation_items(self, req):
                 yield 'metanav', 'test2', 'Test 2'
         req = Mock(chrome={}, abs_href=Href('http://example.org/trac.cgi'),
-                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='/')
+                   href=Href('/trac.cgi'), base_path='/trac.cgi', path_info='/',
+                   add_redirect_handler=lambda handler: None)
         chrome = Chrome(self.env)
 
         # Test with both items set in the order option
diff --git a/trac/wiki/web_ui.py b/trac/wiki/web_ui.py
--- a/trac/wiki/web_ui.py
+++ b/trac/wiki/web_ui.py
@@ -36,8 +36,8 @@
 from trac.util.text import shorten_line
 from trac.util.translation import _
 from trac.versioncontrol.diff import get_diff_options, diff_blocks
-from trac.web.chrome import add_link, add_script, add_stylesheet, \
-                            add_ctxtnav, add_warning, prevnext_nav, \
+from trac.web.chrome import add_ctxtnav, add_link, add_notice, add_script, \
+                            add_stylesheet, add_warning, prevnext_nav, \
                             INavigationContributor, ITemplateProvider
 from trac.web import IRequestHandler
 from trac.wiki.api import IWikiPageManipulator, WikiSystem
@@ -254,8 +254,18 @@
         db.commit()
 
         if not page.exists:
+            add_notice(req, _("The page '%(name)s' has been deleted.",
+                              name=page.name))
             req.redirect(req.href.wiki())
         else:
+            if version and old_version and version > old_version:
+                add_notice(req, _("The versions %(from_)d to %(to)d of the "
+                                  "page '%(name)s' have been deleted.",
+                                  from_=old_version + 1, to=version))
+            else:
+                add_notice(req, _("The version %(version)d of the page "
+                                  "'%(name)s' has been deleted.",
+                                  version=version, name=page.name))
             req.redirect(req.href.wiki(page.name))
 
     def _do_save(self, req, page):
@@ -276,6 +286,7 @@
             page.save(get_reporter_id(req, 'author'),
                             req.args.get('comment'),
                             req.remote_addr)
+            add_notice(req, _("Your changes have been saved."))
             req.redirect(get_resource_url(self.env, page.resource, req.href,
                                           version=None))
         except TracError:

