How to avoid recursive triggering of events in WPF?

杀马特。学长 韩版系。学妹 提交于 2021-02-16 10:53:06

问题


I am having two WPF (from the standard set) widgets A and B. When I change some property of A it should be set on B, when it is change in B it should be set on A.

Now I have this ugly recursion --> I change A, so code changes B, but since B is changed, it changes A, so it changes B... You have the picture.

How to avoid this recursion the most "standard" way? Naive deleting and adding event handlers does not work, and checking if the new value is the same as old value is not applicable here (because of the fluctuation of calculation -- I am not setting the same value to A and B, but transformed).

Background

I always try to put minimum info about the problem to avoid confusion. However, this might help

  • I didn't write those widgets, I just handle the events, that's all
  • despite the title "recursive triggering", the handlers are called sequentially, so you have the sequence entry-exit-entry-exit-entry-exit, not entry-entry-entry-exit-exit-exit

    and the last, probably the least important, but nevertheless

  • in this particular case I have common handler for A and B

A and B (in this case) are scrollviewers and I try to maintain proportionally the same position for both of them. The project (by Karin Huber) is here: http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

Event triggering

The idea of blocking the events is so popular that I added the sequence of triggering the events, here we go:

  • I change the A
  • A handler is called
  • I disable handler of A
  • I change B (this is stored, but not triggered)
  • I enable handler of A
  • now the event is get from the queue
  • B handler is called
  • I disable handler of B
  • I change A
  • ...

As you see, this is futile.


回答1:


First of all, I would think about the design because those circular dependencies are often a sign of bad design.

However, there might be situations where such dependencies are the only way to go. In these case, I would suggest to use private flags indicating whether a change in B was caused by a change in A. Something like this (updated):

public class A
{
    private bool m_ignoreChangesInB = false;

    private void B_ChangeOccurred(object sender, EventArgs e)
    {
        if (!m_ignoreChangesInB)
        {
            // handle the changes...
        }
    }

    private void SomeMethodThatChangesB()
    {
        m_ignoreChangesInB = true;
        // perform changes in B...
        m_ignoreChangesInB = false;
    }
}

The same approach should be used in class B. However, this approach does not handle changes from multiple threads. If A or B might be changed from multiple threads at the same time, you will have to use appropriate techniques to avoid that property changes are lost.




回答2:


Rather than raising events, refactor your code so that the event handlers for A and B call another method to do the actual work.

private void EventHandlerA(object sender, EventArgs e)
{
    ChangeA();
    ChangeB();
}

private void EventHandlerB(object sender, EventArgs e)
{
    ChangeB();
    ChangeA();
}

You could then extend/change these methods if you need to do subtly different things if changing A directly or via B.

UPDATE

Given that you can't change/don't have access to the code this isn't the solution.




回答3:


ChrisFs solution is probably the way to go, but sometimes we remove the event, make the change, and re-add the event handler:

Imagine a DataContext, with the DataContextChanged event:

DataContextChanged -= OnDataContextChanged;
DataContext = new object();
DataContextChanged += OnDataContextChanged;

This way, you will know for sure your event handler will not go off so to speak. The event still fires however ;).




回答4:


and checking if the new value is the same as old value is not applicable here (because of the fluctuation of calculation -- I am not setting the same value to A and B, but transformed).

So what you're saying is that something like this happens:

  1. A.Foo gets set to x.
  2. A_FooChanged sets B.Bar to f(x).
  3. B_BarChanged sets A.Foo to g(f(x)), which is not x.
  4. A_FooChanged sets B.Bar to f(g(f(x))).

and so on. Is this correct? Because if g(f(x)) is x, then the solution's simple: B_BarChanged should only set A.Foo if A.Foo != g(f(x).

Assuming that this recursion has no calculable end state, then the event handlers need some way to know the context in which the events they're handling were triggered. You can't get that information from the normal event protocol, because events are designed to decouple operations that this design is coupling.

It sounds like you need an out-of-band way for these controls to signal to each other. It could be as simple as using a HashSet<EventHandler> that's a property of the Window. I'd consider something like this:

private void A_FooChanged(object sender, EventArgs e)
{
   if (!SignalSet.Contains(B_BarChanged))
   {
      SignalSet.Add(A_FooChanged);
      B.Bar = f(A.Foo);
      SignalSet.Remove(A_FooChanged);
   }
}

This breaks down if A sets B.Bar, and B sets C.Baz, and C sets A.Foo, though I suspect that the requirements themselves break down if that happens. In that case, you probably have to resort to looking at a stack trace. That's not pretty, but then nothing about this problem is pretty.




回答5:


A slightly hackish solution is to set a blackout time period during which you ignore subsequent event handler firings. If you do this, be sure to use DateTime.UtcNow rather than DateTime.Now to avoid a daylight savings boundary condition.

If you don't want a dependency on timing (the events may legitimately update quickly), you can use a similar type of blocking, though this is even more hackish:

        private int _handlerCounter;
        private void Handler()
        {
            //The logic of this handler will trigger an 'asynchronously reentrant' callback, so we ignore the next (and only the next) callback
            //Note that this breaks down if the callback is not triggered, so we need to make certain the reentrancy will occur
            //If we can't ensure that, we need to at least detect that it won't occur and manually decrement the counter
            if (Interlocked.CompareExchange(ref _handlerCounter, 0, 1) == 0)
            {
                //Call set on A/B, which triggers callback of this same handler for B/A
            }
            else
            {
                Interlocked.Decrement(ref _handlerCounter);
            }
        }



回答6:


Because the positions are scaled, or otherwise changed between the ScrollViewers, you can't use a simple binding, but would a Converter work?

<Window x:Class="Application1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SurfaceApplication21"
    Title="SurfaceApplication21"
    >
    <Window.Resources>
        <local:InvertDoubleConverter x:Key="idc" />
    </Window.Resources>
  <Grid>
        <StackPanel>
            <Slider Minimum="-100" Maximum="100" Name="a" Height="23" HorizontalAlignment="Left" Margin="30,12,0,0" VerticalAlignment="Top" Width="100" />
            <Slider Minimum="-100" Maximum="100" Value="{Binding ElementName=a, Path=Value, Converter={StaticResource idc}}" Name="b" Height="23" HorizontalAlignment="Left" Margin="30,12,0,0" VerticalAlignment="Top" Width="100" />
        </StackPanel>
    </Grid>
</Window>

code where you could implement whatever math is needed to scale between the two views.

[ValueConversion(typeof(double), typeof(double))]
public class InvertDoubleConverter : IValueConverter
{

    public object Convert(object value, Type targetType,
        object parameter, System.Globalization.CultureInfo ci)
    {
        return -(double)value;
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, System.Globalization.CultureInfo ci)
    {
        return -(double)value;
    }
}

I have not tried with ScrollViewers, but since the Scrollbars that are part of the ScrollViewer template and Sliders both descend from RangeBase, something like this should work, but you may have to re-template and/or subclass your ScrollViewers.




回答7:


I am no familiar with WPF controls, so the following solutions might not applicable:

  • update only if the new value is not the same as the old value. You already stated that this doesn't apply to your case since the values are always slightly off.
  • hack of keeping a Boolean flag while notifying which prevents recursing if already in a notify. The drawback might that some notifications are missed (i.e. clients are not updated), and that this flag does not work if notifications are posted (instead of directly called).
  • windows controls make a distinction between user actions which raise events and setting data programmatically. The last category does not notify.
  • using Observer (or Mediator) pattern where controls do not update each other directly


来源:https://stackoverflow.com/questions/3574715/how-to-avoid-recursive-triggering-of-events-in-wpf

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!