How to avoid recursive event triggering in WPF?

I have two WPFs (from the standard set) of widgets A and B. When I change a property of A, it must be set to B, when it changes to B, it must be set to A.

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

How to avoid this recursion the most "standard" way? Naively deleting and adding event handlers does not work, and checking that the new value matches the old value is not applicable here (due to fluctuation in the calculation, I do not set the same value for A and B, but convert).

Background

I always try to put minimal information about the problem in order to avoid confusion. However this may help

  • I did not write these widgets, I just process events that everything
  • despite the name "recursive start", the handlers are called sequentially, so you have the sequence entry-exit-entry-exit-entry-exit, not entry-entry-entry-entry-exit-exit-exit

    and the last, perhaps the least important, but nonetheless

  • in this particular case, I have a regular 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 (Karin Huber) is here: http://www.codeproject.com/KB/WPF/ScrollSynchronization.aspx

Trigger event

The idea of โ€‹โ€‹blocking events is so popular that I added an event trigger sequence, here we go:

  • I change A
  • Handler called
  • Disable Handler A
  • I change B (this is saved but not starting)
  • I turn on handler A
  • now the event gets from the queue
  • B-handler called
  • Disable Handler B
  • I change A
  • ...

As you can see, this is useless.

+7
c # events recursion wpf
source share
7 answers

Instead of raising events, reorganize 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(); } 

Then you can extend / change these methods if you need to do subtly different things if you change A directly or through B.

UPDATE

Given that you cannot change / not have access to the code, this is not a solution.

+2
source share

First of all, I would think about design, because these circular dependencies are often a sign of poor design.

However, there may be situations where such dependencies are the only way. In this case, I would suggest using private flags indicating whether the change in B was caused by the change of 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 can be changed from multiple threads at the same time, you will have to use appropriate methods to avoid losing properties.

+2
source share

ChrisFs solution is probably the way to go, but sometimes we delete the event, make a change and add the event handler again:

Imagine a DataContext with a DataContextChanged event:

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

That way, you know for sure that your event handler won't say that. The event is still triggered, but).

0
source share

and checking that the new value matches the old value is not applicable here (due to fluctuation in the calculation - I do not set the same value in A and B, but it is converted).

So you say something like this happens:

  • A.Foo gets the value of x .
  • A_FooChanged sets B.Bar to f ( x ).
  • B_BarChanged sets A.Foo to g (f ( x )), which is not x .
  • A_FooChanged sets B.Bar to f (g (f ( x ( x ))).

etc. It's right? Since g (f ( x )) x , then a simple solution: B_BarChanged should only set A.Foo if A.Foo ! = G (f ( x ).

Assuming that this recursion does not have a calculated final state, then the event handlers must somehow find out the context in which the events that they process were fired. You cannot get this information from the usual event protocol, because events are designed to decouple the operations that this project links.

It looks like you need an out-of-band way for these controls to signal each other. It can be as simple as using the HashSet<EventHandler> Window property. I would think 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 if A installs B.Bar , and B installs C.Baz , and C installs A.Foo , although I suspect the requirements themselves will break if this happens. In this case, you probably have to resort to viewing the stack trace. This is ugly, but then nothing about this problem is pretty.

0
source share

A bit of a hacky solution is to set a timeout period during which you ignore the subsequent arrows of the event handler. If you do, use DateTime.UtcNow rather than DateTime.Now to avoid the daylight saving time boundary condition.

If you don't need a time dependency (events can legitimately be updated quickly), you can use a similar type of lock, although this is even more hacky:

  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); } } 
0
source share

Since the positions are scaled or otherwise changed between ScrollViewers, you cannot use simple snapping, but does the 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> 

where you can implement any math needed to scale between 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 using ScrollViewers, but since the scrollbars that are part of the ScrollViewer template and sliders go down from RangeBase, something like this should work, but you may have to re-create the template and / or subclass ScrollViewers.

0
source share

I am not familiar with WPF controls, so the following solutions may not apply:

  • updated only if the new value does not match the old value. You have already stated that this does not apply to your case, since the values โ€‹โ€‹are always slightly off.
  • to crack the Boolean flag, notifying that prevents recursion, if already in the notification. The disadvantage may be that some notifications are skipped (i.e., Clients are not updated), and that this flag does not work if notifications are sent (instead of a direct call).
  • Windows controls distinguish between user actions that push events and set data programmatically. The latter category does not notify.
  • using the Observer (or pick) pattern, in which the controls do not update each other directly
0
source share

All Articles