Zoom to and center a specific node

Hello Walter,

In my application I have multiple Layers, containing each a single node with a background image. The user can switch between the nodes with only one being visible at a time. If the user does switch, I need the panel to be centered and zoomed to the now visible node.

ZoomToFit() doesn’t work. Sometimes it does something unpredictable, other times it seems it doesn’t do anything at all.
I’ve also tried setting the Scale property of the Panel in the Diagram, which then does scale correctly but I’m unable to get the Panel to scroll so the node’s in the center of the viewport. Setting the panel’s position does nothing, I can’t set the diagram’s bounds and calling CenterPart with the background node also doesn’t do the trick, though the bounds of the node are set correctly.

How can I get this to work?

DiagramPanel.CenterPart or DiagramPanel.CenterRect should work. There is also the DiagramPanel.MakeVisible method. Most generally you can call DiagramPanel.SetScaleAndPosition.

But I don’t know why those methods are not working for you. Perhaps because you call them too early, before all of the Nodes and Links have been measured and arranged and laid out? If that’s the case, you could call them later, perhaps via BeginInvoke. Here’s some code from the Local View sample:

      myLocalView.Dispatcher.BeginInvoke((Action)(() => {
        Node localnode = myLocalView.PartManager.FindNodeForData(data, myLocalView.Model);
        myLocalView.Panel.CenterPart(localnode);
        myLocalView.Select(localnode);
      }));

I can see DiagramPanel.ZoomToFit not doing anything, because it will scale the whole diagram contents to fit in the viewport.

It is quite awkward as SetScaleAndPosition also does behave like ZoomToFit, that is, zooming incorrectly or not at all. I’m calling the method in an event handler for InitialLayoutCompleted as well as OnNavigatedTo of the view model and, as said, the node I’m trying to scale to does have a size at the moment, so I’d guess it has been loaded.

Now, as only setting Scale manually does work, is there a property to set in order to position the diagram?

I cannot tell what is wrong in your app. Can you tell me how to reproduce the problem?

I’ve just included a button to test whether this is really not a loading issue and it seems it actually is. When I click the button, the DiagramPanel.CenterPart function does work. Is there an event triggered when absolutely everything has finished loading? As I’m working in a ViewModel I don’t have access to the View itself, so I’m unsure how to use BeginInvoke

Yes, the Diagram.InitialLayoutCompleted event, which I thought you were already using.

Yes, but that one didn’t work … so it seems it’s still too early

Were you calling BeginInvoke in the InitialLayoutCompleted event handler?

I was actually doing it like this, as I’m not that well versed in WPF

public MyViewModel() {
    this.Diagram.TemplateApplied += InitializeDiagramData();
}

private InitializeDiagramData(object sender, DiagramEventArgs eventArgs) {
    this.Diagram.InitialLayoutCompleted += ZoomToFit;
}

private void ZoomToFit(object sender, DiagramEventArgs eventArgs) {
    MyNodeData backgroundNode = this.Diagram.NodesSource.FirstOrDefault( ... );
    if (backgroundNode != null) {
        this.Diagram.Panel.Scale = Math.Min(
            this.Diagram.Panel.ViewportWidth / backgroundNode.Width,
            this.Diagram.Panel.ViewportHeight / backgroundNode.Height
        );
        this.Diagram.Panel.CenterPart(this.Diagram.Nodes.FirstOrDefault(node => node.Data == backgroundNode);
    }
}

I was suggesting that you call BeginInvoke in your InitialLayoutCompleted handler.

Tried it but still the same problem occurs. Now even ZoomToFit does scale correctly but just like CenterPart or SetScaleAndPosition it doesn’t place the position correctly.

What I don’t get is why SetScaleAndPosition doesn’t work. Doesn’t this function simply do a transition of the zero point to the one passed as argument?

Yes, it sets DiagramPanel.Position and DiagramPanel.Scale with optional animation.

However, if the user couldn’t scroll that far, trying to set the DiagramPanel.Position won’t work either. That’s because the DiagramPanel.DiagramBounds and the viewport’s bounds are such that the proposed position is beyond where it can be scrolled. Might that be the case in your situation?

This shouldn’t be any problem. Every other part is actually placed within the bounds of the background node, so the background node itself should mark the bounds of the diagram. In my test case, that is a square of about 600x600 px. The viewport has a size of about 1700x900px. So the diagram should be stretched by 1.5 and then centered horizontally (when I set the position to 400x0 px).

A little background information. We have a navigation with subsections, somewhat like this:

  • Apartments
  • Level 1
  • Level 2
  • Rooms
  • Level 1
  • Level 2
  • Components
  • Level 1
  • Level 2

Every top navigation level has its own View and its own Diagram, so when I switch from Apartments to Rooms, a new Diagram is initialized.

Each 2nd level navigation does call OnNavigatedTo but remains in the same View and keeps the same Diagram. Each 2nd level navigation point also claims one background layer, one node layer and optionally one link layer (we don’t need links in the apartment section, so there is none).

Each background layer contains a single node with this template:

<DataTemplate x:Key="BackgroundNodeTemplate">
    <go:NodePanel go:Part.LayerName="{Binding Path=Data.Layer.Id}"
                  go:Part.LayoutId="None"
                  go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
                  IsHitTestVisible="False"
                  go:Part.Visible="{Binding Path=Data.IsVisible}"
                  go:Node.RotationAngle="{Binding Path=Data.Angle}">
        <Image Source="{Binding Path=Data.GroundPlanImagePath}"
               Width="{Binding Path=Data.Width}"
               Height="{Binding Path=Data.Height}" />
    </go:NodePanel>
</DataTemplate>

Data.GroundPlanImagePath points to the location of a .png image file.

Now, when I switch the top level navigation, a new diagram is initialized, I’m waiting for the Diagram.InitialLayoutCompleted event and then set Diagram.Panel.Scale and Diagram.Panel.CenterPart(...). This seems to work so far.

When I switch the 2nd level navigation point, I set the Layer.Visibilty for the navigation point’s corresponding layer to Visible and for all others to Hidden in the OnNavigatedTo method and afterwards I try to scale and center again, which does not work. I suspect setting the visibility causes WPF to actually load the image from the file, which does take too much time and thus is not finished when I try to center so the only thing that happens is the Diagram automatically moving the background node into the Viewport’s bounds, but no centering.

In the latter case, is there an event I could hook up to? I tried LayoutCompleted but that only gets hooked the first time. Would it make sense to set the Layer.Visibility to Collapsed or set the background node’s visibility to get the LayoutCompleted event to fire?

You have set go:Part.LayoutId="None", so changing Node.Visible will not invalidate the Diagram.Layout, so that wouldn’t result in another Diagram.LayoutCompleted event.

Via a Binding you are setting the Width and Height of the Image, so changing the Source shouldn’t cause the background node to change size.

If you do not change the Layer.Visibility, but leave them all visible, do you have the same initialization problem?

I’ve just set the Visibility for each of the background nodes to always visible and opacity to 0.5 and now I can see the problem is caused by the diagram bounds. I’ve included two screenshots of the two layers in my test case. The green spot marks the zero point of the diagram.
Before stepping into the diagram, the user can define the layers, set their scale and set an alignment point so the ground plans can be aligned on a specific point. I’ve set that point to the center of Layer 2 and to the top left corner of layer 1. The background nodes’ locations are set so this alignment point resides in the zero point of the diagram. I’ve also set a widely different scale to each of them so their placement in relativity to the other background node is actually correct. The red border marks the Diagram’s Panel.

Layer 1 is looking good:

Background node of Layer 2 can’t be centered because of the diagram’s bounds.

Now, what can I do to actually get the background node of layer 2 to be centered? Is there a way to set the diagram’s bounds?

You can either set DiagramPanel.Padding or DiagramPanel.FixedBounds so that the DiagramPanel.DiagramBounds is large enough to allow each of your areas to be centered in the viewport.

Now, this is becoming increasingly mysterious. Im setting the DiagramPanel.FixedBounds to the bounds of the background node I’m switching to.

If I select the top level navigation point the centering after the Diagram.InitialLayoutCompleted works. If I switch immediately afterwards to the Level 2 navigation point it doesn’t work, the same if I then switch back to Level 1. If I use the DragSelectingTool before switching, it suddenly works when I switch. It doesn’t matter whether I actually select a node with the tool, it seems to be enough for the temporary rectangle to be drawn. I’ve also tried using the ClickCreatingTool and the ClickSelectingTool but they don’t make it work, only the DragSelectingTool.

I’ve also tried calculating the maximum bounds, so the background node I switch to is in the center of the bounds like this (the green spot is the zero point and the orange rectangle the calculated bounds):

I get the same result with this setting.

This is what the two layers look like if I do not use the DragSelectingTool before navigation:

And this is what they look like after using the DragSelectingTool:

That orange bounds that you calculated is not large enough to allow the green dot to be centered in the viewport, unless the scale changes to be zoomed in enough.

Instead of setting DiagramPanel.FixedBounds, try experimenting by setting DiagramPanel.Padding to maybe 1000 on all sides. See if that helps.

The orange bounds are calculated so the large background node is centered, not the small one, so the green dot is not supposed to be in the middle. The bounds for the small background node look differently.

If setting large FixedBounds or a Padding the user can always see a scrollbar with a huge empty space surrounding the background node. As any action with the diagram is supposed to occur on the background node and never outside of, this is undesirable. I would prefer to set the bounds to the background node itself, so there is no scrollbar on a zoom to fit.

Do you have any explanation why it works if I do a DragSelect and why it does not if I don’t?

Sorry, I misunderstood your intent.

Perhaps you need to make all of those modifications inside a transaction, to make sure everything is updated. That’s just a guess.