7
votes

Comment éviter le déclenchement récursif d'événements dans WPF?

J'ai deux widgets WPF (à partir de la standard) Widgets A et B. Lorsque je modifie certaines biens d'une propriété, il doit être défini sur B, lorsqu'il est modifié dans B, il doit être défini sur un.

Maintenant, j'ai cette nouvelle récursion laid -> Je change A, alors changements de code B, mais depuis B est modifié, cela change A, il change donc de ... vous avez la photo.

Comment éviter cette récursive la méthode la plus "standard"? La suppression naïve et l'ajout de gestionnaires d'événements ne fonctionne pas et ne vérifie pas si la nouvelle valeur est identique à celle de la valeur ancienne n'est pas applicable ici (en raison de la fluctuation du calcul - je ne définit pas la même valeur à A et B, mais transformée) .

Fond

J'essaie toujours de mettre des informations minimales sur le problème pour éviter toute confusion. Cependant, cela pourrait aider

  • Je n'ai pas écrit ces widgets, je supporte juste les événements, c'est tout
  • malgré le titre "déclenchement récursif", les gestionnaires sont appelés séquentiellement, de sorte que vous disposez de la séquence d'entrée-sortie-sortie-sortie-sortie, pas d'entrée-entrée-entrée-entrée-EXIT-EXIT-EXIT-EXIT-EXIT-EXIT-EXIT / p>

    et le dernier, probablement le moins important, mais néanmoins

  • Dans ce cas particulier, j'ai un gestionnaire commun pour A et B

    A et B (dans ce cas) sont des valeurs de défilement et j'essaie de maintenir proportionnellement la même position pour les deux. Le projet (de Karin Huber) est ici: http://www.codeproject.com/kb/wpf/scrollsynchronization.aspx

    Déclenchement d'événement

    L'idée de bloquer les événements est si populaire que j'ai ajouté la séquence de déclencher les événements, nous allons ici:

    • Je change le A
    • Un gestionnaire est appelé
    • Je désactive le gestionnaire d'un
    • i change b (ceci est stocké, mais pas déclenché)
    • Je active le gestionnaire d'un
    • L'événement est maintenant issu de la file d'attente
    • B HANDER est appelé
    • Je désactive le gestionnaire de b
    • je change un
    • ...

      Comme vous le voyez, c'est inutile.


0 commentaires

7 Réponses :


2
votes

plutôt que d'augmenter les événements, refacteur votre code de sorte que les gestionnaires d'événements pour A et B appellent une autre méthode pour effectuer le travail réel.

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

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


3 commentaires

Il n'est pas faisable - il s'agit de WPF, A et B sont des widgets et les widgets déclenchent en interne.


Mais les événements sont accrochés aux manipulateurs d'événements et vous pouvez sûrement influencer ce qui se passe à l'intérieur de ces , ne pouvez-vous pas?


Chrisien, bien sûr, mais je ne peux que appeler ce qui est exposé par designer du widget. Donc, une fois que je change, il déclenche l'événement, et cela déclenche mes manutentionnaires ... et nous avons "récursivité" (peut-être que je devrais mettre cela en devise à l'accent, ce n'est pas une vraie récursive).



3
votes

Tout d'abord, je penserais à la conception parce que ces dépendances circulaires sont souvent un signe de mauvaise conception.

Cependant, il pourrait y avoir des situations dans lesquelles de telles dépendances sont la seule façon d'aller. Dans ces cas, je suggère d'utiliser des drapeaux privés indiquant si un changement de B a été causé par un changement de A. Quelque chose comme ceci ( mis à jour ): xxx

La même approche doit être utilisée dans la classe B. Toutefois, cette approche ne traite pas les variations de plusieurs threads. Si A ou B pourrait être changé de multiples threads en même temps, vous devrez utiliser des techniques appropriées pour éviter que les modifications de propriété soient perdues.


6 commentaires

J'ai ajouté l'accent sur le fait que ce sont les widgets WPF, pas la mienne - c'est-à-dire de l'ensemble standard et je viens de les utiliser. Seuls les gestionnaires d'événements sont à moi, le reste n'est pas personnalisé WPF. Et cela ne fonctionne pas, car le déclenchement récursif n'est que notre illusion - les événements sont traités de manière séquentielle.


Cela n'a pas d'importance. En fin de compte, les situations problématiques sont lorsque votre code change les propriétés. Peut-être que mon exemple de code ne correspond pas à cette situation complètement. Mais l'idée d'avoir un drapeau indiquant que le changement de valeur a été initié par votre code fonctionnerait toujours.


Je ne travaille pas - voir la "trace" de la manière dont les événements sont traités. Cela fonctionnerait bien, si les événements seraient traités immédiatement juste après le changement. Mais ils ne le sont pas, votre drapeau est restauré, puis le gestionnaire est encore appelé.


Ok, maintenant je vois le point. Savez-vous pourquoi les gestionnaires ne sont pas appelés immédiatement? Le changement de propriété est-il exécuté sur un autre fil?


Oui, ces propriétés sont de widgets WPF, ce qui est un fil spécifique. D'ailleurs. Autant de contournement, il serait possible de stocker l'info que les événements doivent être bloqués (indépendamment du WPF). Ainsi, lorsqu'une modifie B, elle doit ajouter à "Liste ignorée" B, et lorsque le gestionnaire B est exécuté, il devrait se retirer de cette liste. Donc, l'appelant s'ajoute toujours, mais enlevez tous les autres.


Oui, pensé à quelque chose de similaire. Cependant, cela ne se sent pas comme une solution propre pour moi. Il semble que cela puisse devenir rapidement problématique ...



0
votes

La solution Chrisfs est probablement la voie à suivre, mais parfois, nous supprimons l'événement, apportons le changement et ajoutons le gestionnaire d'événements:

Imaginez un DataContext, avec l'événement DataContextChanged: P>

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


5 commentaires

J'ai déjà écrit - cela ne fonctionne pas. Les événements sont tirés de manière séquentielle, la récursion n'est qu'une illusion, car les événements sont mis en file d'attente.


Pourquoi cela ne fonctionne-t-il pas? Si vous savez B, vous invoquerez A, vous pourriez décomposer un dans B, faites vos affaires et ReHook A.


Parce que les gestionnaires d'événements ne sont pas ré-entrants. Ils sont traités dans la séquence - j'ai ajouté l'exemple dans le poste maintenant.


Je ne vous ai pas suggéré de supprimer le gestionnaire d'un dans le gestionnaire A. J'ai suggéré de retirer le gestionnaire A dans le gestionnaire de B: # Je change le # un gestionnaire est appelé # je désactive le gestionnaire de b # je change B (c'est Stocké, mais non déclenché) # Je active le gestionnaire du gestionnaire B # B, espérons-le, ne sera pas appelé. Sinon, envisagez de faire l'évolution d'un ouvrier d'arrière-plan.


En fait, j'ai fait plus - je supprime tous les gestionnaires :-) Mais cela n'a pas fonctionné (et après avoir traçable l'appel, je sais pourquoi).



0
votes

et vérifier si la nouvelle valeur est identique à celle de l'ancienne valeur n'est pas applicable ici (en raison de la fluctuation du calcul - je ne définit pas la même valeur à A et B, mais transformée).

Alors, ce que vous dites, c'est que quelque chose comme ça arrive:

  1. a.foo est défini sur x .
  2. a_foochanged définit b.bar à f ( x ).
  3. B_BRARCHANGED SETS A.FOO TO G (F ( X )), qui n'est pas x .
  4. a_foochanged définit b.bar à f (g (f ( x ))).

    et ainsi de suite. Est-ce correct? Parce que si g (f ( x ) est x , alors la solution est simple: b_barchanged ne doit que définir < code> a.foo si a.foo ! = g (f ( x ).

    supposant que cette récursive n'a pas de calculable État final, puis les gestionnaires d'événements ont besoin d'une manière de connaître le contexte dans lequel les événements qu'ils manipulent ont été déclenchés. Vous ne pouvez pas obtenir ces informations du protocole d'événement normal, car les événements sont conçus pour découpler des opérations que cette conception couplait .

    On dirait que vous avez besoin d'une manière hors bande pour que ces commandes se signent mutuellement. Cela pourrait être aussi simple que d'utiliser un hashset c'est un Propriété de la fenêtre . Je considérerais quelque chose comme ceci: xxx

    ceci rompre si un ensemble b.bar , et b ensembles c.baz et c ensembles a.foo , bien que je soupçonne que les exigences elles-mêmes se décomposent si cela se produit. Dans ce cas, vous Il suffit de recourir à une trace de pile. Ce n'est pas jolie, mais alors rien de ce problème est joli.


4 commentaires

Merci pour l'idée! A propos des fonctions, oui g (f (x)) ne doit pas être x (cela peut, mais je ne peux pas l'assumer). J'ai pensé au dictionnaire des objets et des événements, mais j'ai abandonné cette façon - le problème est quand g (f (x)) est x (je ne peux pas le prédire, pas de manière propre au moins ). Les événements sont déclenchés lorsque la nouvelle valeur est différente de celle de l'ancien - et avec ce dictionnaire, j'attendrai un événement à venir, mais cela ne viendra jamais parce que les valeurs sont égales.


Je ne comprends pas la dernière phrase. Il n'y a pas de "attente de l'événement" dans le scénario que j'ai décrit: b_barchanged vérifie le hashset . Si a_foochanged est dans le , puis b_barchanged sait qu'il est appelé dans le contexte de a.foo étant Modifié, et il ne devrait pas changer a.foo .


Regardez, vous déclenchez un, alors vous mettez à jour B - de sorte que vous définissez B pour être ignoré la prochaine fois. Mais ce que vous ne savez pas que b ne sera pas mis à jour car l'ancienne valeur = nouvelle valeur. Après un moment, B sera déclenché (à cause de l'interaction de l'utilisateur) et maintenant, il sera ignoré, car c'est la première fois que vous recevez un événement de B.


Alors vérifiez si f (a.foo) == b.bar et seulement la mise à jour si elle ne le fait pas.



0
votes

Une solution légèrement piraquine consiste à définir une période de temps d'annulation au cours desquelles vous ignorez les tireurs d'événements ultérieurs. Si vous faites cela, assurez-vous d'utiliser denttime.utcnow plutôt que datetime.now pour éviter une condition de limite d'économie de lumière du jour.

Si vous ne voulez pas de dépendance Sur le calendrier (les événements peuvent légitimement mettre à jour rapidement), vous pouvez utiliser un type de blocage similaire, bien que cela soit encore plus hacable: xxx


1 commentaires

Merci mais je n'irai pas dans cette direction - il demande beaucoup de problèmes. Ordinateur plus lent / plus rapide et tout se sépare.



0
votes

Parce que les positions sont à l'échelle ou modifiées autrement entre les valeurs de défilement, vous ne pouvez pas utiliser une simple liaison, mais un convertisseur fonctionne-t-il? XXX PRE>

Code où vous pouviez mettre en œuvre tout ce que vous pouvez mettre en œuvre les mathématiques nécessaire pour échelle entre les deux vues. P>

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


1 commentaires

Mais cela ne change rien vraiment. Les convertisseurs changent la valeur de sortie et la valeur de sortie déclenche à nouveau - cette fois. Vous avez montré un exemple avec une fonction réversible (négation mathématique) et c'est pourquoi cela fonctionne, pas à cause du convertisseur. Pour voir l'effet de la fonction irréversible avec chaque conversion, ajoutez un nombre aléatoire (-10, + 10).



0
votes

Je ne suis pas familier avec les commandes WPF, de sorte que les solutions suivantes pourraient ne pas être applicable:

  • mise à jour uniquement si la nouvelle valeur n'est pas la même que celle de l'ancienne valeur. Vous avez déjà indiqué que cela ne s'applique pas à votre cas puisque les valeurs sont toujours légèrement éteintes.
  • hack de garder un drapeau booléen en notifiant qui empêche le recours si déjà dans une notification. L'inconvénient peut que certaines notifications sont manquées (c'est-à-dire des clients non mis à jour) et que ce drapeau ne fonctionne pas si des notifications sont affichées (au lieu d'appelé directement).
  • Les commandes Windows font une distinction entre les actions de l'utilisateur qui soulèvent des événements et définissent des données par programme. La dernière catégorie ne notifie pas.
  • Utilisation du motif d'observateur (ou de médiateur) où les commandes ne se mettent pas à jour directement

0 commentaires