Addition of Automatic Node Layout Option
Sage-of-Mirrors opened this issue · 6 comments
I have come across an annoying issue with regards to arranging programmatically-created nodes within the graph. It appears that the nodes must be rendered before their sizes are properly set, which means that I cannot properly arrange the nodes because their sizes are report as 0 in both dimensions.
I was able to properly arrange a node network by hooking the ContentRendered
event on the window it occupies, but the window contains several networks in different tabs. Only the first network, the one that had focus be default, reported proper node sizes; the unfocused networks still had the default 0 sizes. I could not find any event exposed by NetworkView
that gave me access to a state where the node sizes of all networks were properly set, and forcing a layout update via the Invalidate
functions did not work either.
If there is not a simpler solution to this issue, I would like to suggest an addition to the NetworkView
, which I am happy to make a pull request for: a Func<NetworkViewModel>
property to contain a function for laying out the nodes, and a bool
property to tell whether the network should automatically run the layout function when NetworkView
's OnRender()
is called. This could possibly mesh with the current layout code in the Toolkit, perhaps by making the Func
a generic layout object instead.
I see, that is indeed a problem. The activation/deactivation event of the networkview might be the trigger you are looking for. If not, a temporary workaround could be to observe changes on the width and height of a node and see when they become non-zero, but that's ugly.
I'll be working on the library at the end of this month or the beginning of september, I'll look into it then.
Ok, so it seems there is no ContentRendered for UserControls, only for Windows. The best alternative is to work with the SizeChanged or LayoutUpdated events, but at that point you might as well just observe the Size property of the nodes.
Here is my suggestion:
myNetwork.Nodes.Connect()
.AutoRefresh(node => node.Size)
.Where(node => node.Size.Width > 0 && node.Size.Height > 0)
.Count()
.Select(sizedNodeCount => sizedNodeCount > 0)
.DistinctUntilChanged()
.Subscribe(sizedNodesVisible =>
{
// Do layouting here if sizedNodesVisible == true
// Optionally, you might want to add
// Where(sizedNodesVisible => sizedNodesVisible == true).FirstAsync()
// above to observe only the first time a node with non-zero size is visible.
});
Important note: make sure you include using DynamicData.Aggregation;
, otherwise the compiler will use the wrong version of .Count()
and the snippet above will not work.
This seems to work! However, as it's written, it will only trigger when the first node in the network viewmodel receives a size. I made the first .Where
statement into:
.Where(node => node.Size.Width > 0 && node.Size.Height > 0 && node == model.Nodes.Items.Last())
And this looks like it always triggers once the last node in the viewmodel has a size.
Encountered another issue: The nodes always report a Size of (120, 42) even if they should be bigger, e.g. if they have embedded controls or longer names.
Right, so it seems WPF updates the size of elements a bunch of times until settling on a final value, after which LayoutUpdated is triggered.
Does this produce the correct size for you?
networkView.Events().LayoutUpdated
.Where(_ => networkView.ViewModel != null && networkView.ViewModel.Nodes.Count > 0)
.Select(_ => networkView.ViewModel.Nodes.Items.Last().Size)
.Where(size => size.Width > 0 && size.Height > 0)
.FirstAsync()
.Subscribe(size => { Debug.WriteLine("size: "+size); });
(.Events()
is part of the ReactiveUI.Events.WPF package)
Looks like that does the trick! Thank you very much!