MakeBitmap changes the ViewportBounds of the diagram

Hello Walter.

In my application from the recent ticket Zoom to and center a specific node I’m creating a bitmap from the final diagram.

As a small recap:

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).

When I navigate to one of the levels I’m doing a zoom to fit on its background node by setting FixedDiagramBounds to the size of this node.

When I navigate away from a level in the Components section I’m creating a bitmap of that one Level in the diagram like this:

BitmapSource floorPlanWithComponents = diagram.Panel?.MakeBitmap(
    new Size(diagram.Panel.DiagramBounds.Width, diagram.Panel.DiagramBounds.Height),
    96,
    new Point(diagram.Panel.DiagramBounds.X, diagram.Panel.DiagramBounds.Y),
    1
);

The call to the Panel.MakeBitmap function changes the Panel.ViewportBounds of the diagram. So, when I navigate from Level 1 to Level 2 in the Components section I’m creating a bitmap of Level 1 and then try to scale to the background node in Level 2. To calculate the scaling factor I need to use the ViewportWidth and ViewportHeight but both are incorrect now and thus my scaling is off.

In my understanding this is an error as the ViewportBounds should not be changed. As a workaround my first thought was to save the ViewportBounds and reset them after the call to MakeBitmap but since multiple other things change (like ViewportWidth, ActualSize, RenderSize, etc.) I’m unsure what to actually set to restore the Panel’s bounds to the correct value.

It has to be able to (pretend) scroll through the diagram in order to render the requested area of the diagram, given by the viewpos parameter.

It does not modify the DiagramPanel.Position and DiagramPanel.Scale properties, as far as I can tell. It does try to make sure everything is back to normal well after the call to the optional act parameter.

But I suggest that in that act parameter Action that you reset the DiagramPanel.Position and DiagramPanel.Scale to be the values that you want, to make sure.

No, it doesn’t modify the Position and Scale. I’m calculating the Scale in my OnNavigatedTo method using the ViewportWidth and ViewportHeight.

  1. Navigate to Components - Level 1
  • The first background node has the bounds (-300, -300, 600, 600)
  • The ViewportWidth and ViewportHeight are (1707, 909)
  1. Scale is calculated using

     Math.Min(
         this.Diagram.Panel.ViewportWidth / (backgroundNode?.Width ?? 1),
         this.Diagram.Panel.ViewportHeight / (backgroundNode?.Height ?? 1)
     );
    
    • Result is ~1.5
  2. Navigate to Components - Level 2

  • In OnNavigatedFrom MakeBitmap is called for Level 1
  • ViewportWidth and ViewportHeight afterwards are (600, 600)
  1. In OnNavigatedTo Scale is calculated with new ViewportWidth and ViewportHeight
  • Result is ~0.167
  • Result with correct viewport dimensions (the ones from step 1.) is ~0.25
  • The second background node has the bounds (0, 0, 3596, 3596)
  1. Navigate back to Level 1
  • ViewportWidth and ViewportHeight after MakeBitmap for Level 2 are (3596, 3596)
  1. Scale is calculated with new ViewportBounds
  • Result is now ~5.99
  • Result should still be ~1.5

I cannot just reset the DiagramPanel.Position and DiagramPanel.Scale, I have to calculate them and the parameters used for the calculations are off. I can totally understand the changes in the viewport dimensions in order to generate the Bitmap, but those changes should absolutely not be reflected outside of the DiagramPanel.MakeBitmap method. The viewport dimensions in my application haven’t changed, so the Diagram’s viewport dimensions shouldn’t change either. They should be reset within the MakeBitmap function. The main problem is that I cannot set them outside of the DiagramPanel class as they have no public setter. I also cannot override the DiagramPanel.MakeBitmap method as it’s not virtual. So, how do I get the correct, initial ViewportWidth and ViewportHeight?

Just to be clear, are you calling MakeBitmap during your method that sets DiagramPanel.Position and .Scale? And doing so messes up the values of DiagramPanel.ViewportWidth and .ViewportHeight?

If so, my guess is that because MakeBitmap is somewhat asynchronous, you need to do everything that you want in the Action passed to MakeBitmap.

No. As said, I’m calling MakeBitmap in the OnNavigatedFrom method and calculate Position and Scale in the OnNavigatedTo method. What do you mean by somewhat asynchronous?

Oh. What is the time relationship of those two method calls, and are they called synchronously?

It might work if you call OnNavigatedTo asynchronously in the Action of MakeBitmap.

I’m not calling them myself and I have no influence on their calling. We are using Prism and they get called automatically upon navigating from one view to another.

The thing is, I’m calling MakeBitmap and it returns a result, so I assume it’s finished. It’s not actually asynchronous so I can’t use await for it to be finished. I did try to pass a callback action as argument and found out that OnNavigatedTo indeed gets called before the callback action. Within the callback the ViewportWidth and ViewportHeight were back to their initial values but how am I supposed to handle this? I’d say the design is flawed and needs fixing but that’s a different matter. In any case I need a solution to my problem.

I’m glad that you confirmed that the property values are correct in the Action after the DiagramPanel.MakeBitmap call.

The DiagramPanel.ViewportWidth property depends on the DiagramPanel having been remeasured and rearranged, so that’s why it’s not valid immediately after MakeBitmap returns.

And before you ask, I should say that it’s essential that MakeBitmap remeasure and rearrange everything in order to do its rendering correctly.

It seems to me that you can delay your computation and setting of DiagramPanel.Scale and DiagramPanel.Position by doing all that in an Action called by myDiagram.Dispatcher.BeginInvoke.

Yes, I already mentioned I unserstand that a recalculation of the ViewportBounds is necessary in order to create the Bitmap properly but it still doesn’t make sense to me that this function is not asynchronous itself, or even better yet, that there’s not an asynchronous and synchronous version of it with the latter actually not returning until the method is fully finished and the temporary viewport changes have been reverted. This is not a good design and should be changed.

Anyway, I’ll try your suggestion and get back to you once done. I imagine, though, the user is taken to the different level, sees the off-scale and off-position of the diagram, is displeased but starts to work with it anyway until he finally gets the repositioning and rescaling thrown in. He probably can’t do much in the meantime as I don’t think it takes up that much time but I assume our customer will complain about this. We’ll see, let’s hope it works well enough.

Well, the overloaded method that takes an Action is the “asynchronous” version.

Remember that the GoXam and WPF APIs were designed a long time ago for .NET 3.5 and Silverlight 3, long before async/await existed. (And before Silverlight disappeared.)

I see.

So. I tried using Dispatcher.Invoke but it still gets called before MakeBitmap calls the callback method.

public override void OnNavigatedTo(NavigationContext navigationContext)
{
    this.Diagram.Dispatcher.Invoke(() => {
        // Get the current background node
        GroundPlanNodeData backgroundNode = ...

        this.Diagram.Panel.FixedBounds = new Rect(
            new Point(backgroundNode?.Location.X ?? 0, backgroundNode?.Location.Y ?? 0),
            new Size(backgroundNode?.Width ?? this.Diagram.Panel.ViewportWidth, backgroundNode?.Height ?? this.Diagram.Panel.ViewportHeight)
        );
        this.Diagram.Panel.Scale = Math.Min(
            this.Diagram.Panel.ViewportWidth / (backgroundNode?.Width ?? 1),
            this.Diagram.Panel.ViewportHeight / (backgroundNode?.Height ?? 1)
        );
        this.Diagram.Panel.HorizontalContentAlignment = HorizontalAlignment.Center;
        this.Diagram.Panel.VerticalContentAlignment = VerticalAlignment.Center;
    });
}

Try using DispatcherPriority.Background.

This throws an InvalidOperationException saying “Cannot perform this operation while dispatcher processing is suspended.”

What!? That’s surprising. The implementation of Diagram.MakeBitmap does that, using DispatcherPriority.Background, to call the Action afterwards.

So I cannot explain that.

So … what do I do now?

Create a minimal example that demonstrates the problem, so that I can experiment with it. Start with the Minimal sample and add your code to set the viewport and call MakeBitmap.

As this includes Prism, multiple views and a navigation with a dynamic setup it would require too much time to single out the problem.

Instead I’ve rewritten the application so that my ZoomToFit function in the view model is now public, I pass the view model instead of the diagram to the function that calls MakeBitmap and afterwards in the callback I call the ZoomToFit on the view model. I consider this to be horrible design but it works so far. Thank you for your help.