diff --git a/reviewboard/admin/urls.py b/reviewboard/admin/urls.py
index 8e956b95cda674bd5caa7808f4c26d63532d08f0..e3a865c0e0f667209633c47dc54958c2e52c6b33 100644
--- a/reviewboard/admin/urls.py
+++ b/reviewboard/admin/urls.py
@@ -36,6 +36,7 @@ from reviewboard.admin import forms, views
 
 NEWS_FEED = 'https://www.reviewboard.org/news/feed/'
 
+
 urlpatterns = [
     url(r'^$', views.dashboard, name='admin-dashboard'),
 
diff --git a/reviewboard/static/rb/css/common.less b/reviewboard/static/rb/css/common.less
index d9fab7776a7f3940c66b270b47a65b3c1ddef892..b9d224cbfb2c6ff452cc09cfffe4aea214533c3f 100644
--- a/reviewboard/static/rb/css/common.less
+++ b/reviewboard/static/rb/css/common.less
@@ -510,4 +510,11 @@ html[xmlns] .clearfix {
   height: 1%;
 }
 
+/**
+ * Disables interaction with an element and any of it's children
+ */
+.rb-u-disabled-container {
+  pointer-events: none;
+}
+
 // vim: set et ts=2 sw=2:
diff --git a/reviewboard/static/rb/js/views/abstractReviewableView.es6.js b/reviewboard/static/rb/js/views/abstractReviewableView.es6.js
index f83d63a48df30b914db7f8d284948c7c597a67d4..d7f9ca8e46737587a5e16afad6b59a4b272fe8ac 100644
--- a/reviewboard/static/rb/js/views/abstractReviewableView.es6.js
+++ b/reviewboard/static/rb/js/views/abstractReviewableView.es6.js
@@ -33,6 +33,7 @@ RB.AbstractReviewableView = Backbone.View.extend({
         this.commentDlg = null;
         this._activeCommentBlock = null;
         this.renderedInline = options.renderedInline || false;
+        this.commentBlockViews = [];
     },
 
     /**
@@ -47,11 +48,16 @@ RB.AbstractReviewableView = Backbone.View.extend({
      */
     render() {
         this.renderContent();
+        this.renderCommentBlocks();
+        return this;
+    },
 
+    /**
+     * Renders all comment blocks into the view.
+     */
+    renderCommentBlocks() {
         this.model.commentBlocks.each(this._addCommentBlockView, this);
         this.model.commentBlocks.on('add', this._addCommentBlockView, this);
-
-        return this;
     },
 
     /**
@@ -127,6 +133,28 @@ RB.AbstractReviewableView = Backbone.View.extend({
         });
     },
 
+    /**
+     * Removes all comments from the view, and re-attaches them
+     */
+    refreshComments() {
+        this._hideCommentDlg();
+        this._disposeComments();
+        this.renderCommentBlocks();
+    },
+
+    _hideCommentDlg() {
+        if (this.commentDlg) {
+            this.commentDlg.close();
+        }
+    },
+
+    _disposeComments() {
+        this.commentBlockViews.forEach((view) => {
+            view.dispose();
+        });
+        this.commentBlockViews = [];
+    },
+
     /**
      * Add a CommentBlockView for the given CommentBlock.
      *
@@ -145,6 +173,7 @@ RB.AbstractReviewableView = Backbone.View.extend({
 
         commentBlockView.on('clicked', () => this.showCommentDlg(commentBlockView));
         commentBlockView.render();
+        this.commentBlockViews.push(commentBlockView);
         this.trigger('commentBlockViewAdded', commentBlockView);
     },
 });
diff --git a/reviewboard/static/rb/js/views/tests/textBasedReviewableViewTests.es6.js b/reviewboard/static/rb/js/views/tests/textBasedReviewableViewTests.es6.js
index 33324fd6b4c8deda983d9a5412d0f33c06fe6989..c212fe81f6afbad2a8d525b69a79ad917ed41f23 100644
--- a/reviewboard/static/rb/js/views/tests/textBasedReviewableViewTests.es6.js
+++ b/reviewboard/static/rb/js/views/tests/textBasedReviewableViewTests.es6.js
@@ -11,9 +11,21 @@ suite('rb/views/TextBasedReviewableView', function() {
        </div>
        <table class="text-review-ui-rendered-table"></table>
        <table class="text-review-ui-text-table"></table>
+       <div class="render-options"></div>
       </div>
     `;
 
+    function getMostRecentApiCallOptions() {
+        return RB.apiCall.calls.mostRecent().args[0];
+    }
+
+    function spyAndForceAjaxSuccess(responseBody = '') {
+        spyOn($, 'ajax').and.callFake(request => {
+            request.success(responseBody);
+            request.complete();
+        });
+    }
+
     let $container;
     let reviewRequest;
     let model;
@@ -57,6 +69,8 @@ suite('rb/views/TextBasedReviewableView', function() {
             }
         });
 
+        spyOn(RB, 'apiCall').and.callThrough();
+
         view.render();
     });
 
@@ -83,4 +97,70 @@ suite('rb/views/TextBasedReviewableView', function() {
         expect($container.find('.active').attr('data-view-mode')).toBe('rendered');
         expect(model.get('viewMode')).toBe('rendered');
     });
+
+    it('reloadContentFromServer disables render options during the request',
+        function() {
+            const $renderOptions = $('.render-options');
+
+            view.reloadContentFromServer(
+                view.CONTENT_TYPE_RENDERED_TEXT, {}, view._$renderedTable);
+
+            expect($renderOptions.hasClass('rb-u-disabled-container')).toEqual(true);
+        }
+    );
+
+    it('reloadContentFromServer re-enables render options after the request',
+        function() {
+            spyAndForceAjaxSuccess();
+            const $renderOptions = $('.render-options');
+
+            view.reloadContentFromServer(
+                view.CONTENT_TYPE_RENDERED_TEXT, {}, view._$renderedTable);
+
+            expect($renderOptions.hasClass('rb-u-disabled-container')).toEqual(false);
+        }
+    );
+
+    it('reloadContentFromServer properly combines extra render option data',
+        function() {
+            view.reloadContentFromServer(
+                view.CONTENT_TYPE_RENDERED_TEXT, {
+                    sortKeys: false
+                }, view._$renderedTable);
+
+            const options = getMostRecentApiCallOptions();
+            expect(options.data).toEqual({
+                type: 'rendered',
+                sortKeys: false
+            });
+        }
+    );
+
+    it('reloadContentFromServer should emit contentReloaded on success',
+        function() {
+            spyAndForceAjaxSuccess();
+
+            let contentReloaded = false;
+            view.on('contentReloaded', () => {
+                contentReloaded = true;
+            });
+
+            view.reloadContentFromServer(
+                view.CONTENT_TYPE_RENDERED_TEXT, {}, view._$renderedTable);
+
+            expect(contentReloaded).toEqual(true);
+        }
+    );
+
+    it('reloadContentFromServer should update the contents of the element',
+        function() {
+            const contents = '<div>new table contents</div>';
+            spyAndForceAjaxSuccess(contents);
+
+            view.reloadContentFromServer(
+                view.CONTENT_TYPE_RENDERED_TEXT, {}, view._$renderedTable);
+
+            expect(view._$renderedTable.html()).toEqual(contents);
+        }
+    );
 });
diff --git a/reviewboard/static/rb/js/views/textBasedReviewableView.es6.js b/reviewboard/static/rb/js/views/textBasedReviewableView.es6.js
index 2efaa3b4d5ab3c2ff91c5862523cbe63b0a63d2a..586386a47c8bb32866f6b12f737437c5f7344ea2 100644
--- a/reviewboard/static/rb/js/views/textBasedReviewableView.es6.js
+++ b/reviewboard/static/rb/js/views/textBasedReviewableView.es6.js
@@ -54,6 +54,9 @@ RB.TextBasedReviewableView = RB.FileAttachmentReviewableView.extend({
                 this._scrollToLine(lineNum);
             }
         });
+
+        this.CONTENT_TYPE_RENDERED_TEXT = 'rendered';
+        this.CONTENT_TYPE_SOURCE_TEXT = 'source';
     },
 
     /**
@@ -66,6 +69,59 @@ RB.TextBasedReviewableView = RB.FileAttachmentReviewableView.extend({
         this._renderedSelector.remove();
     },
 
+    /**
+     * Gets the endpoint to hit to reload content for the file
+     * from the server.
+     *
+     * Returns:
+     *     String:
+     *     The endpoint to hit.
+     */
+    getReloadContentEndpoint() {
+        // TODO: change this to the actual endpoint
+        return '/admin/testdynamic/';
+    },
+
+    /**
+     * Updates the specified element with by reloading it's text content
+     * from the server.
+     *
+     * Args:
+     *     renderType (string):
+     *         The type of the content that should be reloaded.
+     *     options (object):
+     *         Extra details to pass to the endpoint as query parameters.
+     *     $elementToUpdate (jQuery):
+     *         The DOM element to update.
+     */
+    reloadContentFromServer(renderType, options, $elementToUpdate) {
+        const $renderOptions = $('.render-options');
+        $renderOptions.addClass('rb-u-disabled-container');
+
+        $elementToUpdate.html('');
+
+        RB.apiCall({
+            url: this.getReloadContentEndpoint(),
+            type: 'GET',
+            data: $.extend(true, options, {
+                type: renderType
+            }),
+            dataType: 'html',
+            success: (response) => {
+                $elementToUpdate.html(response);
+
+                this.trigger('contentReloaded', {
+                    renderType,
+                    options,
+                    $el: $elementToUpdate
+                });
+            },
+            complete: () => {
+                $renderOptions.removeClass('rb-u-disabled-container');
+            }
+        });
+    },
+
     /**
      * Render the view.
      */
diff --git a/reviewboard/static/rb/js/views/textCommentRowSelector.es6.js b/reviewboard/static/rb/js/views/textCommentRowSelector.es6.js
index eca8deaa855f1f365a0b695dbdd517823ec479c6..a2b313e468435134d37ebcaa71caf9d614e5ec8d 100644
--- a/reviewboard/static/rb/js/views/textCommentRowSelector.es6.js
+++ b/reviewboard/static/rb/js/views/textCommentRowSelector.es6.js
@@ -60,6 +60,13 @@ RB.TextCommentRowSelector = Backbone.View.extend({
 
         this._$ghostCommentFlag = null;
         this._$ghostCommentFlagCell = null;
+
+        options.reviewableView.on('contentReloaded', (context) => {
+            if (context.$el === options.el) {
+                this._reset();
+                options.reviewableView.refreshComments();
+            }
+        });
     },
 
     /**
