Skip to content Skip to sidebar Skip to footer

Viewmodel Memory Leak - Getting Rid Of A Circular Dependency

I have knockout.js child view models living in an observableArray that depend on an observable in their parent view model. The parent's observable is bound to a select drop-down, a

Solution 1:

Check my comments, as I think they will help you a bit.

I made some changes to your fiddle. I didn't quite understand why you included underscore as it was not needed to bind the events. Also, your childViewModel and familyViewModel's were both actually just models. The isSelected property on the family was just an extra binding that meant each time you changed the selectedFamily you had to go out and do additional updates. Last, if you want to reset the children for the selectedFamily only, just remove the ko.utils.arrayForEach function and just do selectedFamily().children([]); I am not sure why you would want to only reset the selected families children but it is pretty easy to do.

Needs code -

var addChild = function () {
      self.selectedFamily().push(new childModel(self.selectedFamily().name());
 };

http://jsfiddle.net/xhzkC/2/

Solution 2:

Fiddle solution referenced earlier.

The crux of your problem is that you want to not only keep track of the currently selected family (.selectedFamily) but that you also want each family to know if they're selected. I think the simplest solution is to subscribe to selectedFamily and monitor its value before it changes and after it changes to set a family's isSelected value. I like this approach because there's no need to loop through the families array in order to do this.

self.selectedFamily = ko.observable();
self.selectedFamily.subscribe(function (oldValue) {
    if (oldValue) {
        oldValue.isSelected(false);
    }
}, null, 'beforeChange');
self.selectedFamily.subscribe(function (newValue) {
    if (newValue) {
       newValue.isSelected(true);
    }
});

Subscriptions default functionality passes in the new value after the value has already changed. Here you'll have access to the newly selected family and you can set its isSelected property to true. But subscriptions have two built in topics that get called by default. The first is the 'change' topic, that we just discussed, and the other is 'beforeChange'. This gets called before the value changes and gives you an opportunity to perform any business logic before a value is about to change, like setting an isSelected flag to false.

----------

Note, this next part is just some considerations I went through and extra information. The above is an adequate solution but for those who want to know more, read on...

----------

Another approach is to make selectedFamily a read/write computed with a private observable backing like this:

var _selectedFamily = ko.observable();
self.selectedFamily = ko.computed({
    read: _selectedFamily,
    write: function(newValue) {
        var oldValue = _selectedFamily();
        if (oldValue) {
            oldValue.isSelected(false);
        }
        if (newValue) {
           newValue.isSelected(true);
        }
        _selectedFamily(newValue);
    }
});

How I have this set up isn't bullet proof but should give you an idea of the concept here. This isn't a bad approach, and is one I'd go with if I had a need to make a decision based on the current and next value that would determine the outcome of setting the computed's result. But in this situation, I'm not a fan of setting the isSelected flag in here because it has no bearing on the resulting value. Assuming there was a need to create a computed to house logic that determined the outcome of the value that gets set, I would still opt with the subscriptions solution, as previously described, but on the privately backed _selectedFamily observable, in the off chance that there's a need to set the privately back observable's value without going through the computed.


The other piece I've addressed in the code is the click bindings on the buttons. I wrapped them in a with binding. Bindings that are bound to functions have the object of the current context passed into them by default. Now instead of having to do this:

self.addChild = function () {
    var selectedFamily = self.selectedFamily();
    // ...code...
};

You can do:

self.addChild = function(selectedFamily) {
};

I've also moved the <span data-bind="visible: isSelected">| Selected!</span> binding out of the children context and into the family context as I think this is what was intended. It seemed weird that you'd have the '| isSelected' text beside each child rather than the family name, but, if you wanted it by each child, you could do:

<lidata-bind="text: fullName"><spandata-bind="visible: $parent.isSelected">| Selected!</span></li>

One last feature about the KO foreach binding is binding with an object that has data/as properties. It can make your markup more human readable and really helps with accessing parent chains. Let's say we did want to use the family's isSelected flag to display something by the children's names. Above we have to reference using $parent. In some situations, you have multiple nested foreach bindings and you may find yourself writing things like <span data-bind="text: $parents[2].name"></span> which gives little information about the context you're trying to reference. Using this method, you could instead write:

<divdata-bind="foreach: { data: $root.families, as: 'family' }"><uldata-bind="foreach: { data: $family.children, as: 'child }>
        <li>
            <span data-bind="text:child.firstName"><spandata-bind="text: family.name"></li></ul></div>

In this way, you wouldn't even need a computed to write out the first and lastName. I would use this method when I think I'm going to have a complex nested context binding structure and/or when I want to make a simple nested structure seem more human readable.

http://jsfiddle.net/34ZjB/1/

Post a Comment for "Viewmodel Memory Leak - Getting Rid Of A Circular Dependency"