开发者

How to avoid recursive triggering of events in WPF?

开发者 https://www.devze.com 2023-01-13 18:54 出处:网络
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.

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/ScrollSynchroniza开发者_JAVA技巧tion.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.


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.


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.


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


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.


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);
            }
        }


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.


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
0

精彩评论

暂无评论...
验证码 换一张
取 消