question

misternobody avatar image
misternobody asked ·

Popup sporadically flickers when setting margin (after animation)

I'm creating a drop-down button which consists of a ToggleButton and a ContextMenu. The menu opens when the button is clicked and is aligned with the button using a CustomPopupPlacementCallback.

I apply a customized animation to the menu when it opens, like this:

 private void ContextMenu_Opened(object sender, RoutedEventArgs e)
 {
     ContextMenu contextMenu = (ContextMenu)sender;

     NameScope.SetNameScope(this, new NameScope());
    
     TranslateTransform translation = new TranslateTransform();
     RegisterName("TranslateTransform", translation);
    
     contextMenu.RenderTransform = translation;

     DoubleAnimation translationAnimation = new DoubleAnimation()
     {
         From = -20,
         To = 0,
         Duration = new Duration(new TimeSpan(0, 0, 0, 0, 200)),
         EasingFunction = new PowerEase() { EasingMode = EasingMode.EaseOut, Power = 3 }
     };
    
     Storyboard.SetTargetProperty(translationAnimation, new PropertyPath(TranslateTransform.YProperty));
     Storyboard.SetTargetName(translationAnimation, "TranslateTransform");
    
     Storyboard storyboard = new Storyboard();
     storyboard.Children.Add(translationAnimation);
    
     storyboard.Begin(this);
 }

This works fine and makes the menu appear to "slide out of the button":

7991-dropdownbutton-01.gif


Side notes:

  • I'm using a custom animation because the default PopupAnimation.Slide doesn't meet my needs in terms of timing

  • I'm aware that I could also animate the ContextMenu.VerticalOffsetProperty, but this would make the menu overlap the button during the animation. Animating the RenderTransform, on the other hand, seems to be doing what I want.

Now I have added a dropshadow to the menu, which requires the ContextMenu to have a margin (otherwise the dropshadow would be clipped). This kind of breaks the alignment of the menu because it now overlaps the button during the animation (which I wanted to avoid in the first place):

7933-dropdownbutton-02.gif

So I've come up with the idea of removing the top margin from the menu during the animation and restoring it afterwards, like this:

 private void ContextMenu_Opened(object sender, RoutedEventArgs e)
 {
     Thickness originalMargin = contextMenu.Margin;
     contextMenu.Margin = new Thickness(
         contextMenu.Margin.Left, 0, contextMenu.Margin.Right, contextMenu.Margin.Bottom);
    
     /* Prepare animation as shown before... */
    
     storyboard.Completed += new EventHandler((animation, eventArgs) =>
     {
         contextMenu.Margin = originalMargin; 
     });
    
     storyboard.Begin(this);
 }

This works as intended most of the time - the popup is aligned perfectly during the animation and the dropshadow becomes visible at the top after the animation has finished. However, when the margin is being restored, the popup sometimes moves out of place very briefly - the example below shows a "good" run followed by a "bad" run:

7954-dropdownbutton-03.gif

I'm aware that changing the Margin property of the ContextMenu will cause the layout system to re-evaluate the placement of the popup. But I don't know why sometimes the menu is immediately re-rendered before the placement is updated.

Is there anything I can do about this issue?

Thank you very much in advance!

Edit: A ready-to-run Visual Studio solution is available at github.com/MisterNobody123/DropDownButtonDemo


windows-wpf
2 comments
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Do you mean that your program has an abnormal jitter?

0 Votes 0 · ·

Hello,
I believe the problem is that when I modify the Margin property or HorizontalOffset/VerticalOffset properties, the menu is sometimes immediately rendered with the updated margin or offset without consulting the CustomPopupPlacementCallback first, i.e. the new values take effect before the layout system has even evaluated whether the new position is valid.
The rest of the program is running fine.

0 Votes 0 · ·

1 Answer

misternobody avatar image
misternobody answered ·

Okay, I think I figured out a workaround.

I'm leaving the Margin property untouched throughout the entire process. This avoids the original problem but, as shown before, makes the menu overlap the button (see second animation in my original question).

Now I have to compensate for the unwanted offset, but I can't use the VerticalOffset property (because removing it after the animation has finished will again result in sporadic flicker). Instead, I use a clipping geometry and animate it so that it always clips the part of the menu which overlaps the button:

 private void ContextMenu_Opened(object sender, RoutedEventArgs e)
 {
     /* Prepare translation animation as shown before... */
    
     RectangleGeometry clipGeometry = new RectangleGeometry(new Rect(
         new Point(-contextMenu.Margin.Left, 0),
         new Size(contextMenu.ActualWidth + contextMenu.Margin.Left + contextMenu.Margin.Right,
             contextMenu.ActualHeight + contextMenu.Margin.Bottom)));
     contextMenu.RegisterName("RectangleGeometry", clipGeometry);
    
     contextMenu.Clip = clipGeometry;
    
     RectAnimation clippingAnimation = new RectAnimation()
     {
         Duration = new Duration(new TimeSpan(0, 0, 0, 0, 200)),
         EasingFunction = new PowerEase() { EasingMode = EasingMode.EaseOut, Power = 3 },
         From = new Rect(
             new Point(-contextMenu.Margin.Left, 20),
             new Size(contextMenu.ActualWidth + contextMenu.Margin.Left + contextMenu.Margin.Right,
                 contextMenu.ActualHeight - 20 + contextMenu.Margin.Bottom))
     };
    
     SetTargetProperty(clippingAnimation, new PropertyPath(RectangleGeometry.RectProperty));
     SetTargetName(clippingAnimation, "RectangleGeometry");
    
     storyboard.Children.Add(clippingAnimation);
    
     storyboard.Completed += new EventHandler((animation, eventArgs) =>
     {
         contextMenu.RenderTransform = null;
         contextMenu.Clip = null; 
     });
    
     storyboard.Begin(this);
 }
Share
10 |1000 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.