Dialog Listview doesn't visually update the correct subcolumn value if the list was sorted

Good day.

I’ve noticed this problem for a long time, but I couldn’t report it because I couldn’t reproduce it on a small scale, until I finally connected all the dots.

Take this script as an example:

function Ontest(scriptCmdData) {
	DOpus.ClearOutput();
	var dlg = scriptCmdData.func.dlg();
	dlg.template = 'dialog1';
	dlg.Create();
	var listview = dlg.Control('listview');
	var static = dlg.Control('static');
	var btn = dlg.Control('btn');
	var row, objs = {};

	for (var i = 0; i <= 5; i++) {
		objs[i] = {
			'name': 'ID' + i,
			'value': NewValue(10)
		};
		row = listview.GetItemAt(listview.AddItem(objs[i].name, i));
		if (row) {
			row.subitems(0).text = objs[i].value;
			objs[i].pos = row.index; //used just for logging purposes
		}
	}
	listview.columns.autosize();
	dlg.Show();
	var msg, sel_id = -1;
	listview.value = 0;
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		if (msg.event === 'click' && msg.control === 'btn') {

			if (sel_id < 0) continue;
			Log('name=' + objs[sel_id].name + '; id=' + sel_id + '; value=' + objs[sel_id].value);
			row = listview.GetItemByName(objs[sel_id].name);
			if (row) {
				Log('row_name=' + row.name + '; row_id=' + row.data + '; row_pos=' + row.index);
				row.subitems(0).text = NewValue(10);
				listview.columns.autosize();
				static.label = (row.index !== objs[sel_id].pos ? ('<#FF00FF>Wrong pos from row "' + row.name + '". Expected "' + row.index + '", but received "' + objs[sel_id].pos + '" instead') : ('Last change:' + row.name + ':' + row.subitems(0).text));
			}
		}
		else if (msg.event === 'selchange' && msg.control === 'listview') {
			Log('id=' + msg.data + '; pos=' + msg.index);
			sel_id = msg.data;
			btn.enabled = sel_id >= 0;
		}
	}

}

dlg_test.opusscriptinstall (1.1 KB)

I tend to use the data value from Control.AddItem(value, data) as an ID to associate an object with a row and its data.
And since there are two ways to get a DialogListItem object: by index and by name, not by that data value. But the position can change when you sort the content (if columns are sortable), I thought getting by name would be the right approach. But I realized that once you've sorted by any column at least once, Control.GetItemByName() returns the wrong object. Or in other words, it returns the object at its original position, even if it visually appears elsewhere. That means if you make a change to the list, it applies to the wrong row, and if it's a long list, it can look like no value changed at all.

Now that I've found the problem, the workaround is to store the initial position and use Control.GetItemAt() instead with that value, which, also incorrectly (because the position changed from its initial one), gives you the correct row visually. I know it sounds confusing :grinning_face:.

In summary, after sorting the list there's a mismatch between what you see and what you get when you request a DialogListItem by name, even though the name obviously stays the same. As far as I understand, this seems like a bug, right? Or am I missing something?

And since the ID (or data) that the user assigns to each row is less likely to change (because it’s user-defined and should be unique, unlike the "name"), could a Control.GetItemByID() or something similar be added :backhand_index_pointing_right: :backhand_index_pointing_left:?

I hope this makes sense!

Hi. I wanted to test your script but the dlg_test.opusscriptintall does not match the code above at all:

function OnInit(initData) {
	initData.name = "dlg_test";
	initData.version = "1.0";
	initData.copyright = "";
	initData.desc = "";
	initData.default_enable = true;
	initData.min_version = "13.0";

}

// Called to add commands to Opus
function OnAddCommands(addCmdData) {
	var cmd = addCmdData.AddCommand();
	cmd.name = "dlg_test";
	cmd.method = "Ondlg_test";
	cmd.desc = "";
	cmd.label = "dlg_test";
	cmd.template = "";
	cmd.hide = false;
	cmd.icon = "script";
}

// Implement the dlg_test command
function Ondlg_test(scriptCmdData) {
	var parent = scriptCmdData.func.sourcetab.lister;
	var dlg = parent.dlg();
	dlg.template = 'dialog1';
	dlg.disable_window = parent;
	dlg.Create();
	var l = '';
	for (var i = 0; i < 200; i++) {
		l += i + '<%ddbi:' + i + '>';
	}
	dlg.Control('markuptext1').label = l;
	dlg.Control('markuptext1').autosize();
	dlg.Show();
	var msg;
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		if (msg.event === 'click' && msg.control === 'button1') { //change comboedit value
			if (dlg.Control('combo1').value.index === -1) {
				//???? 
			}
			else dlg.Control('combo1').value.name = 'changed!'; //Error 0x80004005 if used while index == -1
		}
	}
	return;
}
==SCRIPT RESOURCES
<resources>
	<resource name="dialog1" type="dialog">
		<dialog fontsize="9" height="202" lang="english" width="274">
			<control height="190" name="markuptext1" title="&lt;%ddbi:5&gt;&lt;%ddbi:5&gt;" type="markuptext" width="262" x="6" y="6" />
		</dialog>
	</resource>
</resources>

EDIT: Never mind, I made a dialog with the correct controls.

I'm not sure to get the issue, but I might be missing the point.

From what I see, you consider that there is a problem when row.index differs from objs[sel_id].pos. The thing is the latter (objs[sel_id].pos) is never updated in your code, so it will always hold the initial positions of your objects.
Since you did sort, it's no longer accurate.

Yet, the problem of subitems updating remains.
To me it looks like the subitems are the ones that do not get reordered.

I made a little modification to your script with the following change: when updating a value, I append _ to the name (and reflect it into objs array).
You can see that the proper name gets updated (accesed by row.name), yet the change in the subitem (accessed by row.subitems(0)) is made on the wrong line.

I'll try and play around with it further, but I'd tend to say that the subitems did not move with their DialogListItem during sort.

Code modified:

function TestListViewOrderingAndIndexes(scriptCmdData) {
	DOpus.ClearOutput();
	var dlg = scriptCmdData.func.dlg();
	dlg.template = 'dialog1';
	dlg.Create();
	var listview = dlg.Control('listview');
	var static = dlg.Control('static');
	var btn = dlg.Control('btn');
	var row, objs = {};

	for (var i = 0; i <= 5; i++) {
		objs[i] = {
			'name': 'ID' + i,
			'value': NewValue(10)
		};
		row = listview.GetItemAt(listview.AddItem(objs[i].name, i));
		if (row) {
			row.subitems(0).text = objs[i].value;
			objs[i].pos = row.index; //used just for logging purposes
		}
	}
	listview.columns.autosize();
	dlg.Show();
	var msg, sel_id = -1;
	listview.value = 0;
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		if (msg.event === 'click' && msg.control === 'btn') {

			if (sel_id < 0) continue;
			Log('name=' + objs[sel_id].name + '; id=' + sel_id + '; value=' + objs[sel_id].value);
			row = listview.GetItemByName(objs[sel_id].name);
			if (row) {
				Log('row_name=' + row.name + '; row_id=' + row.data + '; row_pos=' + row.index);
				row.name += "_";
				objs[sel_id].name = row.name;
				row.subitems(0).text = NewValue(10);
				listview.columns.autosize();
				static.label = (row.index !== objs[sel_id].pos ? ('<#FF00FF>Wrong pos from row "' + row.name + '". Expected "' + row.index + '", but received "' + objs[sel_id].pos + '" instead') : ('Last change:' + row.name + ':' + row.subitems(0).text));
			}
		}
		else if (msg.event === 'selchange' && msg.control === 'listview') {
			Log('id=' + msg.data + '; pos=' + msg.index);
			sel_id = msg.data;
			btn.enabled = sel_id >= 0;
		}
	}
	
}

EDIT: The small test below tends to say otherwise ... there must be something different in your code or logic that makes it fail. I'll have a close look later.

// ListViewIssue
// (c) 2024 Stephane

// This is a script for Directory Opus.
// See https://www.gpsoft.com.au/endpoints/redirect.php?page=scripts for development information.



// Called by Directory Opus to initialize the script
function OnInit(initData)
{
	initData.name = "ListViewIssue";
	initData.version = "1.0";
	initData.copyright = "(c) 2024 Stephane";
//	initData.url = "https://resource.dopus.com/c/buttons-scripts/16";
	initData.desc = "";
	initData.default_enable = true;
	initData.min_version = "13.0";
	initData.group = "Test";

}

// Called to add commands to Opus
function OnAddCommands(addCmdData)
{
	var cmd = addCmdData.AddCommand();
	cmd.name = "ListViewIssue";
	cmd.method = "OnListViewIssue";
	cmd.desc = "";
	cmd.label = "ListViewIssue";
	cmd.template = "";
	cmd.hide = false;
	cmd.icon = "script";
}


// Implement the TestGeneric command
function OnListViewIssue(scriptCmdData)
{
	DOpus.ClearOutput();
	var dlg = scriptCmdData.func.dlg();
	dlg.template = 'dialog1';
	dlg.Create();
	var listview = dlg.Control('listview');
	var static = dlg.Control('static');
	var btn = dlg.Control('btn');
	var row, objs = {};

	for (var i = 0; i <= 5; i++) {
		objs[i] = {
			'name': 'ID' + i,
			'value': 5 - i
		};
		row = listview.GetItemAt(listview.AddItem(objs[i].name, i));
		if (row) {
			row.subitems(0).text = objs[i].value;
			objs[i].pos = row.index; //used just for logging purposes
		}
	}
	listview.columns.autosize();

	Log("Before showing dialog");
	for (var i = 0; i <= 5; i++) {
		var itemName = listview.GetItemAt(i).name;
		var msg = "Row=" + i + " > Name=" + itemName;
		msg += "| val(id)=" + listview.GetItemAt(i).subitems(0).text;
		msg += "| val(name)=" + listview.GetItemByName(itemName).subitems(0).text;
		Log(msg);
	}

	dlg.Show();
	var msg, sel_id = -1;
	listview.value = 0;
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		if (msg.event === 'click' && msg.control === 'btn') {

			Log("Checking items");
			for (var i = 0; i <= 5; i++) {
				var itemName = listview.GetItemAt(i).name;
				var msg = "Row=" + i + " > Name=" + itemName;
				msg += "| val(id)=" + listview.GetItemAt(i).subitems(0).text;
				msg += "| val(name)=" + listview.GetItemByName(itemName).subitems(0).text;
				Log(msg);
			}


			// if (sel_id < 0) continue;
			// Log('name=' + objs[sel_id].name + '; id=' + sel_id + '; value=' + objs[sel_id].value);
			// row = listview.GetItemByName(objs[sel_id].name);
			// if (row) {
			// 	Log('row_name=' + row.name + '; row_id=' + row.data + '; row_pos=' + row.index);
			// 	row.name += "_";
			// 	objs[sel_id].name = row.name;
			// 	row.subitems(0).text = NewValue(10);
			// 	listview.columns.autosize();
			// 	static.label = (row.index !== objs[sel_id].pos ? ('<#FF00FF>Wrong pos from row "' + row.name + '". Expected "' + row.index + '", but received "' + objs[sel_id].pos + '" instead') : ('Last change:' + row.name + ':' + row.subitems(0).text));
			// }
		}
		else if (msg.event === 'selchange' && msg.control === 'listview') {
			Log('id=' + msg.data + '; pos=' + msg.index);
			sel_id = msg.data;
			btn.enabled = sel_id >= 0;
		}
	}
	
}

function NewValue(size) {
    var allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    var result = "";
    var i;
    for (i = 0; i < size; i++) {
        var randIdx = Math.floor(Math.random() * allowedChars.length);
        result += allowedChars.charAt(randIdx);
    }
    return result;
}

function Log(msg) {
	DOpus.Output(msg);
}


==SCRIPT RESOURCES
<resources>
	<resource name="dialog1" type="dialog">
		<dialog height="182" lang="francais" width="292">
			<control height="14" name="btn" title="Check Items" type="button" width="50" x="236" y="4" />
			<control height="130" name="listview" type="listview" viewmode="details" width="276" x="8" y="22">
				<columns>
					<item text="Name" />
					<item text="Value" />
				</columns>
			</control>
			<control halign="left" height="8" name="static" title="Static" type="static" valign="top" width="274" x="10" y="160" />
		</dialog>
	</resource>
</resources>

ListViewIssue.opusscriptinstall (2.0 KB)

1 Like

First of all, big thanks to @PassThePeas for taking the time to check this out—it’s really appreciated.

Also, sorry about the incorrect script. I think I may have found another bug while exporting it!

Don't spend too much time on it. That part was there because the original test script had a built-in workaround, and when I posted it, I only left that section to make things clearer—though I think it ended up causing more confusion.

Now that I've looked at this more carefully, I think the title of the post is off (corrected). The issue isn't that you get the wrong DialogListItem object, but rather that there's a mismatch between what's visually shown and the actual values Opus holds for subcolumns, specifically when their order changes.

Here's the script (this time properly shared) that better shows what's going on.
dlgtest.opusscriptinstall (1.6 KB)

In this image, you can see that as far as Opus is concerned, the correct value did change—but visually, the change happened on a different row. ("ID9" was supposed to change, but instead, "ID11" is the one that visually changed—even though querying ID9's subcolumn value shows the update)

1 Like

Exactly the conclusion I just came too in parallel to your post.

Code below show that after an update, the inner values of the DialogListItems (and subitems) are correct, but what is seen does not match:

Code to reproduce:

function OnInit(initData)
{
	initData.name = "GenericTestCommand";
	initData.version = "1.0";
	initData.copyright = "(c) 2024 Stephane";
//	initData.url = "https://resource.dopus.com/c/buttons-scripts/16";
	initData.desc = "";
	initData.default_enable = true;
	initData.min_version = "13.0";
	initData.group = "Test";
}

// Called to add commands to Opus
function OnAddCommands(addCmdData)
{
	var cmd = addCmdData.AddCommand();
	cmd.name = "TestGeneric";
	cmd.method = "OnTestGeneric";
	cmd.desc = "";
	cmd.label = "TestGeneric";
	cmd.template = "INPUT/O";
	cmd.hide = false;
	cmd.icon = "script";
}


// Implement the TestGeneric command
function OnTestGeneric(scriptCmdData)
{
	TestListViewOrderingAndIndexes(scriptCmdData);
}

function TestListViewOrderingAndIndexes(scriptCmdData) {
	DOpus.ClearOutput();
	var dlg = scriptCmdData.func.dlg();
	dlg.template = 'dialog1';
	dlg.Create();
	var listview = dlg.Control('listview');
	var static = dlg.Control('static');
	var btn = dlg.Control('btn');
	var row, objs = {};

	for (var i = 0; i <= 5; i++) {
		objs[i] = {
			'name': 'ID' + i,
			'value': NewValue(10)
		};
		row = listview.GetItemAt(listview.AddItem(objs[i].name, i));
		if (row) {
			row.subitems(0).text = objs[i].value;
			objs[i].pos = row.index; //used just for logging purposes
		}
	}
	listview.columns.autosize();
	dlg.Show();
	var msg, sel_id = -1;
	listview.value = 0;
	while (true) {
		msg = dlg.GetMsg();
		if (!msg.result) break;
		if (msg.event === 'click' && msg.control === 'btn') {
			Log("Button Click");
			CheckItems();

			if (sel_id < 0) continue;
			Log('name=' + objs[sel_id].name + '; id=' + sel_id + '; value=' + objs[sel_id].value);
			row = listview.GetItemByName(objs[sel_id].name);
			if (row) {
				Log('row_name=' + row.name + '; row_id=' + row.data + '; row_pos=' + row.index);
				row.name += "_";
				objs[sel_id].name = row.name;
				row.subitems(0).text = NewValue(10);
				listview.columns.autosize();
				static.label = (row.index !== objs[sel_id].pos ? ('<#FF00FF>Wrong pos from row "' + row.name + '". Expected "' + row.index + '", but received "' + objs[sel_id].pos + '" instead') : ('Last change:' + row.name + ':' + row.subitems(0).text));
			}
			Log("After modification");
			CheckItems();

		}
		else if (msg.event === 'selchange' && msg.control === 'listview') {
			Log('SelChange >> id=' + msg.data + '; pos=' + msg.index);
			sel_id = msg.data;
			btn.enabled = sel_id >= 0;
			CheckItems();
		}
	}

	function CheckItems() {
		Log(" Checking Items:")
		for (var i = 0; i <= 5; i++) {
			var itemName = listview.GetItemAt(i).name;
			var msg = "  > Row=" + i + " > Name=" + itemName;
			msg += "| val(id)=" + listview.GetItemAt(i).subitems(0).text;
			msg += "| val(name)=" + listview.GetItemByName(itemName).subitems(0).text;
			Log(msg);
		}
	}
	
}

function NewValue(size) {
    var allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    var result = "";
    var i;
    for (i = 0; i < size; i++) {
        var randIdx = Math.floor(Math.random() * allowedChars.length);
        result += allowedChars.charAt(randIdx);
    }
    return result;
}

function Log(msg) {
	DOpus.Output(msg);
}

==SCRIPT RESOURCES
<resources>
	<resource name="dialog1" type="dialog">
		<dialog height="182" lang="francais" width="292">
			<control height="14" name="btn" title="New Value" type="button" width="50" x="236" y="4" />
			<control height="130" name="listview" type="listview" viewmode="details" width="276" x="8" y="22">
				<columns>
					<item text="Name" />
					<item text="Value" />
				</columns>
			</control>
			<control halign="left" height="8" name="static" title="Static" type="static" valign="top" width="274" x="10" y="160" />
		</dialog>
	</resource>
</resources>

1 Like

A fix for that will be in the next beta.

3 Likes

Confirmed. Thanks!