I have a winforms TreeView control with the Sorted property set to true. I also override the default sorter by assigning and instance of IComparer to the TreeViewNodeSorter property.
Unfortunately adding a few thousand nodes using the AddRange function takes perhaps 10 seconds. If I set Sorted to false the AddRange function is < 1/2 second. (please no discussions about the validity of adding so many nodes)
Aha I hear you say.. there is a problem in my IComparer object. Not according to the profiler. Barely any time is spent in the sorting object and yet the AddRange function is right at the top of the list of slow functions.
The problem is easy to replicate in a test project. Simply create a list of TreeNodes and add it to the an existing expanded tree node using the AddRange function. This will use the default sort on the tree text - again it is disproportionately slow.
To demonstrate how disproportionately slow it is if I disable the Sorted property in the test probject and use the List<T>.Sort function (with a delegate that compares the Text of the nodes) on my list of nodes before adding them to the tree there is virtually no delay.
This leads to the workaround of sorting the nodes manually before using AddRange. That's OK but it means a lot of work to find the correct insertion point when adding nodes to an existing set of child nodes - rather less convenient than simply setting Sorted to true.
Is there anyway to speed up the behaviour?
EDIT - it seems the only way is to sort before adding.. it's a bit of a hassle but I came up with the following extension method:
public static void AddSortedRange(this TreeNodeCollection existingNodes, IList<TreeNode> nodes, TreeView treeView, IComparer sorter)
{
TreeNode[] array = new TreeNode[nodes.Count + existingNodes.Count];
existingNodes.CopyTo(array, 0);
nodes.CopyTo(array, existingNodes.Count);
Array.Sort(array, sorter);
treeView.BeginUpdate();
existingNodes.Clear();
existingNodes.AddRange(array);
treeView.EndUpdate();
}
It is quicker to copy the existing nodes to an array, append the new nodes, sort the array and then replace that trying to manipulate nodes inline in the tree view - the slowest operation in the above code is the existingNodes.Clear() call
The performance problems you have are related to the fact that you are adding items to a sorted TreeView. What happens behind the scenes when you add to a sorted list is that for each item that you are adding, it tries to find it's place, which means that it needs to go through the whole list for each item, now imagine how many iteration that makes for each new item :)
What you can do is this:
TreeView tv = new TreeView(); // Just so I have a TreeView variable
TreeNode[] nodes = ... // Well, your list of nodes that you want to add
tv.SuspendLayout();
tv.Sorted = false;
tv.Nodes.Clear();
tv.Nodes.AddRange( nodes );
tv.Sorted = true;
tv.ResumeLayout();
For performance reasons we are using the SuspendLayout/ResumeLayout methods to disable the painting process used by the TreeView when manipulating it's items, which we would cause by removing the items and then adding them as well, since it would need to repaint to add the new item that you are adding (for each of the items).
Right before we are doing any changes to the Nodes Collection we have to call Sorted = false; to disable the sorting (this is just temporary - the user will not see any changes because of SuspendLayout).
Then just add the items to the collection (since the TreeView is not sorted for the time being it should be really quick).
Then we enable the sorting again by calling Sorted = true; setting the Sorted Property to true will cause for the collection to do a sort.
This way, the sort will be performed only once (and therefore the TreeView will just go once through the items).
One more thing, if you have a custom sorter defined for the ListView (tv.ListViewItemSorter), set it to null before adding the items as well, just temporary of course, re-enable it again before the ResumeLayout call.
I experienced a locking situation using the Sort() method.
It worked fine for weeks, then once, it stucks, stucking my application with 25% CPU in the task manager.
var allTags = _TagEngine.GetTags(1, force);
try
{
TagTree.BeginUpdate();
TagTree.Nodes.Clear();
foreach (var rec in allTags)
{
... adding nodes in the tree
}
TagTree.Sort(); // <= stuck here !
}
finally
{
TagTree.EndUpdate();
}
So I watch inside the Sort() method using a decompiler, and I noticed it handles already the BeginUpdate/EndUpdate feature internally.
Then I moved the TagTree.Sort() outside the BeginUpdate/EndUpdate, and it works fine since.
var allTags = _TagEngine.GetTags(1, force);
try
{
TagTree.BeginUpdate();
TagTree.Nodes.Clear();
foreach (var rec in allTags)
{
... adding nodes in the tree
}
}
finally
{
TagTree.EndUpdate();
}
TagTree.Sort();
I hardly understood what happened here. Why it worked in the past, and suddenly stoped. Frankly, I did not had time enough to dig further and anyway, the most important is here : it works again.
I made a simple extension to the TreeView control. It is very fast. It moves internal storage to a Dictionary which makes a huge difference. In my real world example I have 100000 records that I need to load. It took 37 minutes before, but now it takes 2.2 seconds!!
You can find example and code on CodeProject: http://www.codeproject.com/Articles/679563/Fast-TreeView
Related
I have an array with a few items in it. Every x seconds, I receive a new array with the latest data. I check if the data has changed, and if it has, I replace the old one with the new one:
if (currentList != responseFromHttpCall) {
currentList = responseFromHttpCall;
}
This messes up the classes provided by ng-animate, as it acts like I replaced all of the items -- well, I do actually, but I don't know how to not.
These changes can occur in the list:
There's one (or more) new item(s) in the list - not necessaryly at the end of the list though.
One (or more) items in the list might be gone (deleted).
One (or more) items might be changed.
Two (or more) items might have been swapped.
Can anyone help me in getting ng-animate to understand what classes to show? I made a small "illustation" of my problem, found here: http://plnkr.co/edit/TS401ra58dgJS18ydsG1?p=preview
Thanks a lot!
To achieve what you want, you will need to modify existing list on controller (vm.list) on every action. I have one solution that may work for your particular example.
you would need to compare 2 lists (loop through first) similar to:
vm.list.forEach((val, index)=>{
// some code to check against array that's coming from ajax call
});
in case of adding you would need to loop against other list (in your case newList):
newList.forEach((val, index)=>{
// some code to check array on controller
});
I'm not saying this is the best solution but it works and will work in your case. Keep in mind - to properly test you will need to click reset after each action since you are looking at same global original list which will persist same data throughout the app cycle since we don't change it - if you want to change it just add before end of each function:
original = angular.copy(vm.list);
You could also make this more generic and put everything on one function, but for example, here's plnkr:
http://plnkr.co/edit/sr5CHji6DbiiknlgFdNm?p=preview
Hope it helps.
I'm designing an inventory system. right now, I need to test whether or not an item is in the inventory in order for the stage to know whether to instantiate that item in the particular level or not.
I add the items to the levels in groups, so this code is located within an array loop which "unloads" the "pack" of items corresponding to each level.
if (inv.indexOf(group[i]) == -1) {
//add item + item functionality
}
This method works when I add the item to the inventory such as this:
inv.push(group[i]);
if (inv.indexOf(group[i]) == -1) {
//add item + item functionality
}
But that doesn't work, because why would I add an item to the inventory without the user collecting it first? so the code is actually structured as so:
if (inv.indexOf(group[i]) == -1) {
//if item is not in inventory, add to stage
addChild(group[i]);
//when a user clicks this (any) item,
group[i].addEventListener(MouseEvent.CLICK, function itemFunctionality(e:MouseEvent){
//target item clicked
var item = e.target;
//add the item to the inventory
inv.push(item);
//sidenote: if i were to check inv.indexOf(item) here, i
//would get a positive index. unfortunately,
//i cant check whether the item is in the inventory
//after its already been added to the level...
item.removeEventListener(MouseEvent.CLICK, itemFunctionality);
});
}
The problem is when you leave and come back to the level, the items you already collected re-instantiate. If you collect an item again, the inventory adds a copy of the item you already collected.
The inv.indexOf(group[i]) checker doesn't understand that when the array loop reaches the corresponding, item group[i] == the object added to the inventory through inv.push(item) or in other words inv.push(e.target) (which, of course, I couldn't write directly into the code)...
When I trace whats inside of static array inv, what group[i] is within the array loop, or what e.target is, they all output the same type of item, "[object itemName]", signifying that the indexOf check SHOULD match up.
Update :
It appears if I make the items static as well as the array group they belong to this method works within the mouse event callback:
inv.push(item);
group.splice(group.indexOf(item), 1);
Though I had to remove the items and the item groups from their own class and put them inside of the level class itself... I feel this method kind of sucks because everything is getting disorganized and grouped into the same class.
Any helpful suggestions?
Objects are matched with their references. It means two objects created from the same class are not identical, they are different objects.
Assign unique IDs to your items and use them in your inventory. Like;
inv.push(item.id);
if (inv.indexOf(item.id) == -1) {
//add item + item functionality
}
Working with IDs is also better for serializing / deserializing.
There's your problem:
The problem is when you leave and come back to the level, the items you already collected re-instantiate.
Why would coming back to a level cause reinstatiation of any kind?
You should only ever once create each level object and part of that process should be creating all objects contained in that level. Visiting a level merely is an interaction with that object, which may include removing objects and adding them to the inventory. Once the objects are gone from the level, they are gone.
There's no reason to reinstantiate a level (or any of the objects within it) when revisiting it. If you are running into this problem because you are using a time line based approach with frames and gotoAndStop() to switch between levels then this is the core of your problem and you should stop doing that.
I'm using angular-ui-grid 3.0.5 with the treeview extension to display a tree. The data loads normally, everything works as expected, except that expandRow fails silently.
My use case is this: suppose we have a path like a > b > c and I need c shown to the user as preselected. I know the selection is correctly done because when I manually expand the parent rows, the child row is indeed selected.
Should I call expandAllRows, all rows would be expanded. However, calling expandRow with references on rows a and b taken from gridOptions.data leads to nothing happening: all rows will remain collapsed.
Is there any precaution to be taken that I have maybe overlooked, or is this a bug?
There's one mention in a closed issue that may be related to this but problem I'm having, but I'm not even sure it's related, given how dry the comment/solution was.
There's no example of using expandRow in the documentation but it's in both the API and the source code.
The gridRow objects mentioned in the documentation http://ui-grid.info/docs/#/api/ui.grid.treeBase.api:PublicApi are not the elements you put into the data array (though this seems to be not explained anywhere).
What the function expects is an object that the grid creates when building the tree, you can access them by looping through the grids treeBase.tree array. This will only be valid when the grid has built the tree, it seems, so it is not directly available when filling in the data, that's why registering a DataChangeCallback helps here https://github.com/angular-ui/ui-grid/issues/3051
// expand the top-level rows
// https://github.com/angular-ui/ui-grid/issues/3051
ctrl.gridApi.grid.registerDataChangeCallback(function() {
if (ctrl.gridApi.grid.treeBase.tree instanceof Array) {
angular.forEach(ctrl.gridApi.grid.treeBase.tree, function(node) {
if (node.row.treeLevel == 0) {
ctrl.gridApi.treeBase.expandRow(node.row);
}
});
}
});
self.onContentReady = function (e) {
e.component.expandRow(e.component.getKeyByRowIndex(0));
e.component.expandRow(e.component.getKeyByRowIndex(1));
};
selec which row you wanna expand
I have a fairly standard requirement — I need to be able to open a dialog where user can change values in data-bound fields, and then choose to click OK or Cancel, where clicking Cancel reverts the changes.
I've looked at IEditableCollectionView, IEditableObject and BindingGroups, but they all seem to be meant for editing a single item at a time. My program provides a collection of objects in a list, user selects an item from the list and edits it using SelectedItem-bound TextBoxes. Meaning that any number of items may be edited, including adding and removing them from the list, and all of those changes need to be reverted if he presses cancel.
At first I was simply making object backups through deep-copy (serialization) and restoring them on cancel, but now the objects must contain references to other, shared objects, making this approach problematic.
What's the best way to approach such a scenario without manually copying objects and/or values back and forth?
In this case the DataTable class would work Perfectly. It can save changes, go back (step by step) or revert all changes and many other features.
DataTable class has a nested feature that goes well with XML.
In case you're willing to save in a database then take a look at EntityFramework
After more thought on the matter, I have concluded that the best way, at least for small-scale implementation, is to write a "by value deep copy" method that copies values of objects fields and properties without replacing the objects themselves (so that any references to the edited objects remain intact even when data is restored).
For this purpose I have written the following extension method:
public static void CopyDataTo(this Object source, Object target) {
// Recurse into lists
if (source is IList) {
var a = 0;
foreach (var item in (IList)source) {
if (a >= ((IList)target).Count) {
var type = item.GetType();
var assembly = Assembly.GetAssembly(type);
var newItem = assembly.CreateInstance(type.FullName);
((IList)target).Add(newItem);
}
item.CopyDataTo(((IList)target)[a]);
a++;
}
while (a < ((IList)target).Count) {
((IList)target).RemoveAt(a);
}
}
// Copy over fields
foreach (var field in source.GetType().GetFields())
field.SetValue(target, field.GetValue(source));
// Copy properties
foreach (var property in source.GetType().GetProperties().Where(
property => property.CanWrite && !property.GetMethod.GetParameters().Any()))
{
property.SetValue(target, property.GetValue(source));
}
}
It's no silver bullet: it only works on objects of the same type, list items have to have a parametrless constructor and there is no way to control recursion depth. In addition, I haven't yet had a chance to test in any long-term or more complex scenarios, but so far it does what it should (copies values between objects) and can be used for a simple backup/restore scenario:
var backup = new TypeOfVariableToEdit();
data.CopyDataTo(backup);
var clickedOK = RunDataEditor(data);
if (!clickedOK)
backup.CopyDataTo(data);
The best approach is not to:
if you need these items, get a fresh copy of them from the database or whatever data storage, allow the user to make changes, and if they press cancel, just discard the changes. If they press save, save the data to the storage and then refresh your existing screens or whatever.
For some reason, a treeview I'm buildling in Silverlight has decided it no longer wants to display the triangle associated with the root level. It still functions correctly though. Pictures below:
As you can see, it is just the root level that is exhibiting this behavior. Any ideas on what could be causing this?
After banging my head into a wall for awhile about this one. I got it, here's the source:
public void HandleGroupData(ObservableCollection<Group> groupTree)
{
foreach (var group in groupTree)
{
var groupNode = new TreeNode(group.DisplayText, ENodeType.Group, group.Id);
GetSubitemsOfGroup(group, groupNode);
RootLevel.Add(groupNode);
}
}
We build the TreeView from the database. Originally the RootLevel.Add and GetSubitems calls were in reverse order. GetSubitems is a routine that recursively is called building the tree in a DFS. What I believe was happening was that we were adding a node to the tree that had no children, so initially the Silverlight GUI builder thought that they had no child nodes so didn't give them little triangles.
Moral of the story: Watch the ordering of tree view creation!