1

I have the following view model, used for editing Person objects in an HTML form:

function PersonModel(person) {
    var self = this;
    self.id = ko.observable(person.Id);
    self.firstName = ko.observable(person.FirstName);
    self.surname = ko.observable(person.Surname);
    self.email = ko.observable(person.Email);
    self.cell = ko.observable(person.Cell);

    self.save = function (data) {
        savePerson(data);
    };
}

When the user wants to edit a Person, I bind a new instance of this to an edit form, like this:

function editPerson(person) {
    var url = "@Url.Action("EditJson", "Person")";
    $.getJSON(url, function (data) {
        $("#person-detail").css("display", "block");
        ko.applyBindings(new PersonModel(data), $("#person-detail")[0]);
    });
}

And in the form I bind a click event to the save method in the view model:

<a href="#" data-bind="click: save">Update</a>

I now have a problem of multiple DB updates being run when I edit a single person record, and I assume this is because I have called applyBindings more than once on the same element, the edit popup. I can sort of confirm this as I have as many DB edits execute as times I call ApplyBindings.

Now I either need to know how to remove bindings applied by applyBindings, or how to only apply bindings once, and update my view model instead of recreating it for each edit. I would much prefer the first approach. A view model should not exhibit singleton characteristics.

ProfK
  • 49,207
  • 121
  • 399
  • 775

2 Answers2

3

Generally you would want to do something like create an observable to hold the currently selected record and then bind against it using the with or template binding.

So, you would have an editor like:

<div data-bind="with: currentItem">
  ...
</div>

Then, you would populate it with your current data:

this.currentItem(new PersonModel(data));

This way you are only applying bindings once and you don't run into issues with multiple event handlers, etc. The with binding will also only render its contents when the object is populated, so it will not show any content when currentItem is empty.

RP Niemeyer
  • 114,592
  • 18
  • 291
  • 211
0

Ryan's solution is the best for most situations. If you're truly dealing with a module of JS and HTML code that can be instantiated multiple times on a page, you might want to look into ko.cleanNode() or ko.removeNode().

Both of those methods are used internally by KO to handle memory leaks and binding cleanup. In a recent project, I have a table with several dynamically added rows of products. Each one of those products has to be editable inline. Instead of duplicating the form html for each row, I opted to setup a sort of KO module pattern where the html is dynamically loaded into the table row, bindings are applied, and bindings are removed once you're done editing.

Here's the basic AMD module I inherit from:

/**
Prototype object for creating individual KnockoutJS modules

@module Shared
@class ko-module-prototype
@namespace
@static
**/
define(["knockout", 'jquery', 'Shared/js/helpers'], function (ko, $, helpers) {
    var exports = {};
    /**
    KO viewModel
    @property vm
    @static
    **/
    exports.vm = {};
    /**
    KO bindings
    @property bindings
    @static
    **/
    exports.bindings = {};
    /**
    Optional binding namespace for avoiding bindings getting overwritten
    @property bindingNamespace
    @static
    **/
    exports.bindingNamespace = null;
    /**
    Whether bindings are registered with KO or not
    @property bindingsRegistered
    @static
    **/
    exports.bindingsRegistered = false;
    /**
    The HTML node wrapping the area we wish to apply KO bindings
    @property $wrapperNode
    @static
    **/
    exports.$wrapperNode = $("body");
    /**
    Optional html string to be loaded in as a template
    @property template
    @static
    **/
    exports.template = null;
    /**
    Injects html, registers bindings, and applies bindings
    @method start
    @static
    **/
    exports.start = function ($wrapperNode) {
        // Update wrapper node
        if ($wrapperNode) this.$wrapperNode = $wrapperNode;

        this._openNodeId = "mod_" + helpers.uniqueId();
        var $targetNode = $wrapperNode;

        // Insert template html
        // Wrap in a div
        if (this.template) {
            this.$wrapperNode.html($("<div></div>").attr("id", this._openNodeId).html(this.template));
            $targetNode = $wrapperNode.find("#" + this._openNodeId);
        }

        // Register bindings
        // Class binding provider has to be setup first...
        if (!this.bindingsRegistered) {
            this.bindingsRegistered = true;
            var register = this.bindings;
            if (this.bindingNamespace !== null) {
                register = { };
                register[this.bindingNamespace] = this.bindings;
            }
            ko.bindingProvider.instance.registerBindings(register);
        }

        ko.applyBindings(this.vm, $targetNode[0]);
    };
    /**
    Removes html, and un-applies bindings
    @method end
    @static
    **/
    exports.end = function () {
        var $openNode = $("#" + this._openNodeId)[0];

        if (this.template !== null) {
            ko.removeNode($openNode);
        } else {
            ko.cleanNode($openNode);
        }
    };
    return exports;
});

It uses Ryan's KO classBindingProvider which you can find here:

https://github.com/rniemeyer/knockout-classBindingProvider

Mike B
  • 2,660
  • 3
  • 20
  • 22
  • I've since added a recursive event listener removal to the end method. Since KO uses jQuery to bind events when it's available, you can use the .unbind method to remove the event listeners... http://stackoverflow.com/questions/7342814/knockoutjs-ko-applybindings-to-partial-view/7342861 – Mike B Oct 19 '12 at 21:09