Search code examples
gridviewknockout.jsdata-bindingdotvvmcontenttemplate

DotVVM: using custom bindings inside the GridViewTemplateColumn ContentTemplate


I've made a jQuery AutoComplete binding for KO. It allows to perform a server side search for suggestions by term. It also allows to fill a text box or an non-input html element with single value retrieved from the server by ID. So far it works OK.

When I place same binding into a ContentTemplate, during the first render of the GridView all the bindings work fine, the data for each id in items is retrieved from the server and the correct Name is injected into the span.

If I am trying to move to a second page of the grid, the main data is retrieved from server, I am getting the new ReviewObjectId-s for each row item, but the server is not requested(no requests in network tab of chrome debugger) and moreover the binding is not initialized at all, the names appear exactly like on previous page. Same behaviour occurs mostly until I go to the last page in pager or until more paging numbers are rendered in pager. Sometimes clicking the next page does the job

Filtering the DataSource to show the same Name for each row (every item has same target ReviewObjectId) often shows the same result.

The custom binding looks like this

<span data-bind="autoComplete:{apiOptions:{find:'/find-organization',get:'/get-organization', textItem:'name',valueItem:'id'},selectedValue: ReviewObjectId}"></span>

The "find" is autocomplete api url which produces a list of suggestions.

The "get" is a fill api url, which provides entity by Id (ReviewObjectId).

TextItem and ValueItem are provided for mapping the received JSON to viewModel.

I am using the same GridViewDataSet as DataSource both in GridView, DataPager and filter, the datasource is always filtered according to page and filter values correctly.

What am I doing wrong? Where should I dig to?

UPD: the binding:

ko.bindingHandlers.autoComplete = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        //initialize autocomplete with some optional options
        var options = valueAccessor().apiOptions;
        if (options && options.find) {
            var value = valueAccessor().selectedValue;
            var data = options.data || { data: {} };
            var searchOptions = { url: options.find, textItem: options.textItem, valueItem: options.valueItem };

            //init text on first load
            if (value() && options.get) {
                var fillOptions = { url: options.get, textItem: options.textItem, valueItem: options.valueItem };
                var fillData = value();
                var fillResult = function (data) {
                    if (data) {
                        if ($(element).is('input')) {
                            $(element).val(data);
                        } else {
                            $(element).html(data);
                        }
                    }
                };

                var promise = new Promise(function(resolve, reject) {
                    fetcher.search(fillOptions, fillData, fillResult);
                });
                if ($(element).is('input')) {
                    promise.then(fetcher.initAutoComplete(element, options, searchOptions, data, value));
                }
            }
            else {
                if ($(element).is('input')) {
                    fetcher.initAutoComplete(element, options, searchOptions, data, value);
                }
            }
        }
    },
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        //var value = ko.utils.unwrapObservable(valueAccessor()) || null;
    }
};

var fetcher = {
    initAutoComplete: function (element, options, searchOptions, data, valueAccessor) {
        $(element).autocomplete({
            delay: options.delay || 300,
            minLength: options.minLength || 3,
            classes: {
                "ui-autocomplete": "dropdown-menu"
            },
            source: function (request, response) {
                //loading values from cache
                if (request.term) {
                    var cacheKey = searchOptions.url + '-' + request.term;
                    if (cacheKey in this.cache) {
                        response(this.cache[cacheKey]);
                        return;
                    }
                }
                //querying server, contract data MUST contain Term
                data.Term = request.term;
                this.search(searchOptions, data, response);
            }.bind(this),
            select: function (e, i) {
                valueAccessor(i.item.val);
                $(element).val(i.item.label);
            }
        });
    },
    search: function(options, data, response) {
        $.ajax({
            url: options.url,
            data: JSON.stringify(data),
            dataType: "json",
            type: "POST",
            contentType: "application/json; charset=utf-8",
            success: function(responseData) {
                var textItemName = options.textItem || "value";
                var valueItemName = options.valueItem || "key";

                //cache results if exist
                var cacheKey = '';
                if (Array.isArray(responseData)) { //search by term
                    var result = $.map(responseData,
                        function (item) {
                            return {
                                label: item[textItemName],
                                val: item[valueItemName]
                            }
                        });
                    if (result.length > 0) {
                        cacheKey = options.url + '-' + data.Term;
                        this.cache[cacheKey] = result;
                    }

                } else { //init by bound value
                    if (responseData[textItemName] && responseData[textItemName].length > 0) {
                        cacheKey = options.url + '-' + responseData[valueItemName];
                        this.cache[cacheKey] = responseData[textItemName];
                    }
                }
                //send result to response
                response(this.cache[cacheKey]);
            }.bind(this),
            error: function (responseData) {
                console.log("error");
            },
            failure: function (responseData) {
                console.log("failure");
            }
        });
    },
    cache: {}
}

Solution

  • I think that you are ignoring the update event in your binding handler. When the page is changed, only the view model is refetched and bindings are updated, the page is not refetched. Have a look at the knockout.js documentation how to use the update function http://knockoutjs.com/documentation/custom-bindings.html:

    ko.bindingHandlers.yourBindingName = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            // This will be called when the binding is first applied to an element
            // Set up any initial state, event handlers, etc. here
        },
        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            // This will be called once when the binding is first applied to an element,
            // and again whenever any observables/computeds that are accessed change
            // Update the DOM element based on the supplied values here.
        }
    };
    

    Knockout basically watches any changes of observables touched when the binding is initialized and then invokes the update function when any of these changes.