Popup sporadically flickers when setting margin (after animation)

mister.nobody 101 Reputation points
2020-05-06T16:21:18.013+00:00

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 Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,671 questions
{count} votes

Accepted answer
  1. mister.nobody 101 Reputation points
    2020-05-11T17:03:07.673+00:00

    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);
    }
    
    0 comments No comments

0 additional answers

Sort by: Most helpful