Quantcast
Channel: Kendo UI – Falafel Software Blog
Viewing all articles
Browse latest Browse all 43

Make a Kendo MVVM calculated field depend on DataSource data

$
0
0

Imagine this: You are developing a web page that displays data in a grid but you also want to be able to show various other views and aggregations outside the grid. How would you do it? You could hook up event handlers to the grid’s DataSource and manually update everything when the data changes. But wouldn’t it be nicer if you could just declare MVVM bindings and let the bindings detect all the changes? I think so. Let me know you how it’s done.

Scalar bindings

First, let’s talk about scalar bindings such at the text and value bindings. You should already know from the Kendo docs that in order for a function to act as a dependent property, you must access other ObservableObject properties using the get method. But how do you do that with a DataSource? Take the following setup (JSBin link):

<div data-role="grid" data-bind="source:gridSource"
       data-editable="true">
  </div>
  <label>
    Total Quantity:
    <input data-bind="value: quantitySum">
  </label>
var viewModel = kendo.observable({
  gridSource: new kendo.data.DataSource({
    data: [
      { category: 'Dogs', quantity: 4 },
      { category: 'Cats', quantity: 3 },
      { category: 'Apples', quantity: 12 },
      { category: 'Oranges', quantity: 8 },
      { category: 'PCs', quantity: 5 }
    ]
  }),
  getGridData: function() {
    return this.gridSource.data();
  },
  quantitySum: function() {
    return _.reduce(this.getGridData(), function sum(total, item) {
      return total + item.get('quantity');
    }, 0);
  }
});

kendo.bind(document.body, viewModel);

In this case, when you edit the items in the grid, the value bound to quantitySum will not update. If you dig deeper, you will find that using get(‘quantity’) does not actually create a dependency. But there is a way to create one and it’s quite simple if perhaps not quite intuitive or documented. Simply access the data like this:

getGridData: function() {
    return this.get('gridSource').data();
  }

By specifying get(‘gridSource’), you create a dependency on the DataSource so that when data within it changes, all scalar bindings will update. (JSBin link)

Source binding

I keep using that word “scalar” to qualify my statements about dependent MVVM functions and here’s why: the same behavior will not occur with a source binding. So for example, if you have this chart and dependent data defined, the chart will not update even though the textbox does: (JSBin link)

<div data-role="chart" data-bind="source:getChartSource"
       data-series="[{
                      field: 'quantity'
                    }]"
       data-category-axis="{
                             field: 'category'
                           }">
  </div>
getChartSource: function() {
    var gridData = this.getGridData();
    return [
      {
        category: 'Cats & Dogs',
        quantity: gridData[0].get('quantity') + gridData[1].get('quantity')
      },
      {
        category: 'Apples & Oranges',
        quantity: gridData[2].get('quantity') + gridData[3].get('quantity')
      },
      {
        category: 'Apples & PCs',
        quantity: gridData[2].get('quantity') + gridData[4].get('quantity')
      }
    ];
  }

From my reading of the Kendo source code, this is intentional. The source binding is only supposed to bind a set of data as a DataSource to a widget; past that point the only thing the source binding cares about is whether the entire source is refreshed, in which case it will update dependent MVVM bindings. I suspect the intention here was that changes to individual items within a DataSource are supposed to be handled by the DataSource rather than by MVVM bindings. So you will have to write some code in order to notify MVVM to refresh the binding. Here are three options:

The first option is just to make MVVM think that the entire DataSource has been replaced, forcing it to refresh all dependent bindings. You can accomplish this by raising a change event with the correct event parameters filled: (JSBin link)

viewModel.trigger('change', { field: 'getChartSource' });

The nice thing about this approach is that it takes the least code and will notify all dependent MVVM bindings. You don’t have to obtain individual widget references and call their refresh methods. The slightly icky thing about this is that it does depend on the Kendo implementation of MVVM change notifications, so there is always a chance that this code will stop working at some future date if the internal implementation changes.

The next option is to do the same thing as above but without depending on any kind-of-private implementation details. Create an observable array that the chart is bound to and manipulate the array: (JSBin link)

<div data-role="chart" data-bind="source:chartSource"
       data-series="[{
                      field: 'quantity'
                    }]"
       data-category-axis="{
                             field: 'category'
                           }">
  </div>
change: function(e) {
      viewModel.set('chartSource', viewModel.getChartSource());
    }
  }),
  chartSource: [],
  getChartSource: function() {

This take slightly more code but keeps you from depending on the parameter structure of the Kendo MVVM change event.

The final option takes the most code, but does have one important difference from the first two: the first two completely refresh the dependent data source, which may perform worse than updating only the changed items. This final example illustrates how one might perform the least amount of data change. (JSBin link)

change: function(e) {
      if (!e.action) {
        // No action means a read or complete refresh
        viewModel.set('chartSource', viewModel.getChartSource());
      } else if (e.action == 'itemchange') {
        var gridData = viewModel.getGridData(),
            item = e.items[0];
        switch (item.get('category')) {
          case 'Dogs':
          case 'Cats':
            viewModel.chartSource[0].set('quantity', gridData[0].get('quantity') + gridData[1].get('quantity'));
            break;
          case 'Apples':
            viewModel.chartSource[1].set('quantity', gridData[2].get('quantity') + gridData[3].get('quantity'));
            viewModel.chartSource[2].set('quantity', gridData[2].get('quantity') + gridData[4].get('quantity'));
            break;
          case 'Oranges':
            viewModel.chartSource[1].set('quantity', gridData[2].get('quantity') + gridData[3].get('quantity'));
            break;
          case 'PCs':
            viewModel.chartSource[2].set('quantity', gridData[2].get('quantity') + gridData[4].get('quantity'));
            break;
        }
      }
    }

Given the amount of code it takes to do this more precise update, I would probably recommend you start with one of the first two techniques and only move to this one once performance becomes a priority.

The post Make a Kendo MVVM calculated field depend on DataSource data appeared first on Falafel Software Blog.


Viewing all articles
Browse latest Browse all 43

Trending Articles