Two problems motivate the creation of this new model. The first is to make the editing experience faster. Our old model did expensive string operations on each edit. Our new model defers string operations until saving. The second is to avoid saving a whole copy of the string to the data store on each change. This would be prohibitively expensive for memory on large data sets.
How the original model works and what problems it poses
Our model originally was based on directly changing the data of the csv file on every change to the grid. The data is stored as a string, so if you had an editor open that looked like this
then the string would look like "1,2,3\n4,5,6\n7,8,9". If you added a row 1, we make this change appear by modifing the string so that it looked like ",,,\n1,2,3\n4,5,6\n7,8,9". Seems simple enough, but what happens if you want to undo the change you made? We have to options. One is to save a copy of the previous string in our data store (which is what we did). This means that the memory requirement for the program is roughly nm, where n is the number of changes and m is the size of the string. You can see how this blows up incredibly fast, even for relatively small data sets. The other option is to actually do an inverse operation on the original string when the undo command is executed. This is what we intially tried to do when we were first considering how to implement undoing. While it does fix the memory problem, it is very tricky to implement and doesn't fix the problem of string operations a long time.
How the new model works and the benefits of it
Our new model doesn't directly modify this string until you save the file. To understand what the new model is doing we have to dig a bit deeper into how the grid displays the correct values in each cell. The grid asks a class called DSVModel
what is at each row/column position by calling a one of DSVModel
's methods called data
. So, the DataGrid
would first call this._model.data('body', 0, 0)
to figure out what should be in the first row and first column (the body
argument specifies what region the DataGrid
is looking at). This method would return 1
, since 1 is the value in the first row, first column cell.
Our new model is called EditorModel
and it sits between The data grid and the model. When The data grid calls data
, it is actually calling EditorModel
's data
method. EditorModel
's data
function In turn calls the DSVModel
's data function. But first we do some pre-processing. We map the requested row and column to the row and column That corresponds to where the field lies. We do this with two arrays, which we call the rowMap
and the columnMap
. Here is a visual of what this looks like.
When the grid
queries for the top left body cell, it does data('body', 0, 0)
. Our code does something like this.
data(region, row, column) {
const row = rowMap[row]; // rowMap[0] = 0;
const column = columnMap[column]; // columnMap[0] = 0;
return this._model.data(region, row, column) // data('body', 0, 0) = 1.
}
Now, let's say we added a column. The new picture looks like this.
We'll cover why the 3 is negative. First, notice that when the datgrid asks for what is in, say, row 0 column 2, our function passes on to the DSVModel
's data
method the question "what is in row 0 column 1". This data
method then returns the exact value that was in row 0 column 1 before the change. But what about if the grid asks for a value in the B column? Here is where the negative comes in. We can add a little if
statement to our data
function.
const row = rowMap[row];
const column = columnMap[column];
if (row < 0 || column < 0) {
return '';
}
This is sufficient to handle moving, deleting, or adding rows/columns. But what if the user inputs data directly into one of the cells. Suppose in the previous example, we input a value hello world
into the first entry of the B column (corresponding to row 0 column 1). How would we know not to just return ''
when this value is queried for? What we do is set up a dictionary (called a MapField
in DataStore
terminology) that records specific values. So when the user put in hello world
, we would add the following key, value pair to what we call our valueMap
.
{ '0,1': 'hello world' }
And we add the following if
statement to our data
function.
const row = rowMap[row];
const column = columnMap[column];
if (valueMap[`${row},${column}`]) {
return valueMap[`${row},${column}`]
}
if (row < 0 || column < 0) {
return '';
}
...
That is essentially how the new model works. Here is a visual that sums it up pretty well.
- editing single cell working
- adding/removing/moving rows columns working
- cut/copy/paste working
- undo/redo working
- saving working
- S & R working
- clearing working