 core/modules/ckeditor/js/ckeditor.admin.js         |    8 +-
 core/modules/contextual/js/contextual.js           |    8 +-
 core/modules/contextual/js/contextual.toolbar.js   |  118 +++++++++++++-------
 core/modules/edit/edit.module                      |    1 +
 core/modules/edit/js/edit.js                       |   18 ++-
 core/modules/edit/js/editors/formEditor.js         |    4 +-
 core/modules/edit/js/models/BaseModel.js           |   41 +++++++
 core/modules/edit/js/models/EntityModel.js         |  109 ++++++++++--------
 core/modules/edit/js/models/FieldModel.js          |   12 +-
 core/modules/edit/js/views/AppView.js              |   85 ++++++++------
 core/modules/edit/js/views/ContextualLinkView.js   |    4 +-
 core/modules/edit/js/views/EditorView.js           |    3 +-
 core/modules/edit/js/views/EntityDecorationView.js |    2 +-
 core/modules/edit/js/views/EntityToolbarView.js    |   18 ++-
 core/modules/edit/js/views/FieldDecorationView.js  |    4 +-
 core/modules/edit/js/views/FieldToolbarView.js     |    2 +-
 core/modules/system/system.module                  |    4 +-
 core/modules/toolbar/js/toolbar.js                 |   14 +--
 core/modules/tour/js/tour.js                       |    4 +-
 19 files changed, 289 insertions(+), 170 deletions(-)

diff --git a/core/modules/ckeditor/js/ckeditor.admin.js b/core/modules/ckeditor/js/ckeditor.admin.js
index ccf2f71..49ca229 100644
--- a/core/modules/ckeditor/js/ckeditor.admin.js
+++ b/core/modules/ckeditor/js/ckeditor.admin.js
@@ -120,8 +120,8 @@ Drupal.ckeditor = {
       this.getCKEditorFeatures(this.model.get('hiddenEditorConfig'), this.disableFeaturesDisallowedByFilters.bind(this));
 
       // Push the active editor configuration to the textarea.
-      this.model.on('change:activeEditorConfig', this.model.sync, this.model);
-      this.model.on('change:isDirty', this.parseEditorDOM, this);
+      this.model.listenTo(this.model, 'change:activeEditorConfig', this.model.sync, this.model);
+      this.listenTo(this.model, 'change:isDirty', this.parseEditorDOM, this);
     },
 
     /**
@@ -462,7 +462,7 @@ Drupal.ckeditor = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change:isDirty change:groupNamesVisible', this.render, this);
+      this.listenTo(this.model, 'change:isDirty change:groupNamesVisible', this.render);
 
       // Add a toggle for the button group names.
       $(Drupal.theme('ckeditorButtonGroupNamesToggle'))
@@ -959,7 +959,7 @@ Drupal.ckeditor = {
     initialize: function () {
       // Announce the button and group positions when the model is no longer
       // dirty.
-      this.model.on('change:isDirty', this.announceMove, this);
+      this.listenTo(this.model, 'change:isDirty', this.announceMove);
     },
 
     /**
diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js
index ab404a2..4616411 100644
--- a/core/modules/contextual/js/contextual.js
+++ b/core/modules/contextual/js/contextual.js
@@ -269,7 +269,7 @@ Drupal.contextual = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change', this.render, this);
+      this.listenTo(this.model, 'change', this.render);
     },
 
     /**
@@ -308,7 +308,9 @@ Drupal.contextual = {
      * {@inheritdoc}
      */
     initialize: function (options) {
-      this.model.on('change', this.render, this);
+      this.options = options;
+
+      this.listenTo(this.model, 'change', this.render);
 
       // Use aria-role form so that the number of items in the list is spoken.
       this.$el.attr('role', 'form');
@@ -399,7 +401,7 @@ Drupal.contextual = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change:hasFocus', this.render, this);
+      this.listenTo(this.model, 'change:hasFocus', this.render);
     },
 
     /**
diff --git a/core/modules/contextual/js/contextual.toolbar.js b/core/modules/contextual/js/contextual.toolbar.js
index e1a01dd..0c6a8b8 100644
--- a/core/modules/contextual/js/contextual.toolbar.js
+++ b/core/modules/contextual/js/contextual.toolbar.js
@@ -20,8 +20,19 @@ var strings = {
  *   A contextual links DOM element as rendered by the server.
  */
 function initContextualToolbar (context) {
+  if (!Drupal.contextual || !Drupal.contextual.collection) {
+    return;
+  }
+
   var contextualToolbar = Drupal.contextualToolbar;
-  var model = contextualToolbar.model = new contextualToolbar.Model();
+  var model = contextualToolbar.model = new contextualToolbar.Model({
+    // Checks whether localStorage indicates we should start in edit mode
+    // rather than view mode.
+    // @see Drupal.contextualToolbar.VisualView.persist()
+    isViewing: localStorage.getItem('Drupal.contextualToolbar.isViewing') !== 'false'
+  }, {
+    contextualCollection: Drupal.contextual.collection,
+  });
 
   var viewOptions = {
     el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
@@ -30,36 +41,6 @@ function initContextualToolbar (context) {
   };
   new contextualToolbar.VisualView(viewOptions);
   new contextualToolbar.AuralView(viewOptions);
-
-  // Show the edit tab while there's >=1 contextual link.
-  if (Drupal.contextual && Drupal.contextual.collection) {
-    var contextualCollection = Drupal.contextual.collection;
-    var trackContextualCount = function () {
-      model.set('contextualCount', contextualCollection.length);
-    };
-    contextualCollection.on('reset remove add', trackContextualCount);
-    trackContextualCount();
-
-    // Whenever edit mode is toggled, lock all contextual links.
-    model.on('change:isViewing', function() {
-      contextualCollection.each(function (contextualModel) {
-        contextualModel.set('isLocked', !model.get('isViewing'));
-      });
-    });
-    // When a new contextual link is added and edit mode is enabled, lock it.
-    contextualCollection.on('add', function (contextualModel) {
-      if (!model.get('isViewing')) {
-        contextualModel.set('isLocked', true);
-      }
-    });
-  }
-
-  // Checks whether localStorage indicates we should start in edit mode
-  // rather than view mode.
-  // @see Drupal.contextualToolbar.VisualView.persist()
-  if (localStorage.getItem('Drupal.contextualToolbar.isViewing') === 'false') {
-    model.set('isViewing', false);
-  }
 }
 
 /**
@@ -96,10 +77,67 @@ Drupal.contextualToolbar = {
       // of tabbable elements when edit mode is enabled.
       tabbingContext: null
     },
-    initialize: function () {
-      this.on('change:contextualCount', function (model) {
-        model.set('isVisible', model.get('contextualCount') > 0);
+
+    /**
+     * {@inheritdoc}
+     *
+     * @param Object attrs
+     * @param Object options
+     *   An object with the following option:
+     *     - Backbone.collection contextualCollection: the collection of
+     *       Drupal.contextual.Model models that represent the contextual links
+     *       on the page.
+     */
+    initialize: function (attrs, options) {
+      // Respond to new/removed contextual links.
+      this.listenTo(options.contextualCollection, {
+        'reset remove add': this.countCountextualLinks,
+        'add': this.lockNewContextualLinks
+      });
+
+      this.listenTo(this, {
+        // Automatically determine visibility.
+        'change:contextualCount': this.updateVisibility,
+        // Whenever edit mode is toggled, lock all contextual links.
+        'change:isViewing': function (model, isViewing) {
+          options.contextualCollection.each(function (contextualModel) {
+            contextualModel.set('isLocked', !isViewing);
+          });
+        }
       });
+    },
+
+    /**
+     * Tracks the number of contextual link models in the collection.
+     *
+     * @param Drupal.contextual.Model affectedModel
+     *   The contextual links model that was added or removed.
+     * @param Backbone.Collection contextualCollection
+     *    The collection of contextual link models.
+     */
+    countCountextualLinks: function (contextualModel, contextualCollection) {
+      this.set('contextualCount', contextualCollection.length);
+    },
+
+    /**
+     * Lock newly added contextual links if edit mode is enabled.
+     *
+     * @param Drupal.contextual.Model addedContextualModel
+     *   The contextual links model that was added.
+     * @param Backbone.Collection contextualCollection
+     *    The collection of contextual link models.
+     */
+    lockNewContextualLinks: function (contextualModel, contextualCollection) {
+      if (!this.get('isViewing')) {
+        contextualModel.set('isLocked', true);
+      }
+    },
+
+    /**
+     * Automatically updates visibility of the view/edit mode toggle.
+     */
+    updateVisibility: function () {
+      this.set('isVisible', this.get('contextualCount') > 0);
     }
   }),
 
@@ -128,8 +166,8 @@ Drupal.contextualToolbar = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change', this.render, this);
-      this.model.on('change:isViewing', this.persist, this);
+      this.listenTo(this.model, 'change', this.render);
+      this.listenTo(this.model, 'change:isViewing', this.persist);
     },
 
     /**
@@ -175,9 +213,11 @@ Drupal.contextualToolbar = {
     /*
      * {@inheritdoc}
      */
-    initialize: function () {
-      this.model.on('change', this.render, this);
-      this.model.on('change:isViewing', this.manageTabbing, this);
+    initialize: function (options) {
+      this.options = options;
+
+      this.listenTo(this.model, 'change', this.render);
+      this.listenTo(this.model, 'change:isViewing', this.manageTabbing);
 
       $(document).on('keyup', _.bind(this.onKeypress, this));
     },
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index bc7b314..6ff17e0 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -55,6 +55,7 @@ function edit_library_info() {
       $path . '/js/edit.js' => $options,
       $path . '/js/util.js' => $options,
       // Models.
+      $path . '/js/models/BaseModel.js' => $options,
       $path . '/js/models/AppModel.js' => $options,
       $path . '/js/models/EntityModel.js' => $options,
       $path . '/js/models/FieldModel.js' => $options,
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index 429e7b2..fe36fd1 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -275,7 +275,7 @@ function processField (fieldElement) {
 
   // If an EntityModel for this field already exists (and hence also a "Quick
   // edit" contextual link), then initialize it immediately.
-  if (Drupal.edit.collections.entities.where({ entityID: entityID, entityInstanceID: entityInstanceID }).length > 0) {
+  if (Drupal.edit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID })) {
     initializeField(fieldElement, fieldID, entityID, entityInstanceID);
   }
   // Otherwise: queue the field. It is now available to be set up when its
@@ -298,10 +298,10 @@ function processField (fieldElement) {
  *   The field's entity's instance ID.
  */
 function initializeField (fieldElement, fieldID, entityID, entityInstanceID) {
-  var entity = Drupal.edit.collections.entities.where({
+  var entity = Drupal.edit.collections.entities.findWhere({
     entityID: entityID,
     entityInstanceID: entityInstanceID
-  })[0];
+  });
 
   $(fieldElement).addClass('edit-field');
 
@@ -528,17 +528,15 @@ function initializeEntityContextualLink (contextualLink) {
 function deleteContainedModelsAndQueues($context) {
   $context.find('[data-edit-entity-id]').addBack('[data-edit-entity-id]').each(function (index, entityElement) {
     // Delete entity model.
-    // @todo change to findWhere() as soon as we have Backbone 1.0 in Drupal
-    // core. @see https://drupal.org/node/1800022
-    var entityModels = Drupal.edit.collections.entities.where({el: entityElement});
-    if (entityModels.length) {
-      var contextualLinkView = entityModels[0].get('contextualLinkView');
+    var entityModel = Drupal.edit.collections.entities.findWhere({el: entityElement});
+    if (entityModel) {
+      var contextualLinkView = entityModel.get('contextualLinkView');
       contextualLinkView.undelegateEvents();
       contextualLinkView.remove();
       // Remove the EntityDecorationView.
-      entityModels[0].get('entityDecorationView').remove();
+      entityModel.get('entityDecorationView').remove();
       // Destroy the EntityModel; this will also destroy its FieldModels.
-      entityModels[0].destroy();
+      entityModel.destroy();
     }
 
     // Filter queue.
diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js
index 2d8fb8e..7eda4e1 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -183,7 +183,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
       // Finally, set the 'html' attribute on the field model. This will cause
       // the field to be rerendered.
-      fieldModel.set('html', response.data);
+      _.defer(function () {
+        fieldModel.set('html', response.data);
+      });
     };
 
     // Unsuccessfully saved; validation errors.
diff --git a/core/modules/edit/js/models/BaseModel.js b/core/modules/edit/js/models/BaseModel.js
new file mode 100644
index 0000000..d7870b6
--- /dev/null
+++ b/core/modules/edit/js/models/BaseModel.js
@@ -0,0 +1,41 @@
+/**
+ * @file
+ * A Backbone Model subclass that enforces validation when calling set().
+ */
+
+(function (Backbone) {
+
+"use strict";
+
+Drupal.edit.BaseModel = Backbone.Model.extend({
+
+  /**
+   * {@inheritdoc}
+   */
+  initialize: function (options) {
+    this.__initialized = true;
+    return Backbone.Model.prototype.initialize.call(this, options);
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  set: function (key, val, options) {
+    if (this.__initialized) {
+      // Deal with both the "key", value and {key:value}-style arguments.
+      if (typeof key === 'object') {
+        key.validate = true;
+      }
+      else {
+        if (!options) {
+          options = {};
+        }
+        options.validate = true;
+      }
+    }
+    return Backbone.Model.prototype.set.call(this, key, val, options);
+  }
+
+});
+
+}(Backbone));
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index 913abb3..2fecf80 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -7,7 +7,7 @@
 
 "use strict";
 
-Drupal.edit.EntityModel = Backbone.Model.extend({
+Drupal.edit.EntityModel = Drupal.edit.BaseModel.extend({
 
   defaults: {
     // The DOM element that represents this entity. It may seem bizarre to
@@ -62,11 +62,14 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     this.set('fields', new Drupal.edit.FieldCollection());
 
     // Respond to entity state changes.
-    this.on('change:state', this.stateChange, this);
+    this.listenTo(this, 'change:state', this.stateChange, this);
 
     // The state of the entity is largely dependent on the state of its
     // fields.
-    this.get('fields').on('change:state', this.fieldStateChange, this);
+    this.listenTo(this.get('fields'), 'change:state', this.fieldStateChange);
+
+    // Call Drupal.edit.BaseModel's initialize() method.
+    Drupal.edit.BaseModel.prototype.initialize.call(this);
   },
 
   /**
@@ -81,9 +84,11 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     var to = state;
     switch (to) {
       case 'closed':
-        this.set('isActive', false);
-        this.set('inTempStore', false);
-        this.set('isDirty', false);
+        this.set({
+          'isActive': false,
+          'inTempStore': false,
+          'isDirty': false
+        });
         break;
 
       case 'launching':
@@ -103,18 +108,9 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
 
       case 'committing':
         // The user indicated they want to save the entity.
-        // For fields already in a candidate-ish state, trigger a change
-        // event so that the entityModel can move to the next state in
-        // committing.
-        this.get('fields').chain()
-          .filter(function (fieldModel) {
-            return _.intersection([fieldModel.get('state')], Drupal.edit.app.readyFieldStates).length;
-          })
-          .each(function (fieldModel) {
-            fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options);
-          });
+        var fields = this.get('fields');
         // For fields that are in an active state, transition them to candidate.
-        this.get('fields').chain()
+        fields.chain()
           .filter(function (fieldModel) {
             return _.intersection([fieldModel.get('state')], ['active']).length;
           })
@@ -123,7 +119,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
           });
         // For fields that are in a changed state, field values must first be
         // stored in TempStore.
-        this.get('fields').chain()
+        fields.chain()
           .filter(function (fieldModel) {
             return _.intersection([fieldModel.get('state')], Drupal.edit.app.changedFieldStates).length;
           })
@@ -193,17 +189,55 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
   },
 
   /**
+   * Updates a Field and Entity model's "inTempStore" when appropriate.
+   *
+   * Helper function.
+   *
+   * @param Drupal.edit.EntityModel entityModel
+   *   The model of the entity for which a field's state attribute has changed.
+   * @param Drupal.edit.FieldModel fieldModel
+   *   The model of the field whose state attribute has changed.
+   *
+   * @see fieldStateChange()
+   */
+  _updateInTempStoreAttributes: function (entityModel, fieldModel) {
+    var current = fieldModel.get('state');
+    var previous = fieldModel.previous('state');
+    var fieldsInTempStore = entityModel.get('fieldsInTempStore');
+    // If the fieldModel changed to the 'saved' state: remember that this
+    // field was saved to TempStore.
+    if (current === 'saved') {
+      // Mark the entity as saved in TempStore, so that we can pass the
+      // proper "reset TempStore" boolean value when communicating with the
+      // server.
+      entityModel.set('inTempStore', true);
+      // Mark the field as saved in TempStore, so that visual indicators
+      // signifying just that may be rendered.
+      fieldModel.set('inTempStore', true);
+      // Remember that this field is in TempStore, restore when rerendered.
+      fieldsInTempStore.push(fieldModel.get('fieldID'));
+      fieldsInTempStore = _.uniq(fieldsInTempStore);
+      entityModel.set('fieldsInTempStore', fieldsInTempStore);
+    }
+    // If the fieldModel changed to the 'candidate' state from the
+    // 'inactive' state, then this is a field for this entity that got
+    // rerendered. Restore its previous 'inTempStore' attribute value.
+    else if (current === 'candidate' && previous === 'inactive') {
+      fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+    }
+  },
+
+  /**
    * Reacts to state changes in this entity's fields.
    *
    * @param Drupal.edit.FieldModel fieldModel
-   *   The model of the field whose state property changed.
+   *   The model of the field whose state attribute changed.
    * @param String state
    *   The state of the associated field. One of Drupal.edit.FieldModel.states.
    */
   fieldStateChange: function (fieldModel, state) {
     var entityModel = this;
     var fieldState = state;
-    var fieldsInTempStore = this.get('fieldsInTempStore');
     // Switch on the entityModel state.
     // The EntityModel responds to FieldModel state changes as a function of its
     // state. For example, a field switching back to 'candidate' state when its
@@ -245,43 +279,22 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
         if (fieldState === 'changed') {
           entityModel.set('isDirty', true);
         }
-        // If the fieldModel changed to the 'saved' state: remember that this
-        // field was saved to TempStore.
-        else if (fieldState === 'saved') {
-          // Mark the entity as saved in TempStore, so that we can pass the
-          // proper "reset TempStore" boolean value when communicating with the
-          // server.
-          entityModel.set('inTempStore', true);
-          // Mark the field as saved in TempStore, so that visual indicators
-          // signifying just that may be rendered.
-          fieldModel.set('inTempStore', true);
-          // Remember that this field is in TempStore, restore when rerendered.
-          fieldsInTempStore.push(fieldModel.get('fieldID'));
-          fieldsInTempStore = _.uniq(fieldsInTempStore);
-          entityModel.set('fieldsInTempStore', fieldsInTempStore);
-        }
-        // If the fieldModel changed to the 'candidate' state from the
-        // 'inactive' state, then this is a field for this entity that got
-        // rerendered. Restore its previous 'inTempStore' attribute value.
-        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
-          fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+        else {
+          this._updateInTempStoreAttributes(entityModel, fieldModel);
         }
         break;
 
       case 'committing':
         // If the field save returned a validation error, set the state of the
-        // entity back to opened.
+        // entity back to 'opened'.
         if (fieldState === 'invalid') {
           // A state change in reaction to another state change must be deferred.
           _.defer(function() {
             entityModel.set('state', 'opened', { reason: 'invalid' });
           });
         }
-        // If the fieldModel changed to the 'candidate' state from the
-        // 'inactive' state, then this is a field for this entity that got
-        // rerendered. Restore its previous 'inTempStore' attribute value.
-        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
-          fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0);
+        else {
+          this._updateInTempStoreAttributes(entityModel, fieldModel);
         }
 
         // Attempt to save the entity. If the entity's fields are not yet all in
@@ -501,7 +514,9 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
    * @inheritdoc
    */
   destroy: function (options) {
-    Backbone.Model.prototype.destroy.apply(this, options);
+    Drupal.edit.BaseModel.prototype.destroy.call(this, options);
+
+    this.stopListening();
 
     // Destroy all fields of this entity.
     this.get('fields').each(function (fieldModel) {
diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js
index 13f88cc..ea537a0 100644
--- a/core/modules/edit/js/models/FieldModel.js
+++ b/core/modules/edit/js/models/FieldModel.js
@@ -10,7 +10,7 @@
 /**
  * State of an in-place editable field in the DOM.
  */
-Drupal.edit.FieldModel = Backbone.Model.extend({
+Drupal.edit.FieldModel = Drupal.edit.BaseModel.extend({
 
   defaults: {
     // The DOM element that represents this field. It may seem bizarre to have
@@ -73,6 +73,9 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
 
     // Automatically generate the logical field ID.
     this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/'));
+
+    // Call Drupal.edit.BaseModel's initialize() method.
+    Drupal.edit.BaseModel.prototype.initialize.call(this, options);
   },
 
   /**
@@ -82,7 +85,7 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
     if (this.get('state') !== 'inactive') {
       throw new Error("FieldModel cannot be destroyed if it is not inactive state.");
     }
-    Backbone.Model.prototype.destroy.apply(this, options);
+    Drupal.edit.BaseModel.prototype.destroy.call(this, options);
   },
 
   /**
@@ -97,11 +100,6 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
    * {@inheritdoc}
    */
   validate: function (attrs, options) {
-    // We only care about validating the 'state' attribute.
-    if (!_.has(attrs, 'state')) {
-      return;
-    }
-
     var current = this.get('state');
     var next = attrs.state;
     if (current !== next) {
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 2d85d2e..93caf5c 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -36,21 +36,21 @@ Drupal.edit.AppView = Backbone.View.extend({
     this.changedFieldStates = ['changed', 'saving', 'saved', 'invalid'];
     this.readyFieldStates = ['candidate', 'highlighted'];
 
-    options.entitiesCollection
+    this.listenTo(options.entitiesCollection, {
       // Track app state.
-      .on('change:state', this.appStateChange, this)
-      .on('change:isActive', this.enforceSingleActiveEntity, this);
+      'change:state': this.appStateChange,
+      'change:isActive': this.enforceSingleActiveEntity
+    });
 
-    options.fieldsCollection
-      // Track app state.
-      .on('change:state', this.editorStateChange, this)
-      // Respond to field model HTML representation change events.
-      .on('change:html', this.propagateUpdatedField, this)
-      .on('change:html', this.renderUpdatedField, this)
-      // Respond to addition.
-      .on('add', this.rerenderedFieldToCandidate, this)
-      // Respond to destruction.
-      .on('destroy', this.teardownEditor, this);
+    // Track app state.
+    this.listenTo(options.fieldsCollection, 'change:state', this.editorStateChange);
+    // Respond to field model HTML representation change events.
+    this.listenTo(options.fieldsCollection, 'change:html', this.renderUpdatedField);
+    this.listenTo(options.fieldsCollection, 'change:html', this.propagateUpdatedField);
+    // Respond to addition.
+    this.listenTo(options.fieldsCollection, 'add', this.rerenderedFieldToCandidate);
+    // Respond to destruction.
+    this.listenTo(options.fieldsCollection, 'destroy', this.teardownEditor);
   },
 
   /**
@@ -436,31 +436,48 @@ Drupal.edit.AppView = Backbone.View.extend({
     var $fieldWrapper = $(fieldModel.get('el'));
     var $context = $fieldWrapper.parent();
 
+    var renderField = function () {
+      // Destroy the field model; this will cause all attached views to be
+      // destroyed too, and removal from all collections in which it exists.
+      fieldModel.destroy();
+
+      // Replace the old content with the new content.
+      $fieldWrapper.replaceWith(html);
+
+      // Attach behaviors again to the modified piece of HTML; this will
+      // create a new field model and call rerenderedFieldToCandidate() with
+      // it.
+      Drupal.attachBehaviors($context);
+    };
+
     // When propagating the changes of another instance of this field, this
     // field is not being actively edited and hence no state changes are
     // necessary. So: only update the state of this field when the rerendering
-    // of this field happens not because of propagation, but because it is being
-    // edited itself.
+    // of this field happens not because of propagation, but because it is
+    // being edited itself.
     if (!options.propagation) {
-      // First set the state to 'candidate', to allow all attached views to
-      // clean up all their "active state"-related changes.
-      fieldModel.set('state', 'candidate');
+      // Deferred because renderUpdatedField is reacting to a field model change
+      // event, and we want to make sure that event fully propagates before
+      // making another change to the same model.
+      _.defer(function () {
+        // First set the state to 'candidate', to allow all attached views to
+        // clean up all their "active state"-related changes.
+        fieldModel.set('state', 'candidate');
+
+        // Similarly, the above .set() call's change event must fully propagate
+        // before calling it again.
+        _.defer(function () {
+          // Set the field's state to 'inactive', to enable the updating of its
+          // DOM value.
+          fieldModel.set('state', 'inactive', { reason: 'rerender' });
 
-      // Set the field's state to 'inactive', to enable the updating of its DOM
-      // value.
-      fieldModel.set('state', 'inactive', { reason: 'rerender' });
+          renderField();
+        });
+      });
+    }
+    else {
+      renderField();
     }
-
-    // Destroy the field model; this will cause all attached views to be
-    // destroyed too, and removal from all collections in which it exists.
-    fieldModel.destroy();
-
-    // Replace the old content with the new content.
-    $fieldWrapper.replaceWith(html);
-
-    // Attach behaviors again to the modified piece of HTML; this will create
-    // a new field model and call rerenderedFieldToCandidate() with it.
-    Drupal.attachBehaviors($context);
   },
 
   /**
@@ -523,10 +540,10 @@ Drupal.edit.AppView = Backbone.View.extend({
    *   A field that was just added to the collection of fields.
    */
   rerenderedFieldToCandidate: function (fieldModel) {
-    var activeEntity = Drupal.edit.collections.entities.where({isActive: true})[0];
+    var activeEntity = Drupal.edit.collections.entities.findWhere({isActive: true});
 
     // Early-return if there is no active entity.
-    if (activeEntity === null) {
+    if (!activeEntity) {
       return;
     }
 
diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js
index 0478d7c..65a6077 100644
--- a/core/modules/edit/js/views/ContextualLinkView.js
+++ b/core/modules/edit/js/views/ContextualLinkView.js
@@ -35,11 +35,11 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({
    */
   initialize: function (options) {
     // Insert the text of the quick edit toggle.
-    this.$el.find('a').text(this.options.strings.quickEdit);
+    this.$el.find('a').text(options.strings.quickEdit);
     // Initial render.
     this.render();
     // Re-render whenever this entity's isActive attribute changes.
-    this.model.on('change:isActive', this.render, this);
+    this.listenTo(this.model, 'change:isActive', this.render);
   },
 
   /**
diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js
index ce57d6f..a00432d 100644
--- a/core/modules/edit/js/views/EditorView.js
+++ b/core/modules/edit/js/views/EditorView.js
@@ -40,7 +40,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
    */
   initialize: function (options) {
     this.fieldModel = options.fieldModel;
-    this.fieldModel.on('change:state', this.stateChange, this);
+    this.listenTo(this.fieldModel, 'change:state', this.stateChange);
   },
 
   /**
@@ -50,7 +50,6 @@ Drupal.edit.EditorView = Backbone.View.extend({
     // The el property is the field, which should not be removed. Remove the
     // pointer to it, then call Backbone.View.prototype.remove().
     this.setElement();
-    this.fieldModel.off(null, null, this);
     Backbone.View.prototype.remove.call(this);
   },
 
diff --git a/core/modules/edit/js/views/EntityDecorationView.js b/core/modules/edit/js/views/EntityDecorationView.js
index f6fcb75..ad107f9 100644
--- a/core/modules/edit/js/views/EntityDecorationView.js
+++ b/core/modules/edit/js/views/EntityDecorationView.js
@@ -15,7 +15,7 @@ Drupal.edit.EntityDecorationView = Backbone.View.extend({
    * Associated with the DOM root node of an editable entity.
    */
   initialize: function () {
-    this.model.on('change', this.render, this);
+    this.listenTo(this.model, 'change', this.render);
   },
 
   /**
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
index c5844d2..889335a 100644
--- a/core/modules/edit/js/views/EntityToolbarView.js
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -13,9 +13,9 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
 
   events: function () {
     var map = {
-      'click.edit button.action-save': 'onClickSave',
-      'click.edit button.action-cancel': 'onClickCancel',
-      'mouseenter.edit': 'onMouseenter'
+      'click button.action-save': 'onClickSave',
+      'click button.action-cancel': 'onClickCancel',
+      'mouseenter': 'onMouseenter'
     };
     return map;
   },
@@ -29,11 +29,11 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
     this.$entity = $(this.model.get('el'));
 
     // Rerender whenever the entity state changes.
-    this.model.on('change:isActive change:isDirty change:state', this.render, this);
+    this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render);
     // Also rerender whenever a different field is highlighted or activated.
-    this.appModel.on('change:highlightedField change:activeField', this.render, this);
+    this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render);
     // Rerender when a field of the entity changes state.
-    this.model.get('fields').on('change:state', this.fieldStateChange, this);
+    this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange);
 
     // Reposition the entity toolbar as the viewport and the position within the
     // viewport changes.
@@ -117,7 +117,13 @@ Drupal.edit.EntityToolbarView = Backbone.View.extend({
    * {@inheritdoc}
    */
   remove: function () {
+    // Remove additional DOM elements controlled by this View.
     this.$fence.remove();
+
+    // Stop listening to additional events.
+    $(window).off('resize.edit scroll.edit');
+    $(document).off('drupalViewportOffsetChange.edit');
+
     Backbone.View.prototype.remove.call(this);
   },
 
diff --git a/core/modules/edit/js/views/FieldDecorationView.js b/core/modules/edit/js/views/FieldDecorationView.js
index 4dfc403..4172840 100644
--- a/core/modules/edit/js/views/FieldDecorationView.js
+++ b/core/modules/edit/js/views/FieldDecorationView.js
@@ -29,8 +29,8 @@ Drupal.edit.FieldDecorationView = Backbone.View.extend({
   initialize: function (options) {
     this.editorView = options.editorView;
 
-    this.model.on('change:state', this.stateChange, this);
-    this.model.on('change:isChanged change:inTempStore', this.renderChanged, this);
+    this.listenTo(this.model, 'change:state', this.stateChange);
+    this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
   },
 
   /**
diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js
index 49e5b27..8aee09f 100644
--- a/core/modules/edit/js/views/FieldToolbarView.js
+++ b/core/modules/edit/js/views/FieldToolbarView.js
@@ -28,7 +28,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
     // Generate a DOM-compatible ID for the form container DOM element.
     this._id = 'edit-toolbar-for-' + this.model.id.replace(/[\/\[\]]/g, '_');
 
-    this.model.on('change:state', this.stateChange, this);
+    this.listenTo(this.model, 'change:state', this.stateChange)
   },
 
   /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index f4bf194..ab44835 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1744,7 +1744,7 @@ function system_library_info() {
   $libraries['underscore'] = array(
     'title' => 'Underscore.js',
     'website' => 'http://underscorejs.org/',
-    'version' => '1.4.0',
+    'version' => '1.5.2',
     'js' => array(
       'core/assets/vendor/underscore/underscore.js' => array('group' => JS_LIBRARY, 'weight' => -20),
     ),
@@ -1754,7 +1754,7 @@ function system_library_info() {
   $libraries['backbone'] = array(
     'title' => 'Backbone.js',
     'website' => 'http://backbonejs.org/',
-    'version' => '0.9.2',
+    'version' => '1.1.0',
     'js' => array(
       'core/assets/vendor/backbone/backbone.js' => array('group' => JS_LIBRARY, 'weight' => -19),
     ),
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 955ef59..fa7dffa 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -257,8 +257,8 @@ Drupal.toolbar = {
     initialize: function (options) {
       this.strings = options.strings;
 
-      this.model.on('change:orientation', this.onOrientationChange, this);
-      this.model.on('change:activeTray', this.onActiveTrayChange, this);
+      this.listenTo(this.model, 'change:orientation', this.onOrientationChange);
+      this.listenTo(this.model, 'change:activeTray', this.onActiveTrayChange);
     },
 
     /**
@@ -311,9 +311,9 @@ Drupal.toolbar = {
     initialize: function (options) {
       this.strings = options.strings;
 
-      this.model.on('change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render, this);
-      this.model.on('change:mqMatches', this.onMediaQueryChange, this);
-      this.model.on('change:offsets', this.adjustPlacement, this);
+      this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render);
+      this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange);
+      this.listenTo(this.model, 'change:offsets', this.adjustPlacement);
 
       // Add the tray orientation toggles.
       this.$el
@@ -576,7 +576,7 @@ Drupal.toolbar = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change:subtrees', this.render, this);
+      this.listenTo(this.model, 'change:subtrees', this.render);
     },
 
     /**
@@ -611,7 +611,7 @@ Drupal.toolbar = {
      * {@inheritdoc}
      */
     initialize: function () {
-      this.model.on('change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render, this);
+      this.listenTo(this.model, 'change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render);
     },
 
     /**
diff --git a/core/modules/tour/js/tour.js b/core/modules/tour/js/tour.js
index 0438cdd..0f7a8fd 100644
--- a/core/modules/tour/js/tour.js
+++ b/core/modules/tour/js/tour.js
@@ -74,8 +74,8 @@ Drupal.tour.views.ToggleTourView = Backbone.View.extend({
    * Implements Backbone Views' initialize().
    */
   initialize: function () {
-    this.model.on('change:tour change:isActive', this.render, this);
-    this.model.on('change:isActive', this.toggleTour, this);
+    this.listenTo(this.model, 'change:tour change:isActive', this.render);
+    this.listenTo(this.model, 'change:isActive', this.toggleTour);
   },
 
   /**
