I have a Textblock over which I'd like to open a Popup when the mouse is hovering above it. I have binded the IsOpen property using MultiBinding to Popup's IsMouseOver and to the TextBlock's IsMouseOver, and it works, except when the mouse is moved from the text to popup, the popup flickers.
The cause of flickering is the order of execution of events under the hood:
Mouse moves from textblock to popup-->IsMouseOver of textblock is set to false --> converter is called with both parameters being false --> only then IsMouseOver of popup is set to true --> converter is executed with both parameters being false, popup disappears --> Converter called and executed again because another event was raised for IsMouseOver of popup earlier, this time IsMouseOver of Popup True  --> popup appears again. I have tried adding StaysOpen=False, but then it never closes/behaves differently than expected.
Question: how do I avoid the flickering?
Code:
<Grid>
    <ListBox ItemsSource="{Binding RandomNames}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition></ColumnDefinition>
                        <ColumnDefinition></ColumnDefinition>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Name: "
                               Grid.Column="0"/>
                    <TextBlock Grid.Column="1"
                               x:Name="NameBlock"
                               Text="{Binding}">
                        <TextBlock.Style>
                            <Style TargetType="TextBlock">
                                <Style.Triggers>
                                    <Trigger Property="IsMouseOver" Value="True">
                                        <Setter Property="Foreground" Value="Red" />
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </TextBlock.Style>
                    </TextBlock>
                    <Popup x:Name="PopupX"
                           Grid.Column="1"
                           PlacementTarget="{Binding ElementName=NameBlock}"
                           Placement="Bottom">
                        <!--<Popup.IsOpen>
                            <MultiBinding Converter="{StaticResource PopupIsOpenConverter}">
                                <Binding ElementName="PopupX" Path="IsMouseOver" Mode="OneWay" />
                                <Binding ElementName="NameBlock" Path="IsMouseOver" Mode="OneWay" />
                            </MultiBinding>
                        </Popup.IsOpen>-->
                        <Popup.Style>
                            <Style TargetType="Popup">
                                <Setter Property="IsOpen" Value="True" />
                                <Style.Triggers>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding IsMouseOver, ElementName=NameBlock}" Value="False" />
                                            <Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="IsOpen" Value="False" />
                                    </MultiDataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Popup.Style>
                        <TextBlock Text="{Binding}"
                                   Foreground="Coral" />
                    </Popup>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>
Converter code
[ValueConversion(typeof(bool), typeof(bool))]
public class PopupIsOpenConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values.Any(value => value is bool && (bool) value);
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new ActionNotSupportedException();
    }
}
                Thanks to this post, I was able to solve the problem using delayed multibinding. Note that the multibinding converter is universal and can accept any regular multibinding converter plus the delay.
My XAML:
<Popup.IsOpen>
    <local:DelayedMultiBindingExtension Converter="{StaticResource PopupIsOpenConverter}" Delay="0:0:0.01">
        <Binding ElementName="PopupX" Path="IsMouseOver" Mode="OneWay" />
        <Binding ElementName="RecipientsTextBlock" Path="IsMouseOver" Mode="OneWay" />
    </local:DelayedMultiBindingExtension>
</Popup.IsOpen>
My multibinding converter:
[ContentProperty("Bindings")]
internal sealed class DelayedMultiBindingExtension : MarkupExtension, IMultiValueConverter, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public Collection<BindingBase> Bindings { get; }
    public IMultiValueConverter Converter { get; set; }
    public object ConverterParameter { get; set; }
    public CultureInfo ConverterCulture { get; set; }
    public BindingMode Mode { get; set; }
    public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
    private object _undelayedValue;
    private object _delayedValue;
    private DispatcherTimer _timer;
    public object CurrentValue
    {
        get { return _delayedValue; }
        set
        {
            _delayedValue = _undelayedValue = value;
            _timer.Stop();
        }
    }
    public int ChangeCount { get; private set; } // Public so Binding can bind to it
    public TimeSpan Delay
    {
        get { return _timer.Interval; }
        set { _timer.Interval = value; }
    }
    public DelayedMultiBindingExtension()
    {
        this.Bindings = new Collection<BindingBase>();
        _timer = new DispatcherTimer();
        _timer.Tick += Timer_Tick;
        _timer.Interval = TimeSpan.FromMilliseconds(10);
    }
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var valueProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (valueProvider == null) return null;
        var bindingTarget = valueProvider.TargetObject as DependencyObject;
        var bindingProperty = valueProvider.TargetProperty as DependencyProperty;
        var multi = new MultiBinding { Converter = this, Mode = Mode, UpdateSourceTrigger = UpdateSourceTrigger };
        foreach (var binding in Bindings) multi.Bindings.Add(binding);
        multi.Bindings.Add(new Binding("ChangeCount") { Source = this, Mode = BindingMode.OneWay });
        var bindingExpression = BindingOperations.SetBinding(bindingTarget, bindingProperty, multi);
        return bindingTarget.GetValue(bindingProperty);
    }
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var newValue = Converter.Convert(values.Take(values.Length - 1).ToArray(),
                                         targetType,
                                         ConverterParameter,
                                         ConverterCulture ?? culture);
        if (Equals(newValue, _undelayedValue)) return _delayedValue;
        _undelayedValue = newValue;
        _timer.Stop();
        _timer.Start();
        return _delayedValue;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return Converter.ConvertBack(value, targetTypes, ConverterParameter, ConverterCulture ?? culture)
                        .Concat(new object[] { ChangeCount }).ToArray();
    }
    private void Timer_Tick(object sender, EventArgs e)
    {
        _timer.Stop();
        _delayedValue = _undelayedValue;
        ChangeCount++;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ChangeCount)));
    }
}
                        I stumbled over the same flicker problem, and was tempted to use your solution, but searched for something more lightweight first.
I solved it going another way (which I usually avoid like the plague): code behind. In this case, of just having a popup being open / closed depending on MouseOver over several controls, with no change to the model, this is OK though imho.
Here my solution:
public class CodebehindOfSomeView
{
    private readonly DispatcherTimer m_ClosePopupTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1) };
    public CodebehindOfSomeView()
    {
        InitializeComponent();
        m_ClosePopupTimer.Tick += ClosePopupTimer_Tick;
    }
    private void ClosePopupTimer_Tick(object _sender, EventArgs _e)
    {
        SomePopup.IsOpen = false;
        m_ClosePopupTimer.Stop();
    }
    private void PopupMouseOverControls_MouseEnter(object _sender, System.Windows.Input.MouseEventArgs _e)
    {
        m_ClosePopupTimer.Stop();
        SomePopup.IsOpen = true;
    }
    private void PopupMouseOverControls_MouseLeave(object _sender, System.Windows.Input.MouseEventArgs _e)
    {
        m_ClosePopupTimer.Start();
    }
}
No converter is used. In the View, just add PopupMouseOverControls_MouseEnter and PopupMouseOverControls_MouseLeave to the MouseEnter and MouseLeave Events of every desired control. Thats it.
A timespan of one millisecond is actually enough to thoroughly get rid of the flickering, if the controls touch each other.
If you would like to give the user a little time to move the mouse from one control to another (over pixels of other controls), just raise the timespan.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With